@pie-players/pie-assessment-player 0.3.75 → 1.0.0-beta.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 +446 -219
- package/custom-elements.json +521 -0
- package/dist/components/AssessmentPlayerDefaultElement.d.ts +25 -0
- package/dist/components/AssessmentPlayerShellElement.d.ts +11 -0
- package/dist/components/assessment-section-player-runtime.d.ts +0 -1
- package/dist/controller/AssessmentController.d.ts +5 -0
- package/dist/{nl-NL-CfezB59b.js → nl-NL-DqnT-1mq.js} +34 -6
- package/dist/pie-assessment-player.js +451 -368
- package/dist/types.d.ts +3 -8
- package/package.json +9 -18
package/README.md
CHANGED
|
@@ -1,209 +1,427 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
# @pie-players/pie-assessment-player
|
|
2
|
+
|
|
3
|
+
`<pie-assessment-player-default>` is the reference assessment player: a
|
|
4
|
+
multi-section player assembled only from the public exports of the section
|
|
5
|
+
player, the assessment toolkit and `@pie-players/pie-players-shared`. It serves
|
|
6
|
+
prototypes and demos, and it is the worked example for teams that build their
|
|
7
|
+
own. It has no production users. Production assessment players are host-built
|
|
8
|
+
from the same packages
|
|
9
|
+
([product scope](../../docs/architecture/architecture.md#product-scope)), and
|
|
10
|
+
this package changes without a compatibility period.
|
|
11
|
+
|
|
12
|
+
This README is the API reference for a host that mounts it: inputs, host
|
|
13
|
+
methods, the controller, hooks, events and persistence.
|
|
14
|
+
[Building a Multi-Section Player](../../docs/assessment-player/integration-guide.md)
|
|
15
|
+
builds a player from the toolkit's assessment-session helpers, with this one as
|
|
16
|
+
the worked example, and the [assessment demos](../../apps/assessment-demos/README.md)
|
|
17
|
+
run it.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @pie-players/pie-assessment-player
|
|
23
|
+
```
|
|
10
24
|
|
|
11
|
-
|
|
12
|
-
|
|
25
|
+
```ts
|
|
26
|
+
import "@pie-players/pie-assessment-player";
|
|
27
|
+
```
|
|
13
28
|
|
|
14
|
-
|
|
29
|
+
Importing the package registers `pie-assessment-player-default` and
|
|
30
|
+
`pie-assessment-player-shell`, and its import of `@pie-players/pie-section-player`
|
|
31
|
+
registers the section player layouts. The entry is bundler-only and
|
|
32
|
+
browser-only: it imports `@pie-players/pie-section-player` and
|
|
33
|
+
`@pie-players/pie-assessment-toolkit` by bare specifier. The package has no
|
|
34
|
+
browser build, so a CDN cannot serve it; its `unpkg` and `jsdelivr` fields name
|
|
35
|
+
the bundler entry.
|
|
36
|
+
|
|
37
|
+
## Elements
|
|
38
|
+
|
|
39
|
+
| Element | Role |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `pie-assessment-player-default` | Runs an assessment's sections one at a time in a section player, with a position label and Back and Next buttons |
|
|
42
|
+
| `pie-assessment-player-shell` | A layout frame: a `navigation` slot above a default slot, filled by the host ([Shell element](#shell-element)) |
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```html
|
|
47
|
+
<pie-assessment-player-default
|
|
48
|
+
assessment-id="algebra-unit-1"
|
|
49
|
+
attempt-id="attempt-42"
|
|
50
|
+
section-player-layout="splitpane"
|
|
51
|
+
></pie-assessment-player-default>
|
|
52
|
+
```
|
|
15
53
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
54
|
+
```ts
|
|
55
|
+
import "@pie-players/pie-assessment-player";
|
|
56
|
+
import type {
|
|
57
|
+
AssessmentDefinition,
|
|
58
|
+
AssessmentPlayerHooks,
|
|
59
|
+
AssessmentPlayerRuntimeConfig,
|
|
60
|
+
AssessmentPlayerRuntimeHostContract,
|
|
61
|
+
} from "@pie-players/pie-assessment-player";
|
|
62
|
+
import type { AssessmentSession, ToolkitCoordinator } from "@pie-players/pie-assessment-toolkit";
|
|
63
|
+
|
|
64
|
+
type AssessmentPlayerElement = HTMLElement &
|
|
65
|
+
AssessmentPlayerRuntimeConfig &
|
|
66
|
+
AssessmentPlayerRuntimeHostContract & {
|
|
67
|
+
coordinator: ToolkitCoordinator | null;
|
|
68
|
+
session: AssessmentSession | null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
declare const assessment: AssessmentDefinition;
|
|
72
|
+
declare const hooks: AssessmentPlayerHooks;
|
|
73
|
+
|
|
74
|
+
const player = document.querySelector<AssessmentPlayerElement>("pie-assessment-player-default");
|
|
75
|
+
if (player) {
|
|
76
|
+
player.assessment = assessment;
|
|
77
|
+
player.hooks = hooks;
|
|
78
|
+
player.env = { mode: "gather", role: "student" };
|
|
79
|
+
}
|
|
80
|
+
```
|
|
23
81
|
|
|
24
|
-
|
|
25
|
-
`
|
|
26
|
-
|
|
82
|
+
An `AssessmentDefinition` lists its sections in `testParts[].sections` or in
|
|
83
|
+
`sections`. The default delivery plan flattens them in order; a section without
|
|
84
|
+
an `identifier` gets `section-{index}`, or `section-{part}-{index}` inside a test
|
|
85
|
+
part.
|
|
86
|
+
|
|
87
|
+
## Inputs
|
|
88
|
+
|
|
89
|
+
| Input | Attribute | Default | A change |
|
|
90
|
+
| --- | --- | --- | --- |
|
|
91
|
+
| `assessmentId` | `assessment-id` | — | Rebuilds the controller |
|
|
92
|
+
| `attemptId` | `attempt-id` | — | Rebuilds the controller. Without one the default persistence neither reads nor writes |
|
|
93
|
+
| `assessment` | property | — | Rebuilds the controller |
|
|
94
|
+
| `hooks` | property | — | Rebuilds the controller ([Hooks](#hooks)) |
|
|
95
|
+
| `session` | property | — | Rebuilds the controller from the new value, under the rules in [Lifecycle](#lifecycle) |
|
|
96
|
+
| `env` | property | — | Sets `runtime` on the mounted section: `{ mode, role }` |
|
|
97
|
+
| `coordinator` | property | — | Sets `runtime` on the mounted section: a host-owned `ToolkitCoordinator` every section shares |
|
|
98
|
+
| `playerType` | `player-type` | `iife` | Sets `runtime` on the mounted section: the element loading strategy, `iife`, `esm` or `preloaded` |
|
|
99
|
+
| `sectionPlayerRuntime` | property | — | Re-attaches instrumentation, then sets `runtime` on the mounted section ([Section runtime](#section-runtime)) |
|
|
100
|
+
| `sectionPlayerLayout` | `section-player-layout` | `splitpane` | Remounts the section, keeping its session and the controller. `vertical` selects the vertical layout; any other value the splitpane |
|
|
101
|
+
| `showNavigation` | `show-navigation` | `true` | Shows or hides the position label and the Back and Next buttons |
|
|
102
|
+
| `locale` | `locale` | `en-US` | Relabels the chrome and sets the section's `locale` attribute, removing it when empty |
|
|
103
|
+
| `debug` | `debug` | — | Sets `window.PIE_DEBUG` ([Debug logging](#debug-logging)) |
|
|
104
|
+
|
|
105
|
+
The element starts once it is connected and holds both `assessmentId` and
|
|
106
|
+
`assessment`; until then it shows an empty state. Inputs assigned in the same
|
|
107
|
+
turn are batched, whether before or after connecting. A rebuild retires the
|
|
108
|
+
current controller at once and starts the next in a microtask, which remounts
|
|
109
|
+
the section. An object input changes only when replaced: mutating an assigned
|
|
110
|
+
object in place signals nothing. `env`, `coordinator`, `playerType` and
|
|
111
|
+
`sectionPlayerRuntime` reach the mounted section without a remount.
|
|
112
|
+
|
|
113
|
+
### Section runtime
|
|
114
|
+
|
|
115
|
+
Each mounted section player receives a `runtime` built from the element's
|
|
116
|
+
inputs:
|
|
117
|
+
|
|
118
|
+
| `runtime` key | Source |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `playerType` | `playerType` |
|
|
121
|
+
| `assessmentId` | `assessmentId`, when set |
|
|
122
|
+
| `env` | `env`, when set |
|
|
123
|
+
| `coordinator` | `coordinator`, when set |
|
|
124
|
+
| `player` | `sectionPlayerRuntime.player`, with `backend` deep-cloned |
|
|
125
|
+
| Any other key | `sectionPlayerRuntime` |
|
|
126
|
+
|
|
127
|
+
A key in `sectionPlayerRuntime` overrides the element's own, so
|
|
128
|
+
`sectionPlayerRuntime.assessmentId` wins over `assessment-id`. The section's
|
|
129
|
+
`section-id` is the section identifier; `attempt-id`, `locale` and `debug` travel
|
|
130
|
+
as section attributes, and `hooks.cardTitleFormatter` becomes the section's
|
|
131
|
+
`hooks`. The section player's [inputs](../section-player/README.md#inputs)
|
|
132
|
+
and [runtime configuration](../section-player/README.md#runtime-configuration)
|
|
133
|
+
define the keys.
|
|
134
|
+
|
|
135
|
+
Without a `coordinator`, each section's toolkit builds its own coordinator,
|
|
136
|
+
without hooks, and disposes it with the section, so tool state, text-to-speech
|
|
137
|
+
and section persistence hooks do not carry across sections. Pass one
|
|
138
|
+
`ToolkitCoordinator` to share them. It stays host-owned, and on a section change
|
|
139
|
+
the element stops its text-to-speech playback.
|
|
140
|
+
|
|
141
|
+
## Host methods
|
|
142
|
+
|
|
143
|
+
The element implements `AssessmentPlayerRuntimeHostContract`:
|
|
144
|
+
|
|
145
|
+
| Method | Returns |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| `getSnapshot()` | `{ readiness, navigation, progress }`, with zero counts before the controller is ready |
|
|
148
|
+
| `selectReadiness()` | `{ phase }`: `bootstrapping`, `hydrating`, `ready` or `error` |
|
|
149
|
+
| `selectNavigation()` | `currentIndex`, `totalSections`, `canNext`, `canPrevious`, `currentSectionId` |
|
|
150
|
+
| `selectProgress()` | `visitedSections`, `totalSections` |
|
|
151
|
+
| `navigateTo(indexOrIdentifier)` | `true` when the section changed, and for the current section's identifier |
|
|
152
|
+
| `navigateNext()`, `navigatePrevious()` | `true` when the section changed |
|
|
153
|
+
| `getAssessmentController()` | The controller handle while connected and ready, else `null` |
|
|
154
|
+
| `waitForAssessmentController(timeoutMs = 5000)` | A promise of the handle, or `null` |
|
|
155
|
+
|
|
156
|
+
Each navigation method first dispatches the cancelable
|
|
157
|
+
`assessment-navigation-requested`, before any range check: a request at the last
|
|
158
|
+
section carries an out-of-range `toIndex` and no `toSectionId`. A canceled request
|
|
159
|
+
returns `false`. Otherwise the method calls the controller and, when the section
|
|
160
|
+
changed, `persist()`. The Back and Next buttons call these methods.
|
|
161
|
+
`navigateTo(identifier)` reports the current index as its `toIndex`, so read the
|
|
162
|
+
target from `toSectionId`; with the current section's identifier it returns `true`
|
|
163
|
+
and dispatches nothing.
|
|
27
164
|
|
|
28
165
|
### Obtaining the handle
|
|
29
166
|
|
|
30
|
-
Two equivalent paths:
|
|
31
|
-
|
|
32
167
|
```ts
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
168
|
+
import type { AssessmentPlayerRuntimeHostContract } from "@pie-players/pie-assessment-player";
|
|
169
|
+
|
|
170
|
+
const player = document.querySelector<HTMLElement & AssessmentPlayerRuntimeHostContract>(
|
|
171
|
+
"pie-assessment-player-default",
|
|
172
|
+
);
|
|
173
|
+
const controller = await player?.waitForAssessmentController(5000);
|
|
37
174
|
```
|
|
38
175
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
observe the same controller. The getter returns `null`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
timeout does not cancel
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
`hooks` and `session` in the same turn, or assign them before connecting. These
|
|
48
|
-
assignments are batched; changing assessment, attempt, hooks or session later
|
|
49
|
-
retires the previous controller and starts a new one. Replace object properties to update them;
|
|
50
|
-
mutating a previously assigned object in place is not an update signal. Runtime
|
|
51
|
-
props, locale, and navigation visibility do not reload the assessment.
|
|
52
|
-
|
|
53
|
-
Failed initialization exposes an error and a keyboard-accessible Retry action.
|
|
54
|
-
The element invokes the ready hook and emits the ready event once per successful
|
|
55
|
-
initialization. Ready hooks are notifications: their rejection reaches `onError`
|
|
56
|
-
and `assessment-error` without undoing successful hydration.
|
|
176
|
+
The handle is ready once initialization and hydration succeed. The getter, the
|
|
177
|
+
waiter, `onAssessmentControllerReady` and `assessment-controller-ready` then
|
|
178
|
+
observe the same controller. The getter returns `null` before that, after a
|
|
179
|
+
failure and after disconnect. A waiter resolves `null` at once on a disconnected
|
|
180
|
+
element or one in the error phase, and later when its initialization fails or is
|
|
181
|
+
retired or its own timeout expires; the timeout does not cancel the
|
|
182
|
+
initialization. Only the newest connected initialization publishes a controller,
|
|
183
|
+
UI, event, hook call or error.
|
|
57
184
|
|
|
58
185
|
### Lifecycle
|
|
59
186
|
|
|
60
|
-
The element owns its
|
|
61
|
-
|
|
62
|
-
listeners
|
|
63
|
-
coordinator
|
|
187
|
+
The element owns its controller. It disposes the controller when an input
|
|
188
|
+
rebuilds it or the element disconnects. `dispose()` is idempotent: it removes the
|
|
189
|
+
listeners, makes the handle unusable and calls `onAssessmentControllerDispose`. A
|
|
190
|
+
host-supplied coordinator stays host-owned; one a section's toolkit built is
|
|
191
|
+
disposed with that section.
|
|
192
|
+
|
|
193
|
+
A failed start shows an error state with a keyboard-accessible Retry button and
|
|
194
|
+
dispatches `assessment-error`. The ready hook and the ready event run once per
|
|
195
|
+
successful start. The hook is a notification: its rejection reaches `onError`
|
|
196
|
+
(phase `controller-init`) and `assessment-error` and leaves the hydrated
|
|
197
|
+
assessment ready.
|
|
64
198
|
|
|
65
199
|
```ts
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
await controller.submit();
|
|
78
|
-
unsubscribe();
|
|
200
|
+
const unsubscribe = controller?.subscribe((event) => {
|
|
201
|
+
if (event.type === "assessment-route-changed") {
|
|
202
|
+
showPosition(event.currentSectionIndex, event.totalSections);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Saves run in call order. submit() saves first and rejects, unsubmitted, when
|
|
207
|
+
// that save fails.
|
|
208
|
+
await controller?.persist();
|
|
209
|
+
await controller?.submit();
|
|
210
|
+
unsubscribe?.();
|
|
79
211
|
```
|
|
80
212
|
|
|
81
|
-
`
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
213
|
+
The `session` property takes an `AssessmentSession`. The controller's first
|
|
214
|
+
hydrate applies it in place of the strategy's `loadSession`, and the strategy
|
|
215
|
+
still receives every save. The value is consumed once: a rebuild for another
|
|
216
|
+
reason, such as an `assessment` change, hydrates from the strategy. Assigning a
|
|
217
|
+
session whose content equals the controller's, as echoing `getSession()` back on
|
|
218
|
+
`assessment-session-changed` does, is a no-op, and so is `null` once the
|
|
219
|
+
controller is ready; any other value rebuilds the controller from it. Reading
|
|
220
|
+
`session` returns the assigned value until the controller is ready, and a copy of
|
|
221
|
+
the controller's session after.
|
|
222
|
+
|
|
223
|
+
On every section mount the element passes the section's saved snapshot, when the
|
|
224
|
+
assessment session holds one, as the section player's `session` property, which
|
|
225
|
+
the section's controller applies before `engine-ready`. A section without an
|
|
226
|
+
entry hydrates from the section strategy
|
|
227
|
+
([Session persistence and submission](#session-persistence-and-submission)).
|
|
228
|
+
Until the section reaches `engine-ready`, for up to 5 seconds, the section region
|
|
229
|
+
is `aria-busy` and the section `inert`. The element then unlocks it and copies
|
|
230
|
+
the section's session into the assessment session on each toolkit
|
|
231
|
+
`session-changed`, and once more before navigation replaces the section,
|
|
232
|
+
including navigation through the controller. A section that fails or times out
|
|
233
|
+
shows a status message, "restore failed" when a snapshot was passed and "load
|
|
234
|
+
failed" otherwise, with Retry; it calls `onError` with phase `navigation` and
|
|
235
|
+
`{ sectionIdentifier }` and dispatches `assessment-error`, and the saved snapshot
|
|
236
|
+
stays intact. This handoff is in memory and acknowledges no durable save.
|
|
237
|
+
|
|
238
|
+
### Assessment controller
|
|
239
|
+
|
|
240
|
+
The handle implements `AssessmentControllerHandle`:
|
|
241
|
+
|
|
242
|
+
| Member | Effect |
|
|
243
|
+
| --- | --- |
|
|
244
|
+
| `initialize()` | Builds the delivery plan, creates a fresh session and runs `hydrate()`. The element calls it |
|
|
245
|
+
| `hydrate()` | Runs `onBeforeAssessmentHydrate`, then applies the assigned or loaded session and emits `assessment-session-applied` when there was one |
|
|
246
|
+
| `persist()` | Saves through the strategy, queued in call order. A failure reaches `onError` (`session-save`) and the promise resolves |
|
|
247
|
+
| `submit()` | Saves, then marks the assessment submitted and emits `assessment-submission-state-changed`. A failed save reaches `onError` and rejects, and the assessment stays unsubmitted |
|
|
248
|
+
| `getSession()` | The controller's live `AssessmentSession`, the shape the strategy exchanges; `null` once disposed |
|
|
249
|
+
| `getRuntimeState()` | `readiness`, `currentSectionIndex`, `totalSections`, `currentSectionId`, `canNext`, `canPrevious`, `visitedSections`, `submitted`, for chrome and diagnostics |
|
|
250
|
+
| `navigateTo(indexOrIdentifier)`, `navigateNext()`, `navigatePrevious()` | Move, emitting `assessment-route-changed`, `assessment-session-changed` and `assessment-progress-changed`; `false` for an index out of range or an unknown identifier |
|
|
251
|
+
| `subscribe(listener)` | Delivers the [event stream](#event-stream) and returns a disposer |
|
|
252
|
+
| `getCurrentSection()`, `getSectionAt(index)` | A delivery-plan entry: `sectionIdentifier`, `section`, `stageIdentifier`, `stageIndex`, `sectionIndex` |
|
|
253
|
+
| `getSectionSession(sectionId)` | That section's `SectionControllerSessionState`, or `null` |
|
|
254
|
+
| `updateSectionSession(sectionId, session)` | Writes a section snapshot into the assessment session and emits `assessment-session-changed` |
|
|
255
|
+
| `dispose()` | Retires the controller ([Lifecycle](#lifecycle)) |
|
|
256
|
+
|
|
257
|
+
Persist `getSession()`. `getRuntimeState()` is derived and carries no stability
|
|
258
|
+
guarantee. Navigation through the controller skips the element's navigation
|
|
259
|
+
request and its `persist()`; the element still captures the outgoing section and
|
|
260
|
+
remounts.
|
|
261
|
+
|
|
262
|
+
## Hooks
|
|
263
|
+
|
|
264
|
+
| Hook | Called |
|
|
265
|
+
| --- | --- |
|
|
266
|
+
| `cardTitleFormatter(context)` | Forwarded to each section player ([Card titles](#card-titles)) |
|
|
267
|
+
| `createAssessmentDeliveryPlan(context, defaults)` | Once per controller, at start. `context` is `{ assessmentId, attemptId, assessment }`; `defaults.createDefaultDeliveryPlan()` flattens the sections into a linear list. A nullish return gets the default |
|
|
268
|
+
| `createAssessmentSessionPersistence(context, defaults)` | Once per controller, at the first hydrate or save, and cached. `context` is `{ assessmentId, attemptId }`; `defaults.createDefaultPersistence()` returns the `localStorage` strategy. A nullish return gets the default |
|
|
269
|
+
| `onBeforeAssessmentHydrate(context)` | Awaited at the start of each hydrate |
|
|
270
|
+
| `onBeforeAssessmentPersist(context, session)` | Awaited before each save, inside the save queue |
|
|
271
|
+
| `onAssessmentControllerReady(controller)` | After a successful start; not awaited |
|
|
272
|
+
| `onAssessmentControllerDispose(controller)` | On dispose |
|
|
273
|
+
| `onError(error, { phase, details })` | On every reported failure |
|
|
274
|
+
|
|
275
|
+
| `onError` phase | Failure |
|
|
276
|
+
| --- | --- |
|
|
277
|
+
| `delivery-plan-create` | `createAssessmentDeliveryPlan` threw |
|
|
278
|
+
| `session-load` | A hydrate failed: its hook, the persistence factory or `loadSession`. On a published controller the element also retires it, shows the error state and dispatches `assessment-error` |
|
|
279
|
+
| `session-save` | A save from `persist()` or `submit()` failed. No DOM event follows |
|
|
280
|
+
| `controller-init` | `onAssessmentControllerReady` rejected |
|
|
281
|
+
| `controller-dispose` | `onAssessmentControllerDispose` rejected |
|
|
282
|
+
| `navigation` | A section failed to load or restore; `details` is `{ sectionIdentifier }` |
|
|
283
|
+
|
|
284
|
+
## Events
|
|
285
|
+
|
|
286
|
+
### DOM events
|
|
287
|
+
|
|
288
|
+
The element dispatches these on itself, bubbling and composed.
|
|
289
|
+
`ASSESSMENT_PLAYER_PUBLIC_EVENTS` holds the names.
|
|
290
|
+
|
|
291
|
+
| Event | Detail | When |
|
|
292
|
+
| --- | --- | --- |
|
|
293
|
+
| `assessment-controller-ready` | `{ controller }` | After a successful start |
|
|
294
|
+
| `assessment-navigation-requested` | `fromIndex`, `toIndex`, `fromSectionId`, `toSectionId`, `reason` (`navigate-to`, `navigate-next`, `navigate-previous`) | Before an element navigation. Cancelable: `preventDefault()` blocks it |
|
|
295
|
+
| `assessment-route-changed` | `currentSectionIndex`, `totalSections`, `currentSectionId`, `previousSectionId`, `canNext`, `canPrevious` | After the section changed |
|
|
296
|
+
| `assessment-session-applied` | `timestamp` | A hydrate applied an assigned or loaded session |
|
|
297
|
+
| `assessment-session-changed` | `timestamp` | The session changed: a move or a section snapshot |
|
|
298
|
+
| `assessment-progress-changed` | `visitedSectionCount`, `totalSections` | After every move |
|
|
299
|
+
| `assessment-submission-state-changed` | `submitted: true` | `submit()` succeeded |
|
|
300
|
+
| `assessment-error` | `{ error }` | A failed start, a failed hydrate of a published controller, a rejected ready hook, or a section that failed to load |
|
|
301
|
+
|
|
302
|
+
The route, session, progress and submission details are the controller's event
|
|
303
|
+
objects, `type` and `timestamp` included. A move arrives in this order, for DOM
|
|
304
|
+
and controller listeners alike: `assessment-session-changed` for the outgoing
|
|
305
|
+
section's snapshot, `assessment-route-changed`, `assessment-session-changed` for
|
|
306
|
+
the position, then `assessment-progress-changed`. The section remounts during
|
|
307
|
+
`assessment-route-changed`.
|
|
135
308
|
|
|
136
|
-
|
|
137
|
-
|
|
309
|
+
```ts
|
|
310
|
+
player?.addEventListener("assessment-navigation-requested", (event) => {
|
|
311
|
+
const { fromSectionId, toSectionId } = (event as CustomEvent).detail;
|
|
312
|
+
if (!isNavigationAllowed(fromSectionId, toSectionId)) event.preventDefault();
|
|
313
|
+
});
|
|
314
|
+
```
|
|
138
315
|
|
|
139
|
-
|
|
316
|
+
### Event stream
|
|
140
317
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
318
|
+
The controller's `subscribe()` delivers `AssessmentControllerEvent`, a union of
|
|
319
|
+
`assessment-route-changed`, `assessment-session-applied`,
|
|
320
|
+
`assessment-session-changed`, `assessment-progress-changed` and
|
|
321
|
+
`assessment-submission-state-changed`, with the payloads in the table above. It
|
|
322
|
+
covers assessment-level change only. Section events come from the section player
|
|
323
|
+
and its controller, and the coordinator's `subscribeItemEvents` and
|
|
324
|
+
`subscribeSectionLifecycleEvents` helpers follow them across sections
|
|
325
|
+
([Subscriptions](../section-player/README.md#subscriptions)).
|
|
326
|
+
|
|
327
|
+
## Session persistence and submission
|
|
328
|
+
|
|
329
|
+
`createAssessmentSessionPersistence` returns the strategy: `loadSession(context)`,
|
|
330
|
+
`saveSession(context, session)` and an optional `clearSession(context)`. The
|
|
331
|
+
default strategy stores the session in `localStorage` under
|
|
332
|
+
`pie:assessment-controller:v1:{assessmentId}:{attemptId}`. Without an attempt id
|
|
333
|
+
it neither reads nor writes, since nothing would tell two learners on one device
|
|
334
|
+
apart. Supply a strategy backed by your own store for anything beyond a demo.
|
|
335
|
+
|
|
336
|
+
A loaded session replaces the controller's wholesale, without a check against the
|
|
337
|
+
delivery plan. Saves run one at a time in call order, so an older write never
|
|
338
|
+
lands after a newer one. The element calls `persist()` only after its own moves:
|
|
339
|
+
a section's answers reach the assessment session on each `session-changed` but
|
|
340
|
+
reach the strategy at the next move, `persist()` or `submit()`, so a host that
|
|
341
|
+
saves on answers calls `persist()` itself.
|
|
342
|
+
|
|
343
|
+
`submit()` is local. It saves, then sets `submitted`, which the session does not
|
|
344
|
+
record: a reload starts unsubmitted, and navigation still works after submit.
|
|
345
|
+
[Authoritative assessment submission](../../docs/prds/assessment-authoritative-submission.md)
|
|
346
|
+
proposes a host-confirmed submission.
|
|
347
|
+
|
|
348
|
+
The section layer persists too. A section with no entry in the assessment
|
|
349
|
+
session hydrates from the coordinator's section strategy, by default
|
|
350
|
+
`localStorage` keyed by assessment, section and attempt id, and the coordinator
|
|
351
|
+
saves the section there before it disposes the section's controller
|
|
352
|
+
([Section session persistence](../section-player/README.md#section-session-persistence)).
|
|
353
|
+
To make the assessment controller the one owner, pass a coordinator whose
|
|
354
|
+
`hooks.createSectionSessionPersistence` returns a strategy that loads and saves
|
|
355
|
+
nothing.
|
|
356
|
+
|
|
357
|
+
## Backend delivery for embedded items
|
|
358
|
+
|
|
359
|
+
Item delivery against a backend is configured on
|
|
360
|
+
`sectionPlayerRuntime.player.backend`. The element passes a deep clone to each
|
|
361
|
+
section player, which derives every embedded item player's `backend` from it
|
|
362
|
+
([Backend delivery for embedded items](../section-player/README.md#backend-delivery-for-embedded-items)).
|
|
363
|
+
The `attempt-id` is an assessment attempt and never becomes
|
|
364
|
+
`backend.delivery.assignmentId`; a host that delivers under an assignment sets
|
|
365
|
+
`assignmentId` itself. Assessment session persistence stays on
|
|
366
|
+
`createAssessmentSessionPersistence`. The item player's
|
|
367
|
+
[backend support](../../docs/item-player/backend-support.md) guide covers the
|
|
368
|
+
delivery contract.
|
|
369
|
+
|
|
370
|
+
## Navigation chrome
|
|
371
|
+
|
|
372
|
+
The chrome shows a localized position label ("Section 2 of 3") and Back and Next
|
|
373
|
+
buttons, disabled at the first and last section. The section sits in a region
|
|
374
|
+
named by the position label. When focus was inside the element, a section change
|
|
375
|
+
returns it to the same button, or the other one when that is disabled, or the
|
|
376
|
+
region, and a polite status region announces the new position. Navigation from
|
|
377
|
+
the host's own chrome leaves focus with the host and announces nothing, so the
|
|
378
|
+
host announces the change.
|
|
379
|
+
|
|
380
|
+
## Card titles
|
|
381
|
+
|
|
382
|
+
`hooks.cardTitleFormatter` reaches every mounted section player, the first one
|
|
383
|
+
included:
|
|
144
384
|
|
|
145
385
|
```ts
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
cardTitleFormatter: (context
|
|
150
|
-
if (context.kind === "item") {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
if (context.kind === "passage") {
|
|
155
|
-
return "Reading passage";
|
|
156
|
-
}
|
|
157
|
-
return typeof context.defaultTitle === "string" ? context.defaultTitle : "";
|
|
386
|
+
import type { AssessmentPlayerHooks } from "@pie-players/pie-assessment-player";
|
|
387
|
+
|
|
388
|
+
const hooks: AssessmentPlayerHooks = {
|
|
389
|
+
cardTitleFormatter: (context) => {
|
|
390
|
+
if (context.kind === "item") return `Question ${Number(context.itemIndex ?? 0) + 1}`;
|
|
391
|
+
if (context.kind === "passage") return "Reading passage";
|
|
392
|
+
return null;
|
|
158
393
|
},
|
|
159
394
|
};
|
|
160
395
|
```
|
|
161
396
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
- `@pie-players/pie-assessment-player/components/assessment-player-shell-element`
|
|
166
|
-
|
|
167
|
-
## Instrumentation and observability
|
|
168
|
-
|
|
169
|
-
Assessment-player instrumentation is provider-agnostic and built on the shared
|
|
170
|
-
`InstrumentationProvider` contract used across players.
|
|
171
|
-
|
|
172
|
-
Canonical provider injection paths:
|
|
397
|
+
The section player's [Card titles](../section-player/README.md#card-titles)
|
|
398
|
+
lists the context fields; a return that is not a non-empty string keeps the
|
|
399
|
+
default title.
|
|
173
400
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
Provider semantics:
|
|
177
|
-
|
|
178
|
-
- With `trackPageActions: true`, missing/`undefined` provider values use the default New Relic provider path.
|
|
179
|
-
- `instrumentationProvider: null` explicitly disables instrumentation.
|
|
180
|
-
- Invalid provider objects are ignored (optional debug warning), also no-op.
|
|
181
|
-
- Existing `item-player` behavior remains the compatibility anchor.
|
|
182
|
-
- To keep production telemetry while debugging, use `CompositeInstrumentationProvider`
|
|
183
|
-
with `NewRelicInstrumentationProvider` and `DebugPanelInstrumentationProvider`.
|
|
184
|
-
- Toolkit telemetry forwarding uses the same provider path, so tool/backend
|
|
185
|
-
operational events appear in production providers and debug overlays.
|
|
186
|
-
|
|
187
|
-
Assessment-player owned canonical stream:
|
|
188
|
-
|
|
189
|
-
- `pie-assessment-controller-ready`
|
|
190
|
-
- `pie-assessment-navigation-requested`
|
|
191
|
-
- `pie-assessment-route-changed`
|
|
192
|
-
- `pie-assessment-session-applied`
|
|
193
|
-
- `pie-assessment-session-changed`
|
|
194
|
-
- `pie-assessment-progress-changed`
|
|
195
|
-
- `pie-assessment-submission-state-changed`
|
|
196
|
-
- `pie-assessment-error`
|
|
197
|
-
|
|
198
|
-
Ownership boundary: assessment-player owns assessment semantics only. Section
|
|
199
|
-
and toolkit semantics remain in their own streams to avoid overlap. Bridge
|
|
200
|
-
dedupe is a safety net, not the primary correctness mechanism.
|
|
201
|
-
|
|
202
|
-
Toolkit tool/backend operational stream (when toolkit is mounted):
|
|
401
|
+
## Debug logging
|
|
203
402
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
403
|
+
`debug` sets `window.PIE_DEBUG`, the page-wide flag every PIE logger reads.
|
|
404
|
+
`debug="true"` enables it; `"false"`, `"0"` and an empty value disable it, so a
|
|
405
|
+
bare `debug` attribute turns logging off. The value is also set as each section
|
|
406
|
+
player's `debug` attribute when the section mounts.
|
|
407
|
+
|
|
408
|
+
## Instrumentation
|
|
409
|
+
|
|
410
|
+
The element forwards its DOM events to an `InstrumentationProvider` under the
|
|
411
|
+
same names with a `pie-` prefix (`assessment-route-changed` becomes
|
|
412
|
+
`pie-assessment-route-changed`), with `instrumentationLayer: "assessment"`,
|
|
413
|
+
`assessmentId` and `attemptId` attached. The provider is
|
|
414
|
+
`sectionPlayerRuntime.player.loaderConfig.instrumentationProvider`, the one the
|
|
415
|
+
section and item players use, and the bridge re-attaches when
|
|
416
|
+
`sectionPlayerRuntime` changes.
|
|
417
|
+
[Instrumentation providers](../../docs/architecture/instrumentation-providers.md#provider-resolution)
|
|
418
|
+
sets out how an unset, `null` or invalid provider resolves.
|
|
419
|
+
`CompositeInstrumentationProvider` combines providers, such as a production one
|
|
420
|
+
and `DebugPanelInstrumentationProvider`.
|
|
421
|
+
|
|
422
|
+
Section and toolkit events keep their own streams. The toolkit's operational
|
|
423
|
+
events, forwarded when the toolkit is mounted, are listed in
|
|
424
|
+
[Instrumentation providers](../../docs/architecture/instrumentation-providers.md#operational-events).
|
|
207
425
|
|
|
208
426
|
## Preloaded elements
|
|
209
427
|
|
|
@@ -233,47 +451,56 @@ registerPreloadedElements(
|
|
|
233
451
|
);
|
|
234
452
|
```
|
|
235
453
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
for the registration contract.
|
|
454
|
+
A tag missing at pre-warm leaves the section's items unmounted and raises a
|
|
455
|
+
non-recoverable `element-preload` framework error. Item players count as hosted
|
|
456
|
+
under `sectionPlayerRuntime.player.hosted` or an enabled
|
|
457
|
+
`sectionPlayerRuntime.player.backend.delivery`, and then need no `controller`.
|
|
458
|
+
Version pins, element sets, MathJax assets and TypeScript setup are in
|
|
459
|
+
[Registering elements from npm](../../docs/item-player/loading-strategies.md#registering-elements-from-npm).
|
|
460
|
+
|
|
461
|
+
## Shell element
|
|
462
|
+
|
|
463
|
+
`pie-assessment-player-shell` is a layout scaffold for a host that supplies its
|
|
464
|
+
own chrome: a `navigation` slot above a default slot that fills the remaining
|
|
465
|
+
height. The scaffold renders in an open shadow root and the host's children stay
|
|
466
|
+
in light DOM, so document styles reach them. `show-navigation="false"` drops the
|
|
467
|
+
navigation slot; its children stay in place but unrendered.
|
|
468
|
+
|
|
469
|
+
```html
|
|
470
|
+
<pie-assessment-player-shell>
|
|
471
|
+
<div slot="navigation">
|
|
472
|
+
<my-assessment-nav></my-assessment-nav>
|
|
473
|
+
</div>
|
|
474
|
+
<pie-section-player-splitpane></pie-section-player-splitpane>
|
|
475
|
+
<my-timer-widget></my-timer-widget>
|
|
476
|
+
</pie-assessment-player-shell>
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
The shell has no controller. The host mounts the section player, wires
|
|
480
|
+
navigation and drives persistence, as
|
|
481
|
+
[Building a Multi-Section Player](../../docs/assessment-player/integration-guide.md)
|
|
482
|
+
does.
|
|
266
483
|
|
|
267
484
|
## Content trust boundary
|
|
268
485
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
`sanitizeMarkup`
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
486
|
+
Item and passage markup reaches the DOM through `<pie-item-player>`, which
|
|
487
|
+
sanitizes it with DOMPurify by default. The
|
|
488
|
+
[item player README](../item-player/README.md#content-trust-boundary) covers the
|
|
489
|
+
allow-list, the `trust-markup` opt-out and the `sanitizeMarkup` override. Set
|
|
490
|
+
`sectionPlayerRuntime.player.trustMarkup` or
|
|
491
|
+
`sectionPlayerRuntime.player.sanitizeMarkup` here: the assessment player passes
|
|
492
|
+
that runtime to each section player, which forwards `runtime.player` fields to
|
|
493
|
+
its item players
|
|
494
|
+
([section player trust boundary](../section-player/README.md#content-trust-boundary)).
|
|
495
|
+
The [security model](../../docs/security/readme.md) covers the trust boundary
|
|
496
|
+
across the players.
|
|
497
|
+
|
|
498
|
+
## Exports
|
|
499
|
+
|
|
500
|
+
| Entry | Contents |
|
|
501
|
+
| --- | --- |
|
|
502
|
+
| `@pie-players/pie-assessment-player` | Registers both elements. Exports `ASSESSMENT_PLAYER_PUBLIC_EVENTS` and the types of the inputs, hooks, host contract, controller handle, controller events, event details, delivery plan and persistence strategy |
|
|
503
|
+
| `@pie-players/pie-assessment-player/components/assessment-player-default-element` | An alias of the root entry |
|
|
504
|
+
|
|
505
|
+
The `AssessmentController` class is internal: hosts reach a controller through
|
|
506
|
+
the element.
|