@dolphy-app/extension-sdk 0.3.0 → 0.5.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/dist/testing.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { t as AnswerView } from "./answer-view-D6wnyThb.js";
2
- import { AnswerChangeDetail, AnswerElementProps, CommandOutcome, ExtensionCommands, ExtensionEvents, ExtensionLogger, ExtensionModule, ExtensionSettings, ExtensionStorage, GradePolicyInput, GradeResult, GradeValue, JsonSchema, JsonValue, LearningEventName, LearningEventPayloads, LibraryReader, PanelModule, SettingContribution, SettingValue } from "@dolphy-app/extension-api";
1
+ import { t as ExtensionEngine } from "./index-I_CXC6Lj.js";
2
+ import { a as MountContext$1, i as InjectionRegistration$1, l as ServerEntry$1, o as Mountable, r as ClientEntry, s as PanelRegistration$1, t as AppApi$1 } from "./define-entry-lsuxzKdD.js";
3
+ import { AnswerViewProps, AppLocale, AppTheme, ClientCommandRegistration, CommandOutcome, Disposable, ExportInput, ExportResult, ExtensionHookName, ExtensionLogger, ExtensionNotification, ExtensionNotifications, ExtensionSecrets, ExtensionSettings, ExtensionStats, ExtensionStorage, GradePolicyInput, GradeResult, GradeValue, HookRequest, HookResponse, ImportInput, ImportResult, JsonSchema, JsonValue, LearningEventName, LearningEventPayloads, LibraryReader, MarkdownBlockProps, RpcContract, ServerRegistration, SettingDefinition, SettingValue, ThemeRegistration } from "@dolphy-app/extension-api";
4
+ import { Component } from "vue";
3
5
  //#region packages/extension-sdk/src/testing.d.ts
4
6
  export declare const createMemoryLibrary: (files: Readonly<Record<string, string>>) => LibraryReader;
5
7
  /**
@@ -8,6 +10,22 @@ export declare const createMemoryLibrary: (files: Readonly<Record<string, string
8
10
  * JSON text and returned as copies; on rejection nothing changes.
9
11
  */
10
12
  export declare const createMemoryStorage: () => ExtensionStorage;
13
+ /** In-memory secrets for tests: `ExtensionSecrets` plus a switch that imitates a missing system key store. */
14
+ export interface MemorySecrets extends ExtensionSecrets {
15
+ /** Imitates the system key store becoming (un)available; stored values are kept. */
16
+ setAvailable(available: boolean): void;
17
+ }
18
+ export interface MemorySecretsOptions {
19
+ /** Default `true`; `false` imitates Linux `basic_text`, no key store, or an app that is not ready. */
20
+ available?: boolean;
21
+ }
22
+ /**
23
+ * In-memory secrets with the engine's limits and errors
24
+ * (`EXTENSION_SECRET_LIMITS`, `StorageQuotaError`, `SecretsUnavailableError`):
25
+ * without a key store `set` and `get` of an existing key throw, `get` of a
26
+ * missing key gives `undefined` and `delete` works.
27
+ */
28
+ export declare const createMemorySecrets: (options?: MemorySecretsOptions) => MemorySecrets;
11
29
  export interface MemorySettings extends ExtensionSettings {
12
30
  /**
13
31
  * Changes the value as the user does in the dialog: the value is validated
@@ -15,54 +33,82 @@ export interface MemorySettings extends ExtensionSettings {
15
33
  * Unlike the host, a handler failure is not swallowed but rejects the promise.
16
34
  */
17
35
  set(id: string, value: SettingValue): Promise<void>;
36
+ /** Adds definitions, as `server.registerSettings` does; an id registered twice throws. The returned `Disposable` removes them. */
37
+ register(definitions: readonly SettingDefinition[]): Disposable;
18
38
  }
