@dolphy-app/extension-sdk 0.3.0 → 0.4.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  SDK for extension authors: defineExtension, defineAnswerView, test helpers
4
4
 
5
- The package version equals the version of the Dolphy app release it was published from (0.3.0).
5
+ The package version equals the version of the Dolphy app release it was published from (0.4.0).
6
6
 
7
7
  `@dolphy-app/extension-sdk` — extension code (`defineExtension`,
8
8
  `defineExerciseType`), answer views (`defineAnswerView`), panels,
@@ -16,6 +16,20 @@ import { defineExtension } from '@dolphy-app/extension-sdk';
16
16
  import { loadExerciseType } from '@dolphy-app/extension-sdk/testing';
17
17
  ```
18
18
 
19
+ The package ships a guide in `docs/` (`node_modules/@dolphy-app/extension-sdk/docs/`
20
+ after the install): a
21
+ [quick start](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/quick-start.md), recipes for
22
+ [an exercise type](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-exercise-type.md),
23
+ [a theme](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-theme.md),
24
+ [a command and a panel](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-command-panel.md),
25
+ [events and storage](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-event-storage.md),
26
+ [settings](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-settings.md),
27
+ [an importer and an exporter](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-import-export.md),
28
+ [visibility conditions and dependencies](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-when-dependencies.md) and
29
+ [a panel on the UI kit](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-ui-kit.md), a path
30
+ [without a build](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/no-build.md) and notes on
31
+ [debugging](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/debugging.md).
32
+
19
33
  ## Installation
20
34
 
21
35
  ```sh
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as defineAnswerView, i as MountAnswerView, n as AnswerViewApi, r as AnswerViewInstance, t as AnswerView } from "./answer-view-D6wnyThb.js";
2
- import { CommandHandler, ExerciseTypeHandler, ExtensionContext as ExtensionContext$1, ExtensionIdSet, ExtensionModule, GradePolicyHandler, JsonValue, LearningEventHandler, MarkdownRenderContext, MarkdownRendererModule, NotifyEffect, OpenPanelEffect, PanelContext as PanelContext$1, PanelModule } from "@dolphy-app/extension-api";
2
+ import { CommandHandler, ExerciseTypeHandler, ExporterHandler, ExtensionContext as ExtensionContext$1, ExtensionIdSet, ExtensionModule, GradePolicyHandler, ImporterHandler, JsonValue, LearningEventHandler, MarkdownRenderContext, MarkdownRendererModule, NotifyEffect, OpenPanelEffect, PanelContext as PanelContext$1, PanelModule, ScheduleHandler, WidgetContext as WidgetContext$1, WidgetModule } from "@dolphy-app/extension-api";
3
3
  export * from "@dolphy-app/extension-api";
4
4
  //#region packages/extension-sdk/src/ids.d.ts
5
5
  /**
@@ -34,6 +34,8 @@ type HasGeneratedIds = [keyof ExtensionIds] extends [never] ? false : true;
34
34
  type ExtensionContext = ExtensionContext$1<ResolvedIds>;
35
35
  /** Context of a panel module; `call` accepts the declared command ids only. */
36
36
  type PanelContext = PanelContext$1<ResolvedIds['commands']>;
37
+ /** Context of a widget module; `call` accepts the declared command ids only. */
38
+ type WidgetContext = WidgetContext$1<ResolvedIds['commands']>;
37
39
  /**
38
40
  * A record that holds exactly the declared ids: a missing and an extra key are
39
41
  * both compile errors. With no generated declarations any keys are accepted;
@@ -48,6 +50,8 @@ type Exact<Id extends string, Value> = [HasGeneratedIds] extends [false] ? {
48
50
  type ExtensionViews = Exact<ResolvedIds['exerciseTypes'], AnswerView>;
49
51
  /** `export const panels = { … } satisfies ExtensionPanels`: one `defineExtensionPanel` per declared panel. */
50
52
  type ExtensionPanels = Exact<ResolvedIds['panels'], PanelModule<HTMLElement, ResolvedIds['commands']>>;
