@dolphy-app/extension-sdk 0.4.0 → 0.6.0
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/README.md +28 -14
- package/dist/client.d.ts +40 -0
- package/dist/client.js +67 -0
- package/dist/define-entry-BTz2F3qv.js +22 -0
- package/dist/define-entry-lsuxzKdD.d.ts +77 -0
- package/dist/index-I_CXC6Lj.d.ts +1958 -0
- package/dist/index.d.ts +7 -111
- package/dist/index.js +5 -86
- package/dist/react.d.ts +51 -0
- package/dist/react.js +112 -0
- package/dist/rpc.d.ts +28 -0
- package/dist/rpc.js +37 -0
- package/dist/testing.d.ts +184 -241
- package/dist/testing.js +574 -464
- package/docs/debugging.md +82 -64
- package/docs/no-build.md +84 -48
- package/docs/quick-start.md +78 -56
- package/docs/recipe-command-panel.md +265 -118
- package/docs/recipe-event-storage.md +188 -128
- package/docs/recipe-exercise-type.md +271 -183
- package/docs/recipe-hooks.md +158 -0
- package/docs/recipe-import-export.md +48 -53
- package/docs/recipe-mountable.md +322 -0
- package/docs/recipe-react.md +352 -0
- package/docs/recipe-rpc-and-app.md +366 -0
- package/docs/recipe-settings.md +122 -100
- package/docs/recipe-theme.md +72 -40
- package/docs/recipe-when-dependencies.md +188 -82
- package/package.json +29 -7
- package/dist/answer-element-BOQcuxYh.js +0 -114
- package/dist/answer-view-D6wnyThb.d.ts +0 -28
- package/dist/runtime.d.ts +0 -17
- package/dist/runtime.js +0 -24
- package/docs/recipe-ui-kit.md +0 -172
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Recipe: a command and a panel
|
|
2
2
|
|
|
3
3
|
A command appears in the command palette and runs your code; a panel is a
|
|
4
|
-
screen
|
|
5
|
-
`command-panel` template
|
|
4
|
+
screen the command can open: a Vue single-file component the app draws inside
|
|
5
|
+
its own window. This recipe is the `command-panel` template
|
|
6
6
|
(`npx @dolphy-app/create-extension <dir> --id acme.hello --template command-panel`).
|
|
7
7
|
The files below are exactly what the generator writes for the id `acme.hello`.
|
|
8
8
|
See [quick-start.md](quick-start.md) for the commands.
|
|
@@ -20,190 +20,337 @@ File `extension.json` (command-panel):
|
|
|
20
20
|
"name": "Hello panel",
|
|
21
21
|
"description": "Palette commands that greet the learner and open a small panel.",
|
|
22
22
|
"author": "your-github-login",
|
|
23
|
-
"tags": ["productivity"]
|
|
24
|
-
"contributes": {
|
|
25
|
-
"commands": [
|
|
26
|
-
{ "id": "acme.hello.hello", "title": "Say hello", "category": "Hello" },
|
|
27
|
-
{ "id": "acme.hello.open", "title": "Open the hello panel", "category": "Hello" },
|
|
28
|
-
{ "id": "acme.hello.data", "title": "Hello panel data", "palette": false }
|
|
29
|
-
],
|
|
30
|
-
"panels": [{ "id": "acme.hello.view", "title": "Hello" }]
|
|
31
|
-
}
|
|
23
|
+
"tags": ["productivity"]
|
|
32
24
|
}
|
|
33
25
|
```
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
panel calls.
|
|
38
|
-
- `panels[]` declares the screen; its module defaults to `./panel.mjs`, which
|
|
39
|
-
the build writes.
|
|
40
|
-
- No permissions: commands, panels and `notify` need none.
|
|
27
|
+
The commands and the panel are not in the manifest: the two parts register
|
|
28
|
+
them. `dolphy-ext build` writes `main` and `client` into the built manifest.
|
|
41
29
|
|
|
42
|
-
## The
|
|
30
|
+
## The server part
|
|
43
31
|
|
|
44
32
|
File `src/index.ts` (command-panel):
|
|
45
33
|
|
|
46
34
|
```ts
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
'
|
|
35
|
+
export { client } from './client.ts';
|
|
36
|
+
export { server } from './server.ts';
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
File `src/server.ts` (command-panel):
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { defineServer, notify, openPanel } from '@dolphy-app/extension-sdk';
|
|
43
|
+
|
|
44
|
+
// runs in the extension host: every call registers a contribution
|
|
45
|
+
export const server = defineServer((s) => {
|
|
46
|
+
// palette command: shows a notification
|
|
47
|
+
s.registerCommand({
|
|
48
|
+
id: 'acme.hello.hello',
|
|
49
|
+
title: { en: 'Say hello', ru: 'Поздороваться' },
|
|
50
|
+
category: 'Hello',
|
|
51
|
+
run: (args) => {
|
|
62
52
|
const name = typeof args === 'string' ? args : 'world';
|
|
63
53
|
return notify(`Hello, ${name}!`);
|
|
64
54
|
},
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// palette command: opens the panel (registered by the client) with properties
|
|
58
|
+
s.registerCommand({
|
|
59
|
+
id: 'acme.hello.open',
|
|
60
|
+
title: { en: 'Open the hello panel', ru: 'Открыть панель' },
|
|
61
|
+
category: 'Hello',
|
|
62
|
+
run: () => openPanel('acme.hello.view', { name: 'Dolphy' }),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// hidden from the palette (palette: false): the panel asks for data
|
|
66
|
+
s.registerCommand({
|
|
67
|
+
id: 'acme.hello.data',
|
|
68
|
+
title: 'Hello panel data',
|
|
69
|
+
palette: false,
|
|
70
|
+
run: () => ({ message: 'Hello from acme.hello' }),
|
|
71
|
+
});
|
|
70
72
|
});
|
|
73
|
+
```
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
}
|
|
75
|
+
- `server.registerCommand({ id, title, run })` adds a command. `title` and
|
|
76
|
+
`category` are what the palette shows (`LocalizedText`: a string or
|
|
77
|
+
`{ en, ru }`). A command with `palette: false` is hidden from the palette: it
|
|
78
|
+
is a handler only your panel calls.
|
|
79
|
+
- `run(args)` returns what the app does next: `notify(text)` shows a
|
|
80
|
+
notification, `openPanel(id, props)` opens a panel with properties, any JSON
|
|
81
|
+
value is data for the caller, nothing is fine. `args` is whatever the caller
|
|
82
|
+
passes, so check its type. A handler has 10 seconds.
|
|
83
|
+
- Optional fields of a command: `description`, `icon`, `keybindings` and `when`
|
|
84
|
+
(see [visibility conditions](recipe-when-dependencies.md)).
|
|
85
|
+
|
|
86
|
+
## The client part
|
|
87
|
+
|
|
88
|
+
File `src/client.ts` (command-panel):
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { defineClient } from '@dolphy-app/extension-sdk';
|
|
92
|
+
import StatusPanel from './StatusPanel.vue';
|
|
93
|
+
|
|
94
|
+
// runs in the app window: the panel is a Vue component the app draws
|
|
95
|
+
export const client = defineClient((c) => {
|
|
96
|
+
c.addPanel({
|
|
97
|
+
id: 'acme.hello.view',
|
|
98
|
+
title: { en: 'Hello', ru: 'Привет' },
|
|
99
|
+
component: StatusPanel,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
File `src/StatusPanel.vue` (command-panel):
|
|
105
|
+
|
|
106
|
+
```vue
|
|
107
|
+
<script setup lang="ts">
|
|
108
|
+
import { usePanel } from '@dolphy-app/extension-sdk/client';
|
|
109
|
+
import { computed, ref } from 'vue';
|
|
110
|
+
|
|
111
|
+
// `usePanel()` gives the panel the properties it was opened with and `call`
|
|
112
|
+
// for the commands of the extension
|
|
113
|
+
const panel = usePanel();
|
|
114
|
+
const message = ref('');
|
|
115
|
+
// the app opens the panel again with new properties: `panel.props` is
|
|
116
|
+
// reactive, the title follows it
|
|
117
|
+
const name = computed(() => {
|
|
118
|
+
const { props } = panel;
|
|
119
|
+
return typeof props === 'object' && props !== null && 'name' in props
|
|
120
|
+
? String(props.name)
|
|
121
|
+
: 'world';
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const load = async () => {
|
|
125
|
+
const data = await panel.call('acme.hello.data');
|
|
126
|
+
message.value = (data as { message: string }).message;
|
|
127
|
+
};
|
|
128
|
+
void load();
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<template>
|
|
132
|
+
<section class="status-panel">
|
|
133
|
+
<h2>Hello, {{ name }}!</h2>
|
|
134
|
+
<p>{{ message }}</p>
|
|
135
|
+
<v-btn color="primary" @click="load">Reload</v-btn>
|
|
136
|
+
</section>
|
|
137
|
+
</template>
|
|
138
|
+
|
|
139
|
+
<style scoped>
|
|
140
|
+
.status-panel {
|
|
141
|
+
padding: 16px;
|
|
142
|
+
}
|
|
143
|
+
</style>
|
|
98
144
|
```
|
|
99
145
|
|
|
100
|
-
- `
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
`
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
-
|
|
146
|
+
- `client.addPanel({ id, title, component })` adds the screen and its entry in
|
|
147
|
+
the sidebar menu. The component is a Vue component, here a single-file
|
|
148
|
+
component (`.vue`), or a `Mountable` that draws with another framework (see
|
|
149
|
+
[recipe-mountable.md](recipe-mountable.md) and [recipe-react.md](recipe-react.md)).
|
|
150
|
+
`vue` is the app's own instance, so the panel shares its theme and language.
|
|
151
|
+
`openPanel('<id>', props)` of a command opens it.
|
|
152
|
+
- `header: false` (app 0.6.0 and later) removes the app's page header (back
|
|
153
|
+
button, title, extension id): the panel fills the page and draws its own
|
|
154
|
+
`<h1>`. The app moves focus to the panel container, labelled with `title`.
|
|
155
|
+
- A `.vue` file is built as it is: `<script setup lang="ts">`, `<template>`
|
|
156
|
+
and `<style>`. Vuetify components are written as tags (`<v-btn>`,
|
|
157
|
+
`<v-card>`) and Vuetify directives as `v-ripple`, with no import: the build
|
|
158
|
+
turns the ones a template uses into imports from the app's own Vuetify, so
|
|
159
|
+
the theme and the language of the app apply and the bundle does not carry
|
|
160
|
+
Vuetify. A `.vue` file in the server part is a build error.
|
|
161
|
+
- A `<style>` block is not tied to the panel: when `client.mjs` loads, the
|
|
162
|
+
styles of all its components go into one `<style data-dolphy-ext="<extension
|
|
163
|
+
id>">` tag of the window document, so a plain `<style>` reaches the whole
|
|
164
|
+
window. Write `<style scoped>`, as the template does. `<style module>` is
|
|
165
|
+
not supported.
|
|
166
|
+
- `pnpm typecheck` runs `vue-tsc --noEmit`: it checks the `<script setup
|
|
167
|
+
lang="ts">` and the `<template>` of a `.vue` file, which plain `tsc` does not
|
|
168
|
+
look into, and knows `.vue` imports without a `declare module` shim.
|
|
169
|
+
- Inside the component `usePanel()` from `@dolphy-app/extension-sdk/client`
|
|
170
|
+
returns the handle: `panelId`, the reactive `props` the panel was opened with
|
|
171
|
+
(a repeated `openPanel` with new properties updates them in place, so a
|
|
172
|
+
`computed` over `panel.props` follows), the reactive `context`
|
|
173
|
+
(`{ courseId }`) and `call(commandId, args)`.
|
|
174
|
+
- The code of a panel runs in the app window, and `call` is how it reaches the
|
|
175
|
+
commands of the server part, including those with `palette: false`. That is
|
|
176
|
+
why the data command exists.
|
|
177
|
+
- `client.addCommand({ id, title, run })` adds a command whose handler runs in
|
|
178
|
+
the window instead; `run` takes no arguments and returns nothing.
|
|
179
|
+
- The build writes `server` to `main.mjs` and `client` to `client.mjs`.
|
|
112
180
|
|
|
113
181
|
## The tests
|
|
114
182
|
|
|
183
|
+
File `vitest.config.ts` (command-panel):
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import vue from '@vitejs/plugin-vue';
|
|
187
|
+
import { defineConfig } from 'vitest/config';
|
|
188
|
+
|
|
189
|
+
export default defineConfig({ plugins: [vue()] });
|
|
190
|
+
```
|
|
191
|
+
|
|
115
192
|
File `test/index.test.ts` (command-panel):
|
|
116
193
|
|
|
117
194
|
```ts
|
|
118
195
|
// @vitest-environment happy-dom
|
|
196
|
+
import { PANEL_HANDLE_KEY } from '@dolphy-app/extension-sdk';
|
|
197
|
+
import type { JsonValue, PanelHandle } from '@dolphy-app/extension-sdk';
|
|
119
198
|
import {
|
|
120
|
-
|
|
121
|
-
|
|
199
|
+
createTestClient,
|
|
200
|
+
createTestServer,
|
|
122
201
|
} from '@dolphy-app/extension-sdk/testing';
|
|
123
202
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
124
|
-
import {
|
|
203
|
+
import { createApp, defineComponent, h, nextTick, shallowReactive } from 'vue';
|
|
204
|
+
import { client, server } from '../src/index.ts';
|
|
205
|
+
import StatusPanel from '../src/StatusPanel.vue';
|
|
125
206
|
|
|
126
207
|
const disposables: { dispose(): unknown }[] = [];
|
|
127
208
|
afterEach(async () => {
|
|
128
209
|
await Promise.all(disposables.splice(0).map((item) => item.dispose()));
|
|
129
210
|
});
|
|
130
211
|
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
136
|
-
disposables.push(commands);
|
|
137
|
-
return commands;
|
|
212
|
+
const start = async () => {
|
|
213
|
+
const running = await createTestServer(server, { extensionId: 'acme.hello' });
|
|
214
|
+
disposables.push(running);
|
|
215
|
+
return running;
|
|
138
216
|
};
|
|
139
217
|
|
|
140
|
-
describe('acme.hello:
|
|
218
|
+
describe('acme.hello: server', () => {
|
|
141
219
|
it('hello greets the name from the arguments, "world" without them', async () => {
|
|
142
|
-
const
|
|
143
|
-
expect(await commands.run('acme.hello.hello', 'Ada')).toEqual({
|
|
220
|
+
const running = await start();
|
|
221
|
+
expect(await running.commands.run('acme.hello.hello', 'Ada')).toEqual({
|
|
144
222
|
kind: 'notify',
|
|
145
223
|
text: 'Hello, Ada!',
|
|
146
224
|
});
|
|
147
|
-
expect(await commands.run('acme.hello.hello')).toEqual({
|
|
225
|
+
expect(await running.commands.run('acme.hello.hello')).toEqual({
|
|
148
226
|
kind: 'notify',
|
|
149
227
|
text: 'Hello, world!',
|
|
150
228
|
});
|
|
151
229
|
});
|
|
152
230
|
|
|
153
231
|
it('open asks the app to open the panel with properties', async () => {
|
|
154
|
-
const
|
|
155
|
-
expect(await commands.run('acme.hello.open')).toEqual({
|
|
232
|
+
const running = await start();
|
|
233
|
+
expect(await running.commands.run('acme.hello.open')).toEqual({
|
|
156
234
|
kind: 'openPanel',
|
|
157
235
|
panelId: 'acme.hello.view',
|
|
158
236
|
props: { name: 'Dolphy' },
|
|
159
237
|
});
|
|
160
238
|
});
|
|
161
239
|
|
|
162
|
-
it('data returns what the panel shows', async () => {
|
|
163
|
-
const
|
|
164
|
-
expect(await commands.run('acme.hello.data')).toEqual({
|
|
240
|
+
it('data returns what the panel shows and stays out of the palette', async () => {
|
|
241
|
+
const running = await start();
|
|
242
|
+
expect(await running.commands.run('acme.hello.data')).toEqual({
|
|
165
243
|
kind: 'data',
|
|
166
244
|
value: { message: 'Hello from acme.hello' },
|
|
167
245
|
});
|
|
246
|
+
const hidden = running.registration.commands.find(
|
|
247
|
+
(command) => command.id === 'acme.hello.data',
|
|
248
|
+
);
|
|
249
|
+
expect(hidden?.palette).toBe(false);
|
|
168
250
|
});
|
|
169
251
|
});
|
|
170
252
|
|
|
253
|
+
describe('acme.hello: client', () => {
|
|
254
|
+
it('adds the panel that the open command points to', async () => {
|
|
255
|
+
const running = await createTestClient(client, { extensionId: 'acme.hello' });
|
|
256
|
+
disposables.push(running);
|
|
257
|
+
expect(running.panels.map((panel) => panel.id)).toEqual(['acme.hello.view']);
|
|
258
|
+
expect(running.panels[0]?.component).toBe(StatusPanel);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// the app draws `<v-btn>` with its Vuetify; the test gives the panel a plain button
|
|
263
|
+
const VBtn = defineComponent({
|
|
264
|
+
setup: (_props, { slots }) => () => h('button', slots['default']?.()),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// draws the panel the way the app does: the handle is provided to the component
|
|
268
|
+
const mountPanel = async (
|
|
269
|
+
props: JsonValue | undefined,
|
|
270
|
+
call: PanelHandle['call'],
|
|
271
|
+
) => {
|
|
272
|
+
const handle = shallowReactive({
|
|
273
|
+
panelId: 'acme.hello.view',
|
|
274
|
+
props,
|
|
275
|
+
context: { courseId: null },
|
|
276
|
+
call,
|
|
277
|
+
});
|
|
278
|
+
const host = document.createElement('div');
|
|
279
|
+
document.body.append(host);
|
|
280
|
+
const app = createApp({ render: () => h(StatusPanel) });
|
|
281
|
+
app.component('v-btn', VBtn);
|
|
282
|
+
app.provide(PANEL_HANDLE_KEY, handle);
|
|
283
|
+
app.mount(host);
|
|
284
|
+
disposables.push({
|
|
285
|
+
dispose: () => {
|
|
286
|
+
app.unmount();
|
|
287
|
+
host.remove();
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
// the panel asks a command: wait for the reply, then for the redraw
|
|
291
|
+
const settle = async () => {
|
|
292
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
293
|
+
await nextTick();
|
|
294
|
+
};
|
|
295
|
+
await settle();
|
|
296
|
+
return {
|
|
297
|
+
host,
|
|
298
|
+
reopen: async (next: JsonValue) => {
|
|
299
|
+
handle.props = next;
|
|
300
|
+
await settle();
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
|
|
171
305
|
describe('acme.hello: panel', () => {
|
|
172
306
|
it('shows the data command reply and follows new properties', async () => {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
307
|
+
const calls: string[] = [];
|
|
308
|
+
const panel = await mountPanel({ name: 'Ada' }, async (commandId) => {
|
|
309
|
+
calls.push(commandId);
|
|
310
|
+
return { message: 'Hello from the test' };
|
|
176
311
|
});
|
|
177
|
-
|
|
178
|
-
expect(panel.
|
|
179
|
-
expect(panel.container.querySelector('p')?.textContent).toBe(
|
|
312
|
+
expect(panel.host.querySelector('h2')?.textContent).toBe('Hello, Ada!');
|
|
313
|
+
expect(panel.host.querySelector('p')?.textContent).toBe(
|
|
180
314
|
'Hello from the test',
|
|
181
315
|
);
|
|
182
|
-
expect(
|
|
316
|
+
expect(calls).toEqual(['acme.hello.data']);
|
|
183
317
|
|
|
184
|
-
panel.
|
|
185
|
-
expect(panel.
|
|
186
|
-
'Hello, Grace!',
|
|
187
|
-
);
|
|
318
|
+
await panel.reopen({ name: 'Grace' });
|
|
319
|
+
expect(panel.host.querySelector('h2')?.textContent).toBe('Hello, Grace!');
|
|
188
320
|
});
|
|
189
321
|
|
|
190
|
-
it('
|
|
191
|
-
const
|
|
192
|
-
|
|
322
|
+
it('asks the data command again when the button is pressed', async () => {
|
|
323
|
+
const calls: string[] = [];
|
|
324
|
+
const panel = await mountPanel(undefined, async (commandId) => {
|
|
325
|
+
calls.push(commandId);
|
|
326
|
+
return { message: 'x' };
|
|
193
327
|
});
|
|
194
|
-
|
|
195
|
-
panel.
|
|
196
|
-
expect(
|
|
197
|
-
|
|
198
|
-
|
|
328
|
+
panel.host.querySelector('button')?.click();
|
|
329
|
+
await panel.reopen({});
|
|
330
|
+
expect(calls).toEqual(['acme.hello.data', 'acme.hello.data']);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('greets the world when it is opened without properties', async () => {
|
|
334
|
+
const panel = await mountPanel(undefined, async () => ({ message: 'x' }));
|
|
335
|
+
expect(panel.host.querySelector('h2')?.textContent).toBe('Hello, world!');
|
|
199
336
|
});
|
|
200
337
|
});
|
|
201
338
|
```
|
|
202
339
|
|
|
203
|
-
`
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
340
|
+
`createTestServer(server, { extensionId })` runs commands as the host does,
|
|
341
|
+
with the same rules for results: `running.commands.run(id, args)` resolves to
|
|
342
|
+
`{ kind: 'notify' | 'openPanel' | 'data' | 'none', … }`. `running.registration`
|
|
343
|
+
lists what the server registered, which is how the test sees that `data` is
|
|
344
|
+
hidden from the palette. `createTestClient(client, { extensionId })` records the
|
|
345
|
+
panels, so the test checks that the id the `open` command points to exists.
|
|
346
|
+
|
|
347
|
+
A panel is tested like any Vue component: `createApp` mounts it in `happy-dom`,
|
|
348
|
+
and `app.provide(PANEL_HANDLE_KEY, handle)` gives it the handle the app would
|
|
349
|
+
provide. The `call` of the handle answers `panel.call`; here it is a stub, and
|
|
350
|
+
in the events recipe it is wired to the real handlers. `vitest.config.ts` adds
|
|
351
|
+
`@vitejs/plugin-vue` so that the test can import the `.vue` file. The app gives
|
|
352
|
+
`<v-btn>` its Vuetify; the test registers a plain button under that name
|
|
353
|
+
(`app.component('v-btn', …)`) and tests the panel, not Vuetify.
|
|
207
354
|
|
|
208
355
|
## Try and ship
|
|
209
356
|
|