19
- /** In-memory settings from manifest definitions; `initial` provides user values in place of `default`. */
20
- export declare const createMemorySettings: (definitions: readonly SettingContribution[], initial?: Readonly<Record<string, SettingValue>>) => MemorySettings;
21
- export interface MemoryEvents extends ExtensionEvents {
22
- /**
23
- * Sends the event to the subscribed handler and awaits it. With no subscription,
24
- * the event is skipped, as in the host. Unlike the host, a handler failure
25
- * is not swallowed but rejects the promise, and the 2 s handler timeout is not applied.
26
- */
27
- emit<N extends LearningEventName>(name: N, payload: LearningEventPayloads[N]): Promise<void>;
39
+ /**
40
+ * In-memory settings. `definitions` are registered at once, more come through
41
+ * `register`; `initial` provides user values in place of `default`, applied
42
+ * when the setting with that id is registered.
43
+ */
44
+ export declare const createMemorySettings: (definitions?: readonly SettingDefinition[], initial?: Readonly<Record<string, SettingValue>>) => MemorySettings;
45
+ export interface MemoryStatsAttempt {
46
+ /** When the attempt happened: epoch milliseconds, a `Date`, or an ISO-8601 string. */
47
+ at: number | Date | string;
48
+ /** Grade 1-5; 3 and higher counts as correct, as in the app. */
49
+ grade: number;
50
+ /** Course of the attempt, for the `courseId` filter. Without it the attempt belongs to no course. */
51
+ courseId?: string;
28
52
  }
29
- export interface MemoryEventsOptions {
30
- /** Events from `contributes.events`: subscribing to another throws, as in the host. Unset — any are allowed. */
31
- declared?: readonly LearningEventName[];
32
- /** false — subscribing throws `PermissionError`, as for an extension without `learning.events`. Defaults to true. */
33
- permitted?: boolean;
53
+ export interface MemoryStatsOptions {
54
+ /** Attempts the statistics start with; more arrive through `record`. */
55
+ attempts?: readonly MemoryStatsAttempt[];
56
+ /** IANA time zone whose local days are counted. Defaults to the time zone of this process. */
57
+ timeZone?: string;
58
+ /** The current time, for the `current` streak. Defaults to `Date.now`. */
59
+ now?: () => number;
34
60
  }
35
- /** In-memory learning event subscriptions: one handler per event, as in the host. */
36
- export declare const createMemoryEvents: (options?: MemoryEventsOptions) => MemoryEvents;
37
- export interface MemoryCommands extends ExtensionCommands {
38
- /**
39
- * Runs a registered command the way the host does: the same
40
- * argument and result bounds, the same result normalization. An unregistered
41
- * command and an invalid result reject the promise. The 10 s handler timeout is not applied.
42
- */
43
- run(id: string, args?: JsonValue): Promise<CommandOutcome>;
44
- /** Registered commands in registration order. */
45
- ids(): string[];
46
- }
47
- export interface MemoryCommandsOptions {
48
- /** Commands from `contributes.commands`: registering another throws, as in the host. Unset — any are allowed. */
49
- declaredCommands?: readonly string[];
50
- /** Panels from `contributes.panels`: `openPanel` on another is invalid, as in the host. Unset — any. */
51
- declaredPanels?: readonly string[];
52
- }
53
- /** In-memory commands: the same registration rules and result parsing as the host. */
54
- export declare const createMemoryCommands: (options?: MemoryCommandsOptions) => MemoryCommands;
55
- /** What a test replaces in the extension context; by default everything is in memory and silent. */
56
- export interface LoadOptions {
61
+ export interface MemoryStats extends ExtensionStats {
62
+ /** Adds an attempt to the history. */
63
+ record(attempt: MemoryStatsAttempt): void;
64
+ }
65
+ /**
66
+ * In-memory statistics with the semantics of the app: local days in a time zone,
67
+ * correct at grade 3 or higher, `current` streak not broken while today has no
68
+ * attempts yet, one `daily` entry per date (at most `EXTENSION_STATS_LIMITS.dailyDays`).
69
+ */
70
+ export declare const createMemoryStats: (options?: MemoryStatsOptions) => MemoryStats;
71
+ export interface MemoryNotificationsOptions {
72
+ /** false — the operating system does not support notifications: `show` resolves `false`. Defaults to true. */
73
+ supported?: boolean;
74
+ /** false — the user switched notifications off for the extension: `show` resolves `false`. Defaults to true. */
75
+ enabled?: boolean;
76
+ /** The current time for the rate windows. Defaults to `Date.now`. */
77
+ now?: () => number;
78
+ }
79
+ export interface MemoryNotifications extends ExtensionNotifications {
80
+ /** Notifications handed to the (imitated) operating system, oldest first, as the app shows them: sanitized text. */
81
+ readonly shown: readonly ExtensionNotification[];
82
+ /** The user's switch "Notifications" of the extension. */
83
+ setEnabled(enabled: boolean): void;
84
+ /** Whether the imitated operating system supports notifications. */
85
+ setSupported(supported: boolean): void;
86
+ }
87
+ /**
88
+ * In-memory notifications with the engine's rules: the text is sanitized and
89
+ * limited, `perMinute`/`perHour` windows throw `NotificationRateLimitError`,
90
+ * a switched-off extension or an unsupporting system resolves `false` (and
91
+ * does not use the rate limit). `shown` is the log the app's fake notifier
92
+ * would keep.
93
+ */
94
+ export declare const createMemoryNotifications: (options?: MemoryNotificationsOptions) => MemoryNotifications;
95
+ export declare const createSchemaValidator: (schema: JsonSchema) => (value: unknown) => string[];
96
+ /** What a test replaces in the context of the server part; by default everything is in memory and silent. */
97
+ export interface TestServerOptions {
98
+ /** Default `test`. When set, every registered id must be equal to it or start with `<extensionId>.`, as the host checks. */
99
+ extensionId?: string;
57
100
  library?: LibraryReader;
58
101
  logger?: ExtensionLogger;
59
102
  storage?: ExtensionStorage;
60
- settings?: ExtensionSettings;
61
- events?: ExtensionEvents;
62
- commands?: ExtensionCommands;
103
+ secrets?: MemorySecrets;
104
+ stats?: MemoryStats;
105
+ notifications?: MemoryNotifications;
106
+ /** User values of settings in place of `default`, by setting id; every id must be registered by the entry. */
107
+ settingValues?: Readonly<Record<string, SettingValue>>;
108
+ /** The engine `ctx.engine` gives to the entry; by default every use of it throws, as the test server has no engine. */
109
+ engine?: ExtensionEngine;
63
110
  }
64
- export declare const createSchemaValidator: (schema: JsonSchema) => (value: unknown) => string[];
65
- export interface LoadedExerciseType {
111
+ export interface TestExerciseType {
66
112
  project(spec: unknown, options?: {
67
113
  exerciseId?: string;
68
114
  }): Promise<unknown>;
@@ -81,95 +127,176 @@ export interface LoadedExerciseType {
81
127
  } | {
82
128
  found: false;
83
129
  }>;
84
- /** Deactivates the extension module. */
85
- dispose(): Promise<void>;
86
130
  }
87
- export declare const loadExerciseType: (module: ExtensionModule, type: string, options?: LoadOptions) => Promise<LoadedExerciseType>;
88
- export interface LoadedGradePolicy {
131
+ export interface TestGradePolicy {
89
132
  evaluate(input: GradePolicyInput): Promise<GradeValue | null>;
90
- /** Deactivates the extension module. */
133
+ }
134
+ export interface TestImporter {
135
+ /**
136
+ * Runs the importer the way the host does: the input must have the form the
137
+ * importer declares and at most `EXTENSION_TRANSFER_LIMITS.inputBytes`; the
138
+ * result goes through `normalizeImportResult`. An invalid result rejects the
139
+ * promise. The handler timeout is not applied.
140
+ */
141
+ run(input: ImportInput): Promise<ImportResult>;
142
+ }
143
+ export interface TestExporter {
144
+ /**
145
+ * Runs the exporter the way the host does: the input must match the
146
+ * exporter's `scope` and a course snapshot is at most
147
+ * `EXTENSION_TRANSFER_LIMITS.totalBytes`; the result goes through
148
+ * `normalizeExportResult`. An invalid result rejects the promise. The handler
149
+ * timeout is not applied.
150
+ */
151
+ run(input: ExportInput): Promise<ExportResult>;
152
+ }
153
+ /** The server part of an extension, started on in-memory fakes. */
154
+ export interface TestServer {
155
+ readonly extensionId: string;
156
+ /** What the entry registered, as the host's registrar hands it to the engine (checked for types and unique ids only: the host checks the rest). */
157
+ readonly registration: ServerRegistration;
158
+ readonly library: LibraryReader;
159
+ readonly storage: ExtensionStorage;
160
+ readonly secrets: MemorySecrets;
161
+ readonly settings: MemorySettings;
162
+ readonly stats: MemoryStats;
163
+ readonly notifications: MemoryNotifications;
164
+ /** The engine of the context: `options.engine`. */
165
+ readonly engine: ExtensionEngine;
166
+ readonly commands: {
167
+ /**
168
+ * Runs a registered command the way the host does: the same argument and
169
+ * result bounds, the same result normalization. An unregistered command
170
+ * and an invalid result reject the promise. The handler timeout is not applied.
171
+ */
172
+ run(id: string, args?: JsonValue): Promise<CommandOutcome>;
173
+ };
174
+ readonly events: {
175
+ /**
176
+ * Sends the event to the subscribed handler and awaits it. With no
177
+ * subscription the event is skipped, as in the host. Unlike the host, a
178
+ * handler failure is not swallowed but rejects the promise, and the
179
+ * handler timeout is not applied.
180
+ */
181
+ emit<N extends LearningEventName>(name: N, payload: LearningEventPayloads[N]): Promise<void>;
182
+ };
183
+ /**
184
+ * Calls the handler registered with `server.before` the way the host does:
185
+ * the request must pass `EXTENSION_HOOKS[name].request` and the response
186
+ * `EXTENSION_HOOKS[name].response`. An unregistered hook, a schema
187
+ * violation and an error of the handler reject the promise. The handler
188
+ * timeout is not applied, and no other extension's handlers run.
189
+ */
190
+ hook<N extends ExtensionHookName>(name: N, request: HookRequest<N>): Promise<HookResponse<N>>;
191
+ readonly schedule: {
192
+ /**
193
+ * Fires a registered schedule the way the host does and awaits the
194
+ * handler: resolves `true` once the handler returned, `false` while the
195
+ * handler of the previous firing is still running. An unregistered
196
+ * schedule rejects the promise. Unlike the host, a handler failure is not
197
+ * swallowed but rejects the promise.
198
+ */
199
+ fire(id: string): Promise<boolean>;
200
+ };
201
+ exerciseType(id: string): TestExerciseType;
202
+ gradePolicy(id: string): TestGradePolicy;
203
+ importer(id: string): TestImporter;
204
+ exporter(id: string): TestExporter;
205
+ /**
206
+ * Calls the handler registered with `server.handle` under `contract.name`
207
+ * the way the host does: the input must serialize to at most
208
+ * `EXTENSION_RPC_LIMITS.inputChars` characters and pass the input schema
209
+ * the handler registered; the result must pass the output schema. An
210
+ * unregistered contract, a schema violation and an error of the handler
211
+ * reject the promise. The handler timeout is not applied.
212
+ */
213
+ rpc<Input, Output>(contract: RpcContract<Input, Output>, input: Input): Promise<Output>;
214
+ /** Runs the cleanup the entry returned and removes the registrations. */
91
215
  dispose(): Promise<void>;
92
216
  }
93
- export declare const loadGradePolicy: (module: ExtensionModule, id: string, options?: LoadOptions) => Promise<LoadedGradePolicy>;
94
- export interface LoadedEvents {
95
- /** Events are delivered as in the host: to the subscribed handler, one at a time. See `MemoryEvents.emit`. */
96
- emit: MemoryEvents['emit'];
97
- storage: ExtensionStorage;
98
- settings: MemorySettings;
99
- /** Deactivates the extension module. */
217
+ /**
218
+ * Starts `entry` (the `server` export of an extension) with in-memory
219
+ * storage, secrets, settings, statistics, notifications and library, and
220
+ * returns a harness to call what it registered. Registration is all or
221
+ * nothing, as in the host: when `entry` throws, so does `createTestServer`.
222
+ */
223
+ export declare const createTestServer: (entry: ServerEntry$1, options?: TestServerOptions) => Promise<TestServer>;
224
+ /** An answer view as registered: a Vue component or a `Mountable`. */
225
+ export type AnswerViewComponent = Component | Mountable<AnswerViewProps>;
226
+ /** A markdown renderer as registered: a Vue component or a `Mountable`. */
227
+ export type MarkdownRendererComponent = Component | Mountable<MarkdownBlockProps>;
228
+ /** The client part of an extension, started on a recording context. */
229
+ export interface TestClient {
230
+ readonly extensionId: string;
231
+ readonly panels: readonly PanelRegistration$1[];
232
+ /** Injections as registered, with `position` defaulted to `append`. */
233
+ readonly injections: readonly Required<InjectionRegistration$1>[];
234
+ /** Answer views by exercise type id, as registered (a Vue component or a `Mountable`). */
235
+ readonly answerViews: ReadonlyMap<string, AnswerViewComponent>;
236
+ /** Markdown renderers by block language, as registered (a Vue component or a `Mountable`). */
237
+ readonly markdownRenderers: ReadonlyMap<string, MarkdownRendererComponent>;
238
+ readonly themes: readonly ThemeRegistration[];
239
+ readonly commands: readonly ClientCommandRegistration[];
240
+ /** Runs the cleanup the entry returned and removes the registrations. */
100
241
  dispose(): Promise<void>;
101
242
  }
102
- export interface LoadEventsOptions extends Omit<LoadOptions, 'storage' | 'settings' | 'events'>, MemoryEventsOptions {
103
- storage?: ExtensionStorage;
104
- /** Definitions from the manifest's `contributes.settings`; values are read and changed through `settings`. */
105
- settings?: readonly SettingContribution[];
106
- /** User values in place of `default`. */
107
- settingValues?: Readonly<Record<string, SettingValue>>;
243
+ export interface TestClientOptions {
244
+ /** Default `test`. When set, every registered id must be equal to it or start with `<extensionId>.`, as the window checks. */
245
+ extensionId?: string;
246
+ /** The window API `client.app` gives to the entry; by default every use of it throws, as the test client has no window. */
247
+ app?: AppApi$1;
248
+ /** The engine `client.engine` gives to the entry; by default every use of it throws. */
249
+ engine?: ExtensionEngine;
108
250
  }
109
251
  /**
110
- * Activates the module with in-memory storage, settings, and events, and lets the test
111
- * send events and change settings.
252
+ * Starts `entry` (the `client` export of an extension) on a context that
253
+ * records what it adds, so a test can mount the components and read the
254
+ * themes and commands. An id added twice and an injection with a bad target
255
+ * or position fail like in the window.
112
256
  */
113
- export declare const loadEvents: (module: ExtensionModule, options?: LoadEventsOptions) => Promise<LoadedEvents>;
114
- export interface LoadedCommands {
115
- run: MemoryCommands['run'];
116
- ids: MemoryCommands['ids'];
117
- /** Deactivates the extension module. */
118
- dispose(): Promise<void>;
257
+ export declare const createTestClient: (entry: ClientEntry, options?: TestClientOptions) => Promise<TestClient>;
258
+ /** What `mountForTest` gives `ctx`; everything is optional except `props`. */
259
+ export interface MountForTestOptions<Props, Handle = undefined> {
260
+ /** The first `ctx.props`. */
261
+ props: Props;
262
+ /** The element to draw into; by default a new `<div>` of the global `document`. Required without a DOM. */
263
+ el?: HTMLElement;
264
+ /** `ctx.handle`: a panel or injection handle; `undefined` by default. */
265
+ handle?: Handle;
266
+ /** `ctx.app`; by default every use of it throws, as the test has no window. */
267
+ app?: AppApi$1;
268
+ /** `ctx.engine`, the engine of `ctx.callRpc`; by default every use of it throws. */
269
+ engine?: ExtensionEngine;
270
+ /** Default `{ id: 'light', dark: false }`. */
271
+ theme?: AppTheme;
272
+ /** Default `en`. */
273
+ locale?: AppLocale;
274
+ /** Default `test`. */
275
+ extensionId?: string;
119
276
  }
120
- export interface LoadCommandsOptions extends Omit<LoadOptions, 'commands'>, MemoryCommandsOptions {}
121
- /** Activates the module with in-memory commands and lets the test invoke them like the host. */
122
- export declare const loadCommands: (module: ExtensionModule, options?: LoadCommandsOptions) => Promise<LoadedCommands>;
123
- export interface LoadViewOptions extends Partial<AnswerElementProps> {
124
- /** `aria-label` of the host element, as the app sets it. */
125
- label?: string;
126
- /** Where to mount; defaults to a new `div` in `document.body`. */
127
- container?: HTMLElement;
128
- }
129
- export interface LoadedView {
130
- /** The kind's custom element, as the app creates it. */
131
- readonly element: HTMLElement;
132
- /** The element's shadow root: the view renders its UI here. */
133
- readonly root: ShadowRoot;
134
- /** `dolphy-answer-change` events in order. */
135
- readonly changes: readonly AnswerChangeDetail[];
136
- /** How many times the view asked to submit the answer (`dolphy-answer-submit`). */
137
- readonly submissions: number;
138
- /** Sets element properties and waits for the view to apply the update. */
139
- update(props: Partial<AnswerElementProps>): Promise<void>;
140
- query<E extends Element = Element>(selector: string): E | null;
141
- queryAll<E extends Element = Element>(selector: string): E[];
142
- /** Removes the element from the document; the view receives `destroy()`. */
143
- dispose(): void;
277
+ /** A `Mountable` mounted by `mountForTest`. */
278
+ export interface MountedForTest<Props, Handle = undefined> {
279
+ readonly el: HTMLElement;
280
+ /** The context the `Mountable` got. */
281
+ readonly ctx: MountContext$1<Props, Handle>;
282
+ /** Replaces `ctx.props` and calls the `onProps` listeners. */
283
+ setProps(next: Props): void;
284
+ /** Replaces `ctx.theme` and calls the `onTheme` listeners. */
285
+ setTheme(next: AppTheme): void;
286
+ /** Replaces `ctx.locale` and calls the `onLocale` listeners. */
287
+ setLocale(next: AppLocale): void;
288
+ /** Every `ctx.emit` call, in order: `[event, payload]`. */
289
+ readonly emitted: readonly (readonly [string, unknown])[];
290
+ /** Every `ctx.reportError` call, in order. */
291
+ readonly errors: readonly unknown[];
292
+ /** Aborts `ctx.signal` and runs the cleanup `mount` returned, once. */
293
+ unmount(): Promise<void>;
144
294
  }
145
295
  /**
146
- * Mounts a view from `views[id]` in the test DOM environment with the same element
147
- * the app creates (test tags are issued; the manifest `element` is not needed).
296
+ * Mounts a `Mountable` into an element on a recording context, so a test can
297
+ * change the props, the theme and the language, read what the component
298
+ * emitted and reported, and unmount it. Rejects with what `mount` throws. Needs
299
+ * a DOM (`document`) or `options.el`.
148
300
  */
149
- export declare const loadView: (views: Readonly<Record<string, AnswerView>>, id: string, options?: LoadViewOptions) => Promise<LoadedView>;
150
- export interface LoadPanelOptions {
151
- /** Properties the panel was opened with (`openPanel(id, props)`). */
152
- props?: JsonValue;
153
- /** Reply to `ctx.call`; by default the call is rejected. */
154
- call?: (commandId: string, args: JsonValue | undefined) => JsonValue | undefined | Promise<JsonValue | undefined>;
155
- /** Where to mount; defaults to a new `div` in `document.body`. */
156
- container?: HTMLElement;
157
- }
158
- export interface LoadedPanel {
159
- /** Container the panel received in `mount`. */
160
- readonly container: HTMLElement;
161
- /** `ctx.call` invocations in order. */
162
- readonly calls: readonly {
163
- commandId: string;
164
- args: JsonValue | undefined;
165
- }[];
166
- /** Whether `ctx.signal` was aborted (after `dispose()`). */
167
- readonly aborted: boolean;
168
- /** Sends new properties to the panel (`ctx.onProps`). */
169
- setProps(props: JsonValue | undefined): void;
170
- /** Closes the frame: aborts `ctx.signal` and removes the container. */
171
- dispose(): void;
172
- }
173
- /** Mounts a panel from `panels[id]` in the test DOM environment with the same context the frame provides. */
174
- export declare const loadPanel: (panels: Readonly<Record<string, PanelModule<HTMLElement>>>, id: string, options?: LoadPanelOptions) => Promise<LoadedPanel>;
301
+ export declare const mountForTest: <Props, Handle = undefined>(mountable: Mountable<Props, Handle>, options: MountForTestOptions<Props, Handle>) => Promise<MountedForTest<Props, Handle>>;
175
302
  //#endregion