53
+ /** `export const widgets = { … } satisfies ExtensionWidgets`: one `defineExtensionWidget` per declared widget. */
54
+ type ExtensionWidgets = Exact<ResolvedIds['widgets'], WidgetModule<HTMLElement, ResolvedIds['commands']>>;
51
55
  /** `export const markdown = { … } satisfies ExtensionMarkdown`: one `defineMarkdownRenderer` per declared language. */
52
56
  type ExtensionMarkdown = Exact<ResolvedIds['markdownLanguages'], MarkdownRendererModule<HTMLElement>>;
53
57
  //#endregion
@@ -66,6 +70,7 @@ interface InActivate {
66
70
  /**
67
71
  * A record value in `defineExtension` that says "this id is registered in
68
72
  * `activate`" (`ctx.commands.register`, `ctx.events.on`,
73
+ * `ctx.importers.register`, `ctx.exporters.register`, `ctx.schedule.on`,
69
74
  * `ctx.registerExerciseType`, `ctx.registerGradePolicy`) rather than by a
70
75
  * handler in the record. Needed because the records must name every declared
71
76
  * id: a handler that needs `ctx` is written in `activate`, and its id gets this
@@ -85,11 +90,12 @@ type Ids = ResolvedIds;
85
90
  /** Learning-event handlers by event name; the events must be declared in `contributes.events`, the `learning.events` permission is needed. */
86
91
  type EventHandlers = { readonly [N in Ids['events']]: LearningEventHandler<N> | InActivate; };
87
92
  /**
88
- * What `defineExtension` takes. `exerciseTypes`, `gradePolicies`, `events` and
89
- * `commands` name every id `extension.json` declares for them, exactly: a
90
- * handler, or `inActivate` for an id that `activate` registers.
93
+ * What `defineExtension` takes. `exerciseTypes`, `gradePolicies`, `events`,
94
+ * `commands`, `schedules`, `importers` and `exporters` name every id
95
+ * `extension.json` declares for them, exactly: a handler, or `inActivate` for
96
+ * an id that `activate` registers.
91
97
  */
92
- type ExtensionDefinition = Section<'exerciseTypes', Ids['exerciseTypes'], { readonly [K in Ids['exerciseTypes']]: ExerciseTypeHandler | InActivate; }> & Section<'gradePolicies', Ids['gradePolicies'], { readonly [K in Ids['gradePolicies']]: GradePolicyHandler | InActivate; }> & Section<'events', Ids['events'], EventHandlers> & Section<'commands', Ids['commands'], { readonly [K in Ids['commands']]: CommandHandler | InActivate; }> & {
98
+ type ExtensionDefinition = Section<'exerciseTypes', Ids['exerciseTypes'], { readonly [K in Ids['exerciseTypes']]: ExerciseTypeHandler | InActivate; }> & Section<'gradePolicies', Ids['gradePolicies'], { readonly [K in Ids['gradePolicies']]: GradePolicyHandler | InActivate; }> & Section<'events', Ids['events'], EventHandlers> & Section<'commands', Ids['commands'], { readonly [K in Ids['commands']]: CommandHandler | InActivate; }> & Section<'schedules', Ids['schedules'], { readonly [K in Ids['schedules']]: ScheduleHandler | InActivate; }> & Section<'importers', Ids['importers'], { readonly [K in Ids['importers']]: ImporterHandler | InActivate; }> & Section<'exporters', Ids['exporters'], { readonly [K in Ids['exporters']]: ExporterHandler | InActivate; }> & {
93
99
  /** Runs after the records are registered. */
94
100
  activate?(context: ExtensionContext): void | Promise<void>;
95
101
  deactivate?(): void | Promise<void>;
@@ -104,5 +110,7 @@ export declare const defineMarkdownRenderer: (render: (source: string, container
104
110
  //#region packages/extension-sdk/src/panel.d.ts
105
111
  /** An entry of `panels[<panel id>]` in `src/index.ts` (`contributes.panels`); `ctx.call` accepts the declared command ids. */
106
112
  export declare const defineExtensionPanel: (module: PanelModule<HTMLElement, ResolvedIds["commands"]>) => PanelModule<HTMLElement, ResolvedIds["commands"]>;
113
+ /** An entry of `widgets[<widget id>]` in `src/index.ts` (`contributes.widgets`); `ctx.call` accepts the declared command ids. */
114
+ export declare const defineExtensionWidget: (module: WidgetModule<HTMLElement, ResolvedIds["commands"]>) => WidgetModule<HTMLElement, ResolvedIds["commands"]>;
107
115
  //#endregion
108
- export { type AnswerView, type AnswerViewApi, type AnswerViewInstance, type EventHandlers, type ExtensionContext, type ExtensionDefinition, type ExtensionIds, type ExtensionMarkdown, type ExtensionPanels, type ExtensionViews, type InActivate, type MountAnswerView, type PanelContext, defineAnswerView };
116
+ export { type AnswerView, type AnswerViewApi, type AnswerViewInstance, type EventHandlers, type ExtensionContext, type ExtensionDefinition, type ExtensionIds, type ExtensionMarkdown, type ExtensionPanels, type ExtensionViews, type ExtensionWidgets, type InActivate, type MountAnswerView, type PanelContext, type WidgetContext, defineAnswerView };
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ const openPanel = (panelId, props) => props === void 0 ? { openPanel: panelId }
14
14
  /**
15
15
  * A record value in `defineExtension` that says "this id is registered in
16
16
  * `activate`" (`ctx.commands.register`, `ctx.events.on`,
17
+ * `ctx.importers.register`, `ctx.exporters.register`, `ctx.schedule.on`,
17
18
  * `ctx.registerExerciseType`, `ctx.registerGradePolicy`) rather than by a
18
19
  * handler in the record. Needed because the records must name every declared
19
20
  * id: a handler that needs `ctx` is written in `activate`, and its id gets this
@@ -40,6 +41,9 @@ const defineExtension = /* @__NO_SIDE_EFFECTS__ */ (declared) => {
40
41
  for (const [id, handler] of handlersOf(definition.gradePolicies)) registrations.push(context.registerGradePolicy(id, handler));
41
42
  for (const [name, handler] of handlersOf(definition.events)) registrations.push(context.events.on(name, handler));
42
43
  for (const [id, handler] of handlersOf(definition.commands)) registrations.push(context.commands.register(id, handler));
44
+ for (const [id, handler] of handlersOf(definition.schedules)) registrations.push(context.schedule.on(id, handler));
45
+ for (const [id, handler] of handlersOf(definition.importers)) registrations.push(context.importers.register(id, handler));
46
+ for (const [id, handler] of handlersOf(definition.exporters)) registrations.push(context.exporters.register(id, handler));
43
47
  };
44
48
  const activate = async (context) => {
45
49
  try {
@@ -86,6 +90,8 @@ const defineMarkdownRenderer = /* @__NO_SIDE_EFFECTS__ */ (render) => ({ render
86
90
  //#region packages/extension-sdk/src/panel.ts
87
91
  /** An entry of `panels[<panel id>]` in `src/index.ts` (`contributes.panels`); `ctx.call` accepts the declared command ids. */
88
92
  const defineExtensionPanel = /* @__NO_SIDE_EFFECTS__ */ (module) => module;
93
+ /** An entry of `widgets[<widget id>]` in `src/index.ts` (`contributes.widgets`); `ctx.call` accepts the declared command ids. */
94
+ const defineExtensionWidget = /* @__NO_SIDE_EFFECTS__ */ (module) => module;
89
95
 
90
96
  //#endregion
91
- export { defineAnswerView, defineExerciseType, defineExtension, defineExtensionPanel, defineMarkdownRenderer, inActivate, notify, openPanel };
97
+ export { defineAnswerView, defineExerciseType, defineExtension, defineExtensionPanel, defineExtensionWidget, defineMarkdownRenderer, inActivate, notify, openPanel };
package/dist/runtime.d.ts CHANGED
@@ -1,14 +1,17 @@
1
1
  import { t as AnswerView } from "./answer-view-D6wnyThb.js";
2
- import { MarkdownRendererModule, PanelModule } from "@dolphy-app/extension-api";
2
+ import { MarkdownRendererModule, PanelModule, WidgetModule } from "@dolphy-app/extension-api";
3
3
  //#region packages/extension-sdk/src/answer-element.d.ts
4
4
  /** Defines the kind's custom element; calling again with the same tag changes nothing. */
5
5
  export declare const registerAnswerView: (tag: string, view: AnswerView) => void;
6
6
  //#endregion
7
7
  //#region packages/extension-sdk/src/runtime.d.ts
8
8
  type PanelEntry = PanelModule<HTMLElement>;
9
+ type WidgetEntry = WidgetModule<HTMLElement>;
9
10
  type MarkdownEntry = MarkdownRendererModule<HTMLElement>;
10
11
  /** Panel module that selects the `panels` entry by `ctx.panelId` (for a file shared by several panels). */
11
12
  export declare const dispatchPanels: (panels: Readonly<Record<string, PanelEntry>>) => PanelEntry;
13
+ /** Widget module that selects the `widgets` entry by `ctx.widgetId` (for a file shared by several widgets). */
14
+ export declare const dispatchWidgets: (widgets: Readonly<Record<string, WidgetEntry>>) => WidgetEntry;
12
15
  /** Renderer module that selects the `markdown` entry by block language. */
13
16
  export declare const dispatchMarkdown: (renderers: Readonly<Record<string, MarkdownEntry>>) => MarkdownEntry;
14
17
  //#endregion
package/dist/runtime.js CHANGED
@@ -7,6 +7,12 @@ const dispatchPanels = (panels) => ({ mount(container, context) {
7
7
  if (panel === void 0) throw new Error(`panel '${context.panelId}' is not exported`);
8
8
  return panel.mount(container, context);
9
9
  } });
10
+ /** Widget module that selects the `widgets` entry by `ctx.widgetId` (for a file shared by several widgets). */
11
+ const dispatchWidgets = (widgets) => ({ mount(container, context) {
12
+ const widget = widgets[context.widgetId];
13
+ if (widget === void 0) throw new Error(`widget '${context.widgetId}' is not exported`);
14
+ return widget.mount(container, context);
15
+ } });
10
16
  /** Renderer module that selects the `markdown` entry by block language. */
11
17
  const dispatchMarkdown = (renderers) => ({ render(source, container, context) {
12
18
  const renderer = renderers[context.language];
@@ -15,4 +21,4 @@ const dispatchMarkdown = (renderers) => ({ render(source, container, context) {
15
21
  } });
16
22
 
17
23
  //#endregion
18
- export { dispatchMarkdown, dispatchPanels, registerAnswerView };
24
+ export { dispatchMarkdown, dispatchPanels, dispatchWidgets, registerAnswerView };
package/dist/testing.d.ts CHANGED
@@ -1,5 +1,5 @@
1
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";
2
+ import { AnswerChangeDetail, AnswerElementProps, CommandOutcome, ExportInput, ExportResult, ExtensionCommands, ExtensionEvents, ExtensionExporters, ExtensionImporters, ExtensionLogger, ExtensionModule, ExtensionNotification, ExtensionNotifications, ExtensionSchedule, ExtensionSecrets, ExtensionSettings, ExtensionStats, ExtensionStorage, GradePolicyInput, GradeResult, GradeValue, ImportInput, ImportResult, ImporterInputKind, JsonSchema, JsonValue, LearningEventName, LearningEventPayloads, LibraryReader, PanelContextInfo, PanelModule, SettingContribution, SettingValue, WidgetModule } from "@dolphy-app/extension-api";
3
3
  //#region packages/extension-sdk/src/testing.d.ts
4
4
  export declare const createMemoryLibrary: (files: Readonly<Record<string, string>>) => LibraryReader;
5
5
  /**
@@ -8,6 +8,22 @@ export declare const createMemoryLibrary: (files: Readonly<Record<string, string
8
8
  * JSON text and returned as copies; on rejection nothing changes.
9
9
  */
10
10
  export declare const createMemoryStorage: () => ExtensionStorage;
11
+ /** In-memory secrets for tests: `ExtensionSecrets` plus a switch that imitates a missing system key store. */
12
+ export interface MemorySecrets extends ExtensionSecrets {
13
+ /** Imitates the system key store becoming (un)available; stored values are kept. */
14
+ setAvailable(available: boolean): void;
15
+ }
16
+ export interface MemorySecretsOptions {
17
+ /** Default `true`; `false` imitates Linux `basic_text`, no key store, or an app that is not ready. */
18
+ available?: boolean;
19
+ }
20
+ /**
21
+ * In-memory secrets with the engine's limits and errors
22
+ * (`EXTENSION_SECRET_LIMITS`, `StorageQuotaError`, `SecretsUnavailableError`):
23
+ * without a key store `set` and `get` of an existing key throw, `get` of a
24
+ * missing key gives `undefined` and `delete` works.
25
+ */
26
+ export declare const createMemorySecrets: (options?: MemorySecretsOptions) => MemorySecrets;
11
27
  export interface MemorySettings extends ExtensionSettings {
12
28
  /**
13
29
  * Changes the value as the user does in the dialog: the value is validated
@@ -52,14 +68,136 @@ export interface MemoryCommandsOptions {
52
68
  }
53
69
  /** In-memory commands: the same registration rules and result parsing as the host. */
54
70
  export declare const createMemoryCommands: (options?: MemoryCommandsOptions) => MemoryCommands;
71
+ export interface MemorySchedule extends ExtensionSchedule {
72
+ /**
73
+ * Fires a schedule the way the host does and awaits the handler: resolves
74
+ * `true` once the handler returned. With no subscription, or while the
75
+ * handler of the previous firing is still running, the firing is skipped and
76
+ * resolves `false`. Unlike the host, a handler failure is not swallowed but
77
+ * rejects the promise, and the 10 s handler timeout is not applied.
78
+ */
79
+ fire(id: string): Promise<boolean>;
80
+ /** Subscribed schedules in subscription order. */
81
+ ids(): string[];
82
+ }
83
+ export interface MemoryScheduleOptions {
84
+ /** Schedules from `contributes.schedules`: subscribing to another throws, as in the host. Unset — any are allowed. */
85
+ declared?: readonly string[];
86
+ }
87
+ /** In-memory schedule subscriptions: one handler per schedule and no overlapping firings, as in the host. */
88
+ export declare const createMemorySchedule: (options?: MemoryScheduleOptions) => MemorySchedule;
89
+ export interface MemoryImporters extends ExtensionImporters {
90
+ /**
91
+ * Runs a registered importer the way the host does: the input must have the
92
+ * form the importer declares (`text` unless `input: 'bytes'`) and at most
93
+ * `EXTENSION_TRANSFER_LIMITS.inputBytes`; the result goes through the host's
94
+ * rules (`normalizeImportResult`: paths, sizes, number of files). An
95
+ * unregistered importer and an invalid result reject the promise. The 30 s
96
+ * handler timeout is not applied.
97
+ */
98
+ run(id: string, input: ImportInput): Promise<ImportResult>;
99
+ /** Registered importers in registration order. */
100
+ ids(): string[];
101
+ }
102
+ export interface MemoryImportersOptions {
103
+ /** Importers from `contributes.importers`: registering another throws and `input` is checked, as in the host. Unset — any are allowed. */
104
+ declaredImporters?: readonly {
105
+ id: string;
106
+ input?: ImporterInputKind;
107
+ }[];
108
+ }
109
+ /** In-memory importers: the same registration rules and result checks as the host. */
110
+ export declare const createMemoryImporters: (options?: MemoryImportersOptions) => MemoryImporters;
111
+ export interface MemoryExporters extends ExtensionExporters {
112
+ /**
113
+ * Runs a registered exporter the way the host does: the input must match the
114
+ * exporter's declared `scope` and a course snapshot is at most
115
+ * `EXTENSION_TRANSFER_LIMITS.totalBytes`; the result goes through the host's
116
+ * rules (`normalizeExportResult`: file name, size, `text` xor `bytes`). An
117
+ * unregistered exporter and an invalid result reject the promise. The 30 s
118
+ * handler timeout is not applied.
119
+ */
120
+ run(id: string, input: ExportInput): Promise<ExportResult>;
121
+ /** Registered exporters in registration order. */
122
+ ids(): string[];
123
+ }
124
+ export interface MemoryExportersOptions {
125
+ /** Exporters from `contributes.exporters`: registering another throws and `scope` is checked, as in the host. Unset — any are allowed. */
126
+ declaredExporters?: readonly {
127
+ id: string;
128
+ scope: ExportInput['scope'];
129
+ }[];
130
+ }
131
+ /** In-memory exporters: the same registration rules and result checks as the host. */
132
+ export declare const createMemoryExporters: (options?: MemoryExportersOptions) => MemoryExporters;
133
+ export interface MemoryStatsAttempt {
134
+ /** When the attempt happened: epoch milliseconds, a `Date`, or an ISO-8601 string. */
135
+ at: number | Date | string;
136
+ /** Grade 1-5; 3 and higher counts as correct, as in the app. */
137
+ grade: number;
138
+ /** Course of the attempt, for the `courseId` filter. Without it the attempt belongs to no course. */
139
+ courseId?: string;
140
+ }
141
+ export interface MemoryStatsOptions {
142
+ /** Attempts the statistics start with; more arrive through `record`. */
143
+ attempts?: readonly MemoryStatsAttempt[];
144
+ /** IANA time zone whose local days are counted. Defaults to the time zone of this process. */
145
+ timeZone?: string;
146
+ /** The current time, for the `current` streak. Defaults to `Date.now`. */
147
+ now?: () => number;
148
+ /** false — every call rejects with `PermissionError('learning.stats')`, as for an extension without the permission. Defaults to true. */
149
+ permitted?: boolean;
150
+ }
151
+ export interface MemoryStats extends ExtensionStats {
152
+ /** Adds an attempt to the history. */
153
+ record(attempt: MemoryStatsAttempt): void;
154
+ }
155
+ /**
156
+ * In-memory statistics with the semantics of the app: local days in a time zone,
157
+ * correct at grade 3 or higher, `current` streak not broken while today has no
158
+ * attempts yet, one `daily` entry per date (at most `EXTENSION_STATS_LIMITS.dailyDays`).
159
+ */
160
+ export declare const createMemoryStats: (options?: MemoryStatsOptions) => MemoryStats;
161
+ export interface MemoryNotificationsOptions {
162
+ /** false — every call rejects with `PermissionError('notifications')`, as for an extension without the permission. Defaults to true. */
163
+ permitted?: boolean;
164
+ /** false — the operating system does not support notifications: `show` resolves `false`. Defaults to true. */
165
+ supported?: boolean;
166
+ /** false — the user switched notifications off for the extension: `show` resolves `false`. Defaults to true. */
167
+ enabled?: boolean;
168
+ /** The current time for the rate windows. Defaults to `Date.now`. */
169
+ now?: () => number;
170
+ }
171
+ export interface MemoryNotifications extends ExtensionNotifications {
172
+ /** Notifications handed to the (imitated) operating system, oldest first, as the app shows them: sanitized text. */
173
+ readonly shown: readonly ExtensionNotification[];
174
+ /** The user's switch "Notifications" of the extension. */
175
+ setEnabled(enabled: boolean): void;
176
+ /** Whether the imitated operating system supports notifications. */
177
+ setSupported(supported: boolean): void;
178
+ }
179
+ /**
180
+ * In-memory notifications with the engine's rules: the text is sanitized and
181
+ * limited, `perMinute`/`perHour` windows throw `NotificationRateLimitError`,
182
+ * a switched-off extension or an unsupporting system resolves `false` (and
183
+ * does not use the rate limit). `shown` is the log the app's fake notifier
184
+ * would keep.
185
+ */
186
+ export declare const createMemoryNotifications: (options?: MemoryNotificationsOptions) => MemoryNotifications;
55
187
  /** What a test replaces in the extension context; by default everything is in memory and silent. */
56
188
  export interface LoadOptions {
57
189
  library?: LibraryReader;
58
190
  logger?: ExtensionLogger;
59
191
  storage?: ExtensionStorage;
192
+ secrets?: ExtensionSecrets;
60
193
  settings?: ExtensionSettings;
61
194
  events?: ExtensionEvents;
62
195
  commands?: ExtensionCommands;
196
+ importers?: ExtensionImporters;
197
+ exporters?: ExtensionExporters;
198
+ stats?: ExtensionStats;
199
+ notifications?: ExtensionNotifications;
200
+ schedule?: ExtensionSchedule;
63
201
  }
64
202
  export declare const createSchemaValidator: (schema: JsonSchema) => (value: unknown) => string[];
65
203
  export interface LoadedExerciseType {
@@ -95,12 +233,14 @@ export interface LoadedEvents {
95
233
  /** Events are delivered as in the host: to the subscribed handler, one at a time. See `MemoryEvents.emit`. */
96
234
  emit: MemoryEvents['emit'];
97
235
  storage: ExtensionStorage;
236
+ secrets: ExtensionSecrets;
98
237
  settings: MemorySettings;
99
238
  /** Deactivates the extension module. */
100
239
  dispose(): Promise<void>;
101
240
  }
102
- export interface LoadEventsOptions extends Omit<LoadOptions, 'storage' | 'settings' | 'events'>, MemoryEventsOptions {
241
+ export interface LoadEventsOptions extends Omit<LoadOptions, 'storage' | 'secrets' | 'settings' | 'events'>, MemoryEventsOptions {
103
242
  storage?: ExtensionStorage;
243
+ secrets?: ExtensionSecrets;
104
244
  /** Definitions from the manifest's `contributes.settings`; values are read and changed through `settings`. */
105
245
  settings?: readonly SettingContribution[];
106
246
  /** User values in place of `default`. */
@@ -120,6 +260,33 @@ export interface LoadedCommands {
120
260
  export interface LoadCommandsOptions extends Omit<LoadOptions, 'commands'>, MemoryCommandsOptions {}
121
261
  /** Activates the module with in-memory commands and lets the test invoke them like the host. */
122
262
  export declare const loadCommands: (module: ExtensionModule, options?: LoadCommandsOptions) => Promise<LoadedCommands>;
263
+ export interface LoadedImporters {
264
+ run: MemoryImporters['run'];
265
+ ids: MemoryImporters['ids'];
266
+ /** Deactivates the extension module. */
267
+ dispose(): Promise<void>;
268
+ }
269
+ export interface LoadImportersOptions extends Omit<LoadOptions, 'importers'>, MemoryImportersOptions {}
270
+ /** Activates the module with in-memory importers and lets the test run them like the host. */
271
+ export declare const loadImporters: (module: ExtensionModule, options?: LoadImportersOptions) => Promise<LoadedImporters>;
272
+ export interface LoadedExporters {
273
+ run: MemoryExporters['run'];
274
+ ids: MemoryExporters['ids'];
275
+ /** Deactivates the extension module. */
276
+ dispose(): Promise<void>;
277
+ }
278
+ export interface LoadExportersOptions extends Omit<LoadOptions, 'exporters'>, MemoryExportersOptions {}
279
+ /** Activates the module with in-memory exporters and lets the test run them like the host; `stats` feeds `ctx.stats` of a progress exporter. */
280
+ export declare const loadExporters: (module: ExtensionModule, options?: LoadExportersOptions) => Promise<LoadedExporters>;
281
+ export interface LoadedSchedules {
282
+ fire: MemorySchedule['fire'];
283
+ ids: MemorySchedule['ids'];
284
+ /** Deactivates the extension module. */
285
+ dispose(): Promise<void>;
286
+ }
287
+ export interface LoadSchedulesOptions extends Omit<LoadOptions, 'schedule'>, MemoryScheduleOptions {}
288
+ /** Activates the module with in-memory schedules and lets the test fire them like the host. */
289
+ export declare const loadSchedules: (module: ExtensionModule, options?: LoadSchedulesOptions) => Promise<LoadedSchedules>;
123
290
  export interface LoadViewOptions extends Partial<AnswerElementProps> {
124
291
  /** `aria-label` of the host element, as the app sets it. */
125
292
  label?: string;
@@ -152,11 +319,21 @@ export interface LoadPanelOptions {
152
319
  props?: JsonValue;
153
320
  /** Reply to `ctx.call`; by default the call is rejected. */
154
321
  call?: (commandId: string, args: JsonValue | undefined) => JsonValue | undefined | Promise<JsonValue | undefined>;
322
+ /** The surroundings the frame starts with (`ctx.context`); defaults to all courses (`courseId: null`). */
323
+ context?: PanelContextInfo;
324
+ /** Where to mount; defaults to a new `div` in `document.body`. */
325
+ container?: HTMLElement;
326
+ }
327
+ export interface LoadWidgetOptions {
328
+ /** Reply to `ctx.call`; by default the call is rejected. */
329
+ call?: LoadPanelOptions['call'];
330
+ /** The surroundings the frame starts with (`ctx.context`); defaults to all courses (`courseId: null`). */
331
+ context?: PanelContextInfo;
155
332
  /** Where to mount; defaults to a new `div` in `document.body`. */
156
333
  container?: HTMLElement;
157
334
  }
158
- export interface LoadedPanel {
159
- /** Container the panel received in `mount`. */
335
+ export interface LoadedFrame {
336
+ /** Container the module received in `mount`. */
160
337
  readonly container: HTMLElement;
161
338
  /** `ctx.call` invocations in order. */
162
339
  readonly calls: readonly {
@@ -165,11 +342,18 @@ export interface LoadedPanel {
165
342
  }[];
166
343
  /** Whether `ctx.signal` was aborted (after `dispose()`). */
167
344
  readonly aborted: boolean;
168
- /** Sends new properties to the panel (`ctx.onProps`). */
169
- setProps(props: JsonValue | undefined): void;
345
+ /** The app focused another course: updates `ctx.context` and notifies `ctx.onContextChange` subscribers. */
346
+ setContext(context: PanelContextInfo): void;
170
347
  /** Closes the frame: aborts `ctx.signal` and removes the container. */
171
348
  dispose(): void;
172
349
  }
350
+ export interface LoadedPanel extends LoadedFrame {
351
+ /** Sends new properties to the panel (`ctx.onProps`). */
352
+ setProps(props: JsonValue | undefined): void;
353
+ }
354
+ export type LoadedWidget = LoadedFrame;
173
355
  /** Mounts a panel from `panels[id]` in the test DOM environment with the same context the frame provides. */
174
356
  export declare const loadPanel: (panels: Readonly<Record<string, PanelModule<HTMLElement>>>, id: string, options?: LoadPanelOptions) => Promise<LoadedPanel>;
357
+ /** Mounts a widget from `widgets[id]` in the test DOM environment with the same context the frame provides. */
358
+ export declare const loadWidget: (widgets: Readonly<Record<string, WidgetModule<HTMLElement>>>, id: string, options?: LoadWidgetOptions) => Promise<LoadedWidget>;
175
359
  //#endregion