@effect.healthcare/messenger 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/messenger.d.ts +9 -2
- package/dist/messenger.js +17 -10
- package/package.json +7 -2
- package/.claude/CLAUDE.md +0 -88
- package/.claude/context/composition.md +0 -921
- package/.claude/context/data.md +0 -1036
- package/.claude/context/event.md +0 -686
- package/.claude/context/gesture.md +0 -614
- package/.claude/context/operation.md +0 -596
- package/.claude/context/state.md +0 -864
- package/.claude/context/testing.md +0 -876
- package/.claude/context/validation.md +0 -350
- package/.claude/context/workflow.md +0 -608
- package/.claude/settings.json +0 -7
- package/.mcp.json +0 -12
- package/dist/.gitkeep +0 -1
- package/src/index.ts +0 -19
- package/src/messenger.meta.ts +0 -139
- package/src/messenger.ts +0 -403
- package/src/messenger.validation.ts +0 -23
- package/web-dev-server.config.mjs +0 -8
|
@@ -1,876 +0,0 @@
|
|
|
1
|
-
# Testing
|
|
2
|
-
|
|
3
|
-
This document defines an implementation policy for **component testing**, then illustrates how specifications are implemented by applying this policy.
|
|
4
|
-
|
|
5
|
-
This document is intentionally **testing** only, but part of a set of implementation policies which together define the policies which should be followed to transform specifications into an implementation which is predictable, maintainable, and production-ready at scale.
|
|
6
|
-
|
|
7
|
-
TDD discipline and commit sequencing are covered in the workflow policy. This document covers test file structure, test vocabulary, and the required coverage contract for each concern layer.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# How to Read This Policy
|
|
12
|
-
|
|
13
|
-
Rules in this policy fall into three strengths. Read each rule with its strength in mind:
|
|
14
|
-
|
|
15
|
-
- **Principles** are non-negotiable. They derive from framework correctness or TDD discipline and hold for every component. A conflict with a principle is a defect.
|
|
16
|
-
- **Templates** are invariant skeletons with explicit variation points. The skeleton applies to every component; specific steps activate or deactivate based on the component's declared classification (e.g., attribute-backed vs non-attribute, readonly vs writable, payload-bearing vs payload-free). Variation is part of the template, not a deviation from it.
|
|
17
|
-
- **Conventions** are canonical defaults that keep implementations readable across components. A second component may deviate when its shape genuinely warrants it; the deviation should be explained at review time, not forbidden by the policy.
|
|
18
|
-
|
|
19
|
-
When applying this policy to a new component, classify each rule before reconciling conflicts. A conflict with a principle is a defect; a conflict with a template means a variation point needs to be made explicit; a conflict with a convention is a documented deviation.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
# 1) Tests Must Be Organized in Three Dedicated Files
|
|
24
|
-
|
|
25
|
-
All tests for a component must be distributed across three dedicated files, each covering a distinct implementation layer.
|
|
26
|
-
|
|
27
|
-
Policy:
|
|
28
|
-
|
|
29
|
-
- tests for metadata must live in `<component>.meta.test.ts`
|
|
30
|
-
- tests for validation must live in `<component>.validation.test.ts`
|
|
31
|
-
- tests for the component must live in `<component>.test.ts`
|
|
32
|
-
- test logic must not be shared or inlined across files of different concern layers
|
|
33
|
-
- `<component>.meta.test.ts` is the authoritative location for all vocabulary tests
|
|
34
|
-
- `<component>.validation.test.ts` is the authoritative location for all validator tests
|
|
35
|
-
- `<component>.test.ts` is the authoritative location for all component behavioral tests
|
|
36
|
-
|
|
37
|
-
This separation gives each test file a single clear responsibility:
|
|
38
|
-
|
|
39
|
-
- `<component>.meta.test.ts` verifies the vocabulary declared in `<component>.meta.ts`
|
|
40
|
-
- `<component>.validation.test.ts` verifies the validators declared in `<component>.validation.ts`
|
|
41
|
-
- `<component>.test.ts` verifies the behavior declared in the component specification
|
|
42
|
-
|
|
43
|
-
# 2) Tests Must Use the BDD Vocabulary
|
|
44
|
-
|
|
45
|
-
All tests must be expressed using the standard BDD vocabulary provided by the test helper. The vocabulary maps directly to Jasmine primitives with a natural-language prefix.
|
|
46
|
-
|
|
47
|
-
| Keyword | Maps to | Purpose |
|
|
48
|
-
| ------- | -------------------------------- | -------------------------------------------------------------------------------------- |
|
|
49
|
-
| `given` | `describe` prefixed with `Given` | Establishes the outer context — the precondition under which all contained tests apply |
|
|
50
|
-
| `and` | `describe` prefixed with `and` | Introduces additional context layers or continues the preceding context |
|
|
51
|
-
| `when` | `describe` prefixed with `when` | Introduces a triggering action or state change within a context |
|
|
52
|
-
| `then` | `it` prefixed with `then` | Declares a single assertion — the expected observable outcome |
|
|
53
|
-
|
|
54
|
-
Policy:
|
|
55
|
-
|
|
56
|
-
- every assertion must live in a `then` block
|
|
57
|
-
- no assertion may live directly in a `given`, `when`, or `and` block
|
|
58
|
-
- setup and teardown (`beforeEach`, `afterEach`) may appear in any `given`, `and`, or `when` block
|
|
59
|
-
- test descriptions must read as grammatical sentences when the keyword prefix is prepended
|
|
60
|
-
- descriptions must be written in plain language that traces directly to the specification
|
|
61
|
-
|
|
62
|
-
# 3) Test Suites Must Use Typed Describe Wrappers
|
|
63
|
-
|
|
64
|
-
Every top-level test group must use one of the typed describe wrappers that declare both the concern type and the canonical spec name.
|
|
65
|
-
|
|
66
|
-
| Wrapper | Concern | Canonical label source |
|
|
67
|
-
| --------------------------- | ----------------------- | ---------------------------------------------------------------- |
|
|
68
|
-
| `metadata(name, spec)` | Metadata vocabulary | `Metadata.*` (helper-supplied) or `State.*` (component metadata) |
|
|
69
|
-
| `validation(name, spec)` | Validator method | `State.*` (component metadata) |
|
|
70
|
-
| `configuration(name, spec)` | Component configuration | `Configuration.*` (helper-supplied) |
|
|
71
|
-
| `utility(name, spec)` | Utility | `Utilities.*` (helper-supplied) |
|
|
72
|
-
| `composition(name, spec)` | DOM composition | `Composition.*` (helper-supplied) |
|
|
73
|
-
| `element(name, spec)` | Element-scoped concern | `Composition.*` (helper-supplied) |
|
|
74
|
-
| `state(name, spec)` | State contract | `State.*` (component metadata) |
|
|
75
|
-
| `operation(name, spec)` | Public operation | `Operation.*` (component metadata) |
|
|
76
|
-
| `events(name, spec)` | Event contract | `Event.*` (component metadata) |
|
|
77
|
-
| `gesture(name, spec)` | Interaction gesture | `Gesture.*` (component metadata) |
|
|
78
|
-
|
|
79
|
-
Two label-source categories exist and must not be confused:
|
|
80
|
-
|
|
81
|
-
- **Helper-supplied constants** — `Metadata.*`, `Configuration.*`, `Utilities.*`, `Composition.*` — are declared in the test helper and identify orthogonal concern surfaces (the test names themselves).
|
|
82
|
-
- **Component metadata constants** — `State.*`, `Operation.*`, `Event.*`, `Gesture.*` — are declared in `<component>.meta.ts` and identify specification members.
|
|
83
|
-
|
|
84
|
-
Policy:
|
|
85
|
-
|
|
86
|
-
- every top-level test suite must use the typed wrapper that matches its concern
|
|
87
|
-
- the canonical name passed to the wrapper must come from a constant — never from a string literal
|
|
88
|
-
- helper-supplied constants are used when the wrapper name identifies a concern surface
|
|
89
|
-
- component metadata constants are used when the wrapper name identifies a specification member
|
|
90
|
-
- the typed wrapper sets the `type` and `spec` properties on each spec for structured JSON reporting
|
|
91
|
-
|
|
92
|
-
Example:
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
metadata(State.VISIBILITY, () => {
|
|
96
|
-
// tests for the Visibility value domain — name from pin.meta.ts
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
configuration(Configuration.TAG, () => {
|
|
100
|
-
// tests for Pin.Tag — name from the test helper
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
state(State.VISIBILITY, () => {
|
|
104
|
-
// tests for the visibility state contract — name from pin.meta.ts
|
|
105
|
-
});
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
# 4) Test Suite Ordering Must Mirror the Specification Concern Hierarchy
|
|
109
|
-
|
|
110
|
-
Within each test file, suites must be ordered to match the concern hierarchy defined in the workflow policy.
|
|
111
|
-
|
|
112
|
-
Policy for `<component>.meta.test.ts`:
|
|
113
|
-
|
|
114
|
-
- `Tag`
|
|
115
|
-
- `Attributes`
|
|
116
|
-
- `State` — followed immediately by one `metadata(...)` suite per value domain, in the order the state keys appear in `Attributes`
|
|
117
|
-
- `Operation`
|
|
118
|
-
- `Event`
|
|
119
|
-
- `Gesture`
|
|
120
|
-
|
|
121
|
-
Configuration constants such as `CSS` are realized through composition (the stylesheet link is verified in `composition(Composition.CSS, ...)`); they are not declared as metadata suites.
|
|
122
|
-
|
|
123
|
-
Policy for `<component>.validation.test.ts`:
|
|
124
|
-
|
|
125
|
-
- one `validation(...)` suite per attribute-backed state, in the order the keys appear in `Attributes`
|
|
126
|
-
|
|
127
|
-
Policy for `<component>.test.ts`:
|
|
128
|
-
|
|
129
|
-
- `Configuration` — one `configuration(...)` suite per declared configuration surface (`Configuration.TAG`, `Configuration.ATTRIBUTES`)
|
|
130
|
-
- `Utilities` — one `utility(...)` suite per declared utility surface (`Utilities.TEMPLATE`)
|
|
131
|
-
- `Composition` — one `composition(...)` suite per declared composition surface (`Composition.TEMPLATE`, `Composition.CSS`, `Composition.ELEMENT`)
|
|
132
|
-
- `State` — in the order declared in the specification
|
|
133
|
-
- `Operations` — in the order declared in the specification
|
|
134
|
-
- `Events` — in the order declared in the specification
|
|
135
|
-
- `Gestures` — in the order declared in the specification
|
|
136
|
-
|
|
137
|
-
# 5) Metadata Tests Must Verify the Declared Vocabulary
|
|
138
|
-
|
|
139
|
-
For every vocabulary constant declared in `<component>.meta.ts`, the `<component>.meta.test.ts` must verify existence — and, for enum-shaped constants, runtime type and the presence of every declared key.
|
|
140
|
-
|
|
141
|
-
Policy:
|
|
142
|
-
|
|
143
|
-
- every vocabulary constant declared in `<component>.meta.ts` must have a `metadata(...)` suite
|
|
144
|
-
- vocabulary constants are: `Tag`, `Attributes`, `State`, every value-domain constant exported by the component (e.g. `Visibility`, `Status`), `Operation`, `Event`, `Gesture`
|
|
145
|
-
- non-vocabulary configuration constants (e.g. `CSS`) are verified through their realized usage in `<component>.test.ts` and do not require metadata suites
|
|
146
|
-
- each suite must verify the constant is defined
|
|
147
|
-
- for enum-shaped constants, each suite must additionally verify the constant is of runtime type `"object"`, and that every declared key is defined
|
|
148
|
-
- key assertions use `expect(Constant.KEY).toBeDefined()` — not value equality
|
|
149
|
-
- value equality belongs to the validation layer, not the metadata layer
|
|
150
|
-
|
|
151
|
-
## 5.1) Scalar Constants Use the Flat Pattern
|
|
152
|
-
|
|
153
|
-
A scalar constant such as `Tag` has no key membership to verify. Its suite uses a flat `when → then` shape:
|
|
154
|
-
|
|
155
|
-
```typescript
|
|
156
|
-
metadata(Metadata.TAG, () => {
|
|
157
|
-
when("Tag imported", () => {
|
|
158
|
-
then("Tag is defined", () => {
|
|
159
|
-
expect(Tag).toBeDefined();
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
## 5.2) Enum-Shaped Constants Use the Nested Pattern
|
|
166
|
-
|
|
167
|
-
An enum-shaped constant such as `Attributes`, `State`, `Visibility`, `Status`, `Operation`, `Event`, or `Gesture` uses the nested `and → then → and → then → when → then` shape:
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
metadata(State.VISIBILITY, () => {
|
|
171
|
-
and("Visibility imported", () => {
|
|
172
|
-
then("Visibility is defined", () => {
|
|
173
|
-
expect(Visibility).toBeDefined();
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
and("Visibility is defined", () => {
|
|
177
|
-
then("Visibility is an object", () => {
|
|
178
|
-
expect(typeof Visibility).toBe("object");
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
when("Visibility is an object", () => {
|
|
182
|
-
then("`Visibility.VISIBLE` exists", () => {
|
|
183
|
-
expect(Visibility.VISIBLE).toBeDefined();
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
then("`Visibility.HIDDEN` exists", () => {
|
|
187
|
-
expect(Visibility.HIDDEN).toBeDefined();
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
Each declared key is asserted in its own `then` block inside `when("X is an object", ...)`.
|
|
196
|
-
|
|
197
|
-
# 6) Validation Tests Must Cover the Full Input Domain
|
|
198
|
-
|
|
199
|
-
For every validator declared in `<component>.validation.ts`, the `<component>.validation.test.ts` must verify the method exists, accepts every valid domain value without throwing, and rejects every invalid value with the exact error message.
|
|
200
|
-
|
|
201
|
-
Policy:
|
|
202
|
-
|
|
203
|
-
- every validator must have a `validation(...)` suite keyed to the corresponding state name (e.g. `State.VISIBILITY`)
|
|
204
|
-
- the outermost block under the typed wrapper is `given("Validate imported", ...)`
|
|
205
|
-
- each suite must verify the validator method is defined on the `Validate` class
|
|
206
|
-
- each suite must test every valid domain value independently in its own `and(...)` block:
|
|
207
|
-
- the call must not throw
|
|
208
|
-
- the return value must equal the input value
|
|
209
|
-
- each suite must test at least one invalid value:
|
|
210
|
-
- the call must throw
|
|
211
|
-
- the thrown error message must equal the exact expected string
|
|
212
|
-
|
|
213
|
-
Policy for the try/catch test structure:
|
|
214
|
-
|
|
215
|
-
- valid and invalid values are set in `beforeEach`
|
|
216
|
-
- the validator call is wrapped in a `try/catch` inside `beforeEach` — the result and the error are captured into separate variables
|
|
217
|
-
- error and result assertions appear in separate `then` blocks
|
|
218
|
-
- both variables must be declared in scope before the `beforeEach` that captures them
|
|
219
|
-
|
|
220
|
-
Example:
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
validation(State.VISIBILITY, () => {
|
|
224
|
-
given("Validate imported", () => {
|
|
225
|
-
then("Validate is defined", () => {
|
|
226
|
-
expect(Validate).toBeDefined();
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
and("Validate is defined", () => {
|
|
230
|
-
then("`Validate.visibility` static method is defined", () => {
|
|
231
|
-
expect(Validate.visibility).toBeDefined();
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
and("`value` is `Visibility.VISIBLE`", () => {
|
|
235
|
-
let value: any;
|
|
236
|
-
beforeEach(() => {
|
|
237
|
-
value = Visibility.VISIBLE;
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
when("`Validate.visibility(value)` is called", () => {
|
|
241
|
-
let error: unknown | undefined;
|
|
242
|
-
let result: any;
|
|
243
|
-
beforeEach(() => {
|
|
244
|
-
try {
|
|
245
|
-
result = Validate.visibility(value);
|
|
246
|
-
} catch (err) {
|
|
247
|
-
error = err;
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
then("no error is thrown", () => {
|
|
252
|
-
expect(error).toBeUndefined();
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
then("result is `Visibility.VISIBLE`", () => {
|
|
256
|
-
expect(result).toBe(Visibility.VISIBLE);
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
and("`value` is `invalid`", () => {
|
|
262
|
-
let value: any;
|
|
263
|
-
beforeEach(() => {
|
|
264
|
-
value = "invalid";
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
when("`Validate.visibility(value)` is called", () => {
|
|
268
|
-
let error: unknown | undefined;
|
|
269
|
-
beforeEach(() => {
|
|
270
|
-
try {
|
|
271
|
-
Validate.visibility(value);
|
|
272
|
-
} catch (err) {
|
|
273
|
-
error = err;
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
then("an error is thrown", () => {
|
|
278
|
-
expect(error).not.toBeUndefined();
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
and("an error is thrown", () => {
|
|
282
|
-
then(
|
|
283
|
-
"error message contains 'Invalid visibility value: invalid'",
|
|
284
|
-
() => {
|
|
285
|
-
expect((error as Error).message).toEqual(
|
|
286
|
-
"Invalid visibility value: invalid"
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
);
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
# 7) Component Tests Must Follow the Standard DOM Setup Pattern
|
|
299
|
-
|
|
300
|
-
Every test suite in `<component>.test.ts` that exercises the component in the DOM must follow the standard nested setup and teardown pattern.
|
|
301
|
-
|
|
302
|
-
Policy:
|
|
303
|
-
|
|
304
|
-
- the component must be registered in the custom element registry with `define(Pin.Tag, Pin)` inside a `beforeEach`
|
|
305
|
-
- the HTML template must be loaded with `await Pin.Template.load(...)` inside a `beforeEach`
|
|
306
|
-
- the component must be added to the DOM with `add<Pin>(Pin.Tag)` inside a `beforeEach`
|
|
307
|
-
- the component element must be removed with `pin.remove()` inside an `afterEach`
|
|
308
|
-
- the custom element registration must be cleaned up with `remove(Pin.Tag)` inside an `afterEach`
|
|
309
|
-
- DOM setup and teardown must be nested using `and(...)` describe blocks, ordered from outermost (registration) to innermost (element addition)
|
|
310
|
-
- any test that mutates component state must restore it to its default in `afterEach`
|
|
311
|
-
|
|
312
|
-
## 7.1) Outer Block: `given` vs `and`
|
|
313
|
-
|
|
314
|
-
The outermost block under a typed wrapper expresses the test's precondition narrative. Use `given(...)` when the suite verifies a structural or vocabulary contract; use `and(...)` when the suite verifies runtime behavior:
|
|
315
|
-
|
|
316
|
-
| Suite type | Outer block |
|
|
317
|
-
| ---------------------------------------- | ----------- |
|
|
318
|
-
| `state(...)` | `given` |
|
|
319
|
-
| `validation(...)` | `given` |
|
|
320
|
-
| `composition(Composition.TEMPLATE, ...)` | `given` |
|
|
321
|
-
| `composition(Composition.CSS, ...)` | `given` |
|
|
322
|
-
| `composition(Composition.ELEMENT, ...)` | `and` |
|
|
323
|
-
| `operation(...)` | `and` |
|
|
324
|
-
| `events(...)` | `and` |
|
|
325
|
-
| `gesture(...)` | `and` |
|
|
326
|
-
|
|
327
|
-
The distinction is semantic: structural and vocabulary contracts read naturally with an explicit `Given the component is defined...` precondition, while behavioral suites continue the implicit narrative established by their typed wrapper.
|
|
328
|
-
|
|
329
|
-
Example:
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
state(State.VISIBILITY, () => {
|
|
333
|
-
given("Pin is defined in custom element registry", () => {
|
|
334
|
-
beforeEach(() => {
|
|
335
|
-
define(Pin.Tag, Pin);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
and("a HTML Template is added to DOM", () => {
|
|
339
|
-
beforeEach(async () => {
|
|
340
|
-
await Pin.Template.load("pin.template.html");
|
|
341
|
-
});
|
|
342
|
-
afterEach(() => {
|
|
343
|
-
remove(Pin.Tag);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
and("a new pin is added to DOM", () => {
|
|
347
|
-
let pin: Pin;
|
|
348
|
-
beforeEach(() => {
|
|
349
|
-
pin = add<Pin>(Pin.Tag);
|
|
350
|
-
});
|
|
351
|
-
afterEach(() => {
|
|
352
|
-
pin.remove();
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
// assertions here
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
# 8) Configuration, Utility, and Composition Suites Have Defined Coverage
|
|
363
|
-
|
|
364
|
-
These suites verify the orthogonal surfaces exposed by the component class and its template, distinct from the specification members.
|
|
365
|
-
|
|
366
|
-
## 8.1) Configuration Suites
|
|
367
|
-
|
|
368
|
-
A `configuration(...)` suite verifies that a static class member equals the corresponding metadata constant.
|
|
369
|
-
|
|
370
|
-
Required:
|
|
371
|
-
|
|
372
|
-
- the static class getter must be verified as defined
|
|
373
|
-
- the static class getter must equal the metadata constant
|
|
374
|
-
|
|
375
|
-
```typescript
|
|
376
|
-
configuration(Configuration.TAG, () => {
|
|
377
|
-
and("Pin imported", () => {
|
|
378
|
-
then("Pin is defined", () => {
|
|
379
|
-
expect(Pin).toBeDefined();
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
and("Pin is defined", () => {
|
|
383
|
-
then("Pin.Tag static getter is defined", () => {
|
|
384
|
-
expect(Pin.Tag).toBeDefined();
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
and("Pin.Tag static getter is defined", () => {
|
|
388
|
-
then("Pin.Tag is Tag", () => {
|
|
389
|
-
expect(Pin.Tag).toBe(Tag);
|
|
390
|
-
});
|
|
391
|
-
});
|
|
392
|
-
});
|
|
393
|
-
});
|
|
394
|
-
});
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
## 8.2) Utility Suites
|
|
398
|
-
|
|
399
|
-
A `utility(...)` suite verifies that a static utility member is defined and of the correct runtime type.
|
|
400
|
-
|
|
401
|
-
```typescript
|
|
402
|
-
utility(Utilities.TEMPLATE, () => {
|
|
403
|
-
then("Pin.Template static property is defined", () => {
|
|
404
|
-
expect(Pin.Template).toBeDefined();
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
and("Pin.Template static property is defined", () => {
|
|
408
|
-
then("Pin.Template is a Template", () => {
|
|
409
|
-
expect(Pin.Template).toBeInstanceOf(Template);
|
|
410
|
-
});
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
## 8.3) Composition Suites
|
|
416
|
-
|
|
417
|
-
A `composition(...)` suite verifies the realized DOM structure or the realized runtime caching of declared composition parts.
|
|
418
|
-
|
|
419
|
-
- `composition(Composition.TEMPLATE, ...)` verifies that loading the template produces the declared parent-child containment in the shadow root.
|
|
420
|
-
- `composition(Composition.CSS, ...)` verifies that the shadow root contains a stylesheet link to the declared CSS file.
|
|
421
|
-
- `composition(Composition.ELEMENT, ...)` verifies that each `cached: true` part is cached on the component instance and references the realized DOM element.
|
|
422
|
-
|
|
423
|
-
```typescript
|
|
424
|
-
composition(Composition.TEMPLATE, () => {
|
|
425
|
-
given("Pin is defined in custom element registry", () => {
|
|
426
|
-
beforeEach(() => {
|
|
427
|
-
define(Pin.Tag, Pin);
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
and("HTML Template is added to DOM", () => {
|
|
431
|
-
let template: HTMLTemplateElement;
|
|
432
|
-
beforeEach(async () => {
|
|
433
|
-
template = (await Pin.Template.load(
|
|
434
|
-
"pin.template.html"
|
|
435
|
-
)) as HTMLTemplateElement;
|
|
436
|
-
});
|
|
437
|
-
afterEach(() => {
|
|
438
|
-
remove(Pin.Tag);
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
and("a new pin is added to DOM", () => {
|
|
442
|
-
let pin: Pin;
|
|
443
|
-
beforeEach(() => {
|
|
444
|
-
pin = add<Pin>(Pin.Tag);
|
|
445
|
-
});
|
|
446
|
-
afterEach(() => {
|
|
447
|
-
pin.remove();
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
then("`pin.root` contains `div.icon`", () => {
|
|
451
|
-
const div = pin.root.querySelector("div.icon");
|
|
452
|
-
expect(div).not.toBeNull();
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
when("`pin.root` contains `div.icon`", () => {
|
|
456
|
-
let div: HTMLDivElement;
|
|
457
|
-
beforeEach(() => {
|
|
458
|
-
div = pin.root.querySelector("div.icon") as HTMLDivElement;
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
then("`div.icon` contains `svg.unpinned`", () => {
|
|
462
|
-
const svg = div.querySelector("svg.unpinned");
|
|
463
|
-
expect(svg).not.toBeNull();
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
then("`div.icon` contains `svg.pinned`", () => {
|
|
467
|
-
const svg = div.querySelector("svg.pinned");
|
|
468
|
-
expect(svg).not.toBeNull();
|
|
469
|
-
});
|
|
470
|
-
});
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
});
|
|
474
|
-
});
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
# 9) State Tests Must Cover the Full Mutation Contract
|
|
478
|
-
|
|
479
|
-
For every state declared in the specification, the `<component>.test.ts` must verify the full mutation contract. The required assertions depend on whether the state is attribute-backed and whether it permits null normalization.
|
|
480
|
-
|
|
481
|
-
Policy — required for all state:
|
|
482
|
-
|
|
483
|
-
- the getter must be verified as defined
|
|
484
|
-
- the default value must be verified
|
|
485
|
-
- the setter must be verified using `hasSetter(pin, "<name>")`
|
|
486
|
-
- setting a valid value via the setter must update the getter
|
|
487
|
-
- setting an invalid value must throw with the exact error message
|
|
488
|
-
|
|
489
|
-
Policy — required for attribute-backed state:
|
|
490
|
-
|
|
491
|
-
- setting a valid value via the setter must sync the DOM attribute via `setAttribute`
|
|
492
|
-
- setting the DOM attribute via `setAttribute` must drive the getter (`attributeChangedCallback` path)
|
|
493
|
-
- for compulsory attribute-backed state, the attribute default must be verified — both that the attribute exists and that it carries the default value
|
|
494
|
-
|
|
495
|
-
Policy — required when the setter normalizes `null` to a default:
|
|
496
|
-
|
|
497
|
-
- setting `null` must leave the getter at the default value
|
|
498
|
-
|
|
499
|
-
Policy for invalid value tests:
|
|
500
|
-
|
|
501
|
-
- the invalid assignment must be wrapped in a `try/catch` inside `beforeEach`
|
|
502
|
-
- the `Error` is captured and asserted in a `then` block
|
|
503
|
-
- the assignment must be annotated with `// @ts-expect-error` to suppress the type error
|
|
504
|
-
|
|
505
|
-
Example (optional attribute-backed):
|
|
506
|
-
|
|
507
|
-
```typescript
|
|
508
|
-
then("`pin.visibility` getter exists", () => {
|
|
509
|
-
expect(pin.visibility).toBeDefined();
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
when("`pin.visibility` getter exists", () => {
|
|
513
|
-
then("`pin.visibility` is `Visibility.VISIBLE`", () => {
|
|
514
|
-
expect(pin.visibility).toBe(Visibility.VISIBLE);
|
|
515
|
-
});
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
then("`pin.visibility` setter exists", () => {
|
|
519
|
-
expect(hasSetter(pin, "visibility")).toBeTrue();
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
and("`pin.visibility` setter exists", () => {
|
|
523
|
-
when("`pin.visibility` is set to `Visibility.HIDDEN`", () => {
|
|
524
|
-
beforeEach(() => {
|
|
525
|
-
pin.visibility = Visibility.HIDDEN;
|
|
526
|
-
});
|
|
527
|
-
afterEach(() => {
|
|
528
|
-
pin.visibility = Visibility.VISIBLE;
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
then("`pin.visibility` is `Visibility.HIDDEN`", () => {
|
|
532
|
-
expect(pin.visibility).toBe(Visibility.HIDDEN);
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
then("pin `visibility` attribute is `Visibility.HIDDEN`", () => {
|
|
536
|
-
expect(pin.getAttribute(Attributes.VISIBILITY)).toBe(Visibility.HIDDEN);
|
|
537
|
-
});
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
when("`pin.visibility` is set to `null`", () => {
|
|
541
|
-
beforeEach(() => {
|
|
542
|
-
pin.visibility = null;
|
|
543
|
-
});
|
|
544
|
-
afterEach(() => {
|
|
545
|
-
pin.visibility = Visibility.VISIBLE;
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
then("`pin.visibility` is `Visibility.VISIBLE`", () => {
|
|
549
|
-
expect(pin.visibility).toBe(Visibility.VISIBLE);
|
|
550
|
-
});
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
when("`visibility` attribute is set to `Visibility.HIDDEN`", () => {
|
|
554
|
-
beforeEach(() => {
|
|
555
|
-
pin.setAttribute(Attributes.VISIBILITY, Visibility.HIDDEN);
|
|
556
|
-
});
|
|
557
|
-
afterEach(() => {
|
|
558
|
-
pin.removeAttribute(Attributes.VISIBILITY);
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
then("`pin.visibility` is `Visibility.HIDDEN`", () => {
|
|
562
|
-
expect(pin.visibility).toBe(Visibility.HIDDEN);
|
|
563
|
-
});
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
when("`pin.visibility` is set to `invalid`", () => {
|
|
567
|
-
let error: Error;
|
|
568
|
-
beforeEach(() => {
|
|
569
|
-
try {
|
|
570
|
-
// @ts-expect-error
|
|
571
|
-
pin.visibility = "invalid";
|
|
572
|
-
} catch (err) {
|
|
573
|
-
error = err as Error;
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
afterEach(() => {
|
|
577
|
-
pin.visibility = Visibility.VISIBLE;
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
then("an error is thrown", () => {
|
|
581
|
-
expect(error).toBeDefined();
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
then("error message contains 'Invalid visibility value: invalid'", () => {
|
|
585
|
-
expect(error.message).toBe("Invalid visibility value: invalid");
|
|
586
|
-
});
|
|
587
|
-
});
|
|
588
|
-
});
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
Example (compulsory attribute-backed — attribute default verified):
|
|
592
|
-
|
|
593
|
-
```typescript
|
|
594
|
-
then("`status` attribute exists", () => {
|
|
595
|
-
expect(pin.getAttribute(Attributes.STATUS)).not.toBeNull();
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
and("`status` attribute exists", () => {
|
|
599
|
-
then("`status` attribute is `Status.UNPINNED`", () => {
|
|
600
|
-
expect(pin.getAttribute(Attributes.STATUS)).toBe(Status.UNPINNED);
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
when("`status` attribute is set to `Status.PINNED`", () => {
|
|
604
|
-
beforeEach(() => {
|
|
605
|
-
pin.setAttribute(Attributes.STATUS, Status.PINNED);
|
|
606
|
-
});
|
|
607
|
-
afterEach(() => {
|
|
608
|
-
pin.setAttribute(Attributes.STATUS, Status.UNPINNED);
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
then("`pin.status` is `Status.PINNED`", () => {
|
|
612
|
-
expect(pin.status).toBe(Status.PINNED);
|
|
613
|
-
});
|
|
614
|
-
});
|
|
615
|
-
});
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
# 10) Operation Tests Must Verify Existence and Behavioral Outcome
|
|
619
|
-
|
|
620
|
-
For every operation declared in the specification, the `<component>.test.ts` must verify the method exists and produces the state outcome declared in the specification.
|
|
621
|
-
|
|
622
|
-
Policy:
|
|
623
|
-
|
|
624
|
-
- the operation must be verified as defined on the component instance using `expect(pin.{operation}).toBeDefined()`
|
|
625
|
-
- calling the operation must produce the state outcome declared in the specification's `returns` contract
|
|
626
|
-
- the outcome is verified by reading the relevant state getter after calling the operation
|
|
627
|
-
- when an operation is only semantically meaningful from a specific prior state (e.g., `show` requires the component to already be `hidden`), that precondition must be established in `beforeEach` before the operation is called
|
|
628
|
-
- when an operation produces a bidirectional outcome (e.g., `toggle`), both directions must be verified — each direction in its own sibling `and(...)` block establishing the prior state, with the operation invoked in a nested `when("invoking ...")` block
|
|
629
|
-
|
|
630
|
-
Example (single-direction):
|
|
631
|
-
|
|
632
|
-
```typescript
|
|
633
|
-
then("`pin.hide` method exists", () => {
|
|
634
|
-
expect(pin.hide).toBeDefined();
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
and("`pin.hide` method exists", () => {
|
|
638
|
-
when("invoking `pin.hide`", () => {
|
|
639
|
-
beforeEach(() => {
|
|
640
|
-
pin.hide();
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
then("`pin.visibility` is `Visibility.HIDDEN`", () => {
|
|
644
|
-
expect(pin.visibility).toBe(Visibility.HIDDEN);
|
|
645
|
-
});
|
|
646
|
-
});
|
|
647
|
-
});
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
Example (bidirectional toggle):
|
|
651
|
-
|
|
652
|
-
```typescript
|
|
653
|
-
then("`pin.toggle` method exists", () => {
|
|
654
|
-
expect(pin.toggle).toBeDefined();
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
and("`pin.toggle` method exists", () => {
|
|
658
|
-
and("`pin.status` is set to `Status.UNPINNED`", () => {
|
|
659
|
-
beforeEach(() => {
|
|
660
|
-
pin.status = Status.UNPINNED;
|
|
661
|
-
});
|
|
662
|
-
afterEach(() => {
|
|
663
|
-
pin.status = Status.UNPINNED;
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
when("invoking `pin.toggle`", () => {
|
|
667
|
-
beforeEach(() => {
|
|
668
|
-
pin.toggle();
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
then("`pin.status` is `Status.PINNED`", () => {
|
|
672
|
-
expect(pin.status).toBe(Status.PINNED);
|
|
673
|
-
});
|
|
674
|
-
});
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
and("`pin.status` is set to `Status.PINNED`", () => {
|
|
678
|
-
beforeEach(() => {
|
|
679
|
-
pin.status = Status.PINNED;
|
|
680
|
-
});
|
|
681
|
-
afterEach(() => {
|
|
682
|
-
pin.status = Status.UNPINNED;
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
when("invoking `pin.toggle`", () => {
|
|
686
|
-
beforeEach(() => {
|
|
687
|
-
pin.toggle();
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
then("`pin.status` is `Status.UNPINNED`", () => {
|
|
691
|
-
expect(pin.status).toBe(Status.UNPINNED);
|
|
692
|
-
});
|
|
693
|
-
});
|
|
694
|
-
});
|
|
695
|
-
});
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
# 11) Event Tests Must Verify Listener Replacement Semantics and Payload
|
|
699
|
-
|
|
700
|
-
For every event declared in the specification, the `<component>.test.ts` must verify the subscription setter exists, that assigning a new listener deregisters the previous one, and that the dispatched event carries the correct detail payload.
|
|
701
|
-
|
|
702
|
-
Policy:
|
|
703
|
-
|
|
704
|
-
- the event setter must be verified using `hasSetter(pin, Event.ON_{NAME})`
|
|
705
|
-
- listener replacement must be verified: when a second listener replaces the first, only the second listener must be called when the event fires — the first must not be called
|
|
706
|
-
- the event payload must match the structure declared in the specification's `parameters`
|
|
707
|
-
- spies must be created with `jasmine.createSpy`
|
|
708
|
-
- the payload is asserted via `jasmine.objectContaining({ detail: { ... } })` passed to `toHaveBeenCalledWith`
|
|
709
|
-
- for events that fire on a state transition requiring a prior state, the prior state must be established in `beforeEach` before listeners are assigned, so that assigning the listener does not capture the unrelated transition that established the prior state
|
|
710
|
-
|
|
711
|
-
Example:
|
|
712
|
-
|
|
713
|
-
```typescript
|
|
714
|
-
events(Event.ON_HIDE, () => {
|
|
715
|
-
and("Pin is defined in custom element registry", () => {
|
|
716
|
-
beforeEach(() => {
|
|
717
|
-
define(Pin.Tag, Pin);
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
and("HTML Template is added to DOM", () => {
|
|
721
|
-
beforeEach(async () => {
|
|
722
|
-
await Pin.Template.load("pin.template.html");
|
|
723
|
-
});
|
|
724
|
-
afterEach(() => {
|
|
725
|
-
remove(Pin.Tag);
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
and("a new pin is added to DOM", () => {
|
|
729
|
-
let pin: Pin;
|
|
730
|
-
beforeEach(() => {
|
|
731
|
-
pin = add<Pin>(Pin.Tag);
|
|
732
|
-
});
|
|
733
|
-
afterEach(() => {
|
|
734
|
-
pin.remove();
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
then("`pin.onhide` setter exists", () => {
|
|
738
|
-
expect(hasSetter(pin, Event.ON_HIDE)).toBeTrue();
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
and("`pin.onhide` setter exists", () => {
|
|
742
|
-
and("`pin.onhide` is set to a listener", () => {
|
|
743
|
-
let onhide: jasmine.Spy;
|
|
744
|
-
beforeEach(() => {
|
|
745
|
-
onhide = jasmine.createSpy("onhide");
|
|
746
|
-
pin.onhide = onhide;
|
|
747
|
-
});
|
|
748
|
-
afterEach(() => {
|
|
749
|
-
pin.onhide = null;
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
and("`pin.onhide` is set to a new listener", () => {
|
|
753
|
-
let onhide2: jasmine.Spy;
|
|
754
|
-
beforeEach(() => {
|
|
755
|
-
onhide2 = jasmine.createSpy("onhide2");
|
|
756
|
-
pin.onhide = onhide2;
|
|
757
|
-
});
|
|
758
|
-
afterEach(() => {
|
|
759
|
-
pin.onhide = null;
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
when("`pin.visibility` is set to `Visibility.HIDDEN`", () => {
|
|
763
|
-
beforeEach(() => {
|
|
764
|
-
pin.visibility = Visibility.HIDDEN;
|
|
765
|
-
});
|
|
766
|
-
afterEach(() => {
|
|
767
|
-
pin.visibility = Visibility.VISIBLE;
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
then("old `pin.onhide` is not called", () => {
|
|
771
|
-
expect(onhide).not.toHaveBeenCalled();
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
then("new `pin.onhide` is called", () => {
|
|
775
|
-
expect(onhide2).toHaveBeenCalled();
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
then(
|
|
779
|
-
"new `pin.onhide` is called with `{ detail: { visibility: Visibility.HIDDEN } }`",
|
|
780
|
-
() => {
|
|
781
|
-
expect(onhide2).toHaveBeenCalledWith(
|
|
782
|
-
jasmine.objectContaining({
|
|
783
|
-
detail: { visibility: Visibility.HIDDEN }
|
|
784
|
-
})
|
|
785
|
-
);
|
|
786
|
-
}
|
|
787
|
-
);
|
|
788
|
-
});
|
|
789
|
-
});
|
|
790
|
-
});
|
|
791
|
-
});
|
|
792
|
-
});
|
|
793
|
-
});
|
|
794
|
-
});
|
|
795
|
-
});
|
|
796
|
-
```
|
|
797
|
-
|
|
798
|
-
# 12) Gesture Tests Must Verify the End-to-End Interaction Chain
|
|
799
|
-
|
|
800
|
-
For every behavioral gesture declared in the specification, the `<component>.test.ts` must verify the full interaction chain: DOM event on the realized element → operation invocation → state change → event dispatch.
|
|
801
|
-
|
|
802
|
-
Policy:
|
|
803
|
-
|
|
804
|
-
- the DOM event is triggered directly on the realized element via `.click()` or the equivalent native method
|
|
805
|
-
- the element is located by selector (e.g., `pin.root.querySelector(".icon")`) — this verifies that the gesture wiring depends on the realized composition, not on a cached reference name
|
|
806
|
-
- the state outcome of the triggered operation must be verified via the getter
|
|
807
|
-
- the event dispatched as a consequence of the state change must be verified as having been called
|
|
808
|
-
- for gestures that trigger a bidirectional toggle, both directions must be verified in sequence: a second interaction block nested inside the first restores the prior state and confirms the reverse outcome
|
|
809
|
-
- visual gestures declared with `effects` are realized entirely in CSS — they do not require JavaScript assertions in the test suite
|
|
810
|
-
|
|
811
|
-
Example:
|
|
812
|
-
|
|
813
|
-
```typescript
|
|
814
|
-
gesture(Gesture.CLICK, () => {
|
|
815
|
-
and("Pin is defined in custom element registry", () => {
|
|
816
|
-
beforeEach(() => {
|
|
817
|
-
define(Pin.Tag, Pin);
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
and("HTML Template is added to DOM", () => {
|
|
821
|
-
beforeEach(async () => {
|
|
822
|
-
await Pin.Template.load("pin.template.html");
|
|
823
|
-
});
|
|
824
|
-
afterEach(() => {
|
|
825
|
-
remove(Pin.Tag);
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
and("a new pin is added to DOM", () => {
|
|
829
|
-
let pin: Pin;
|
|
830
|
-
beforeEach(() => {
|
|
831
|
-
pin = add<Pin>(Pin.Tag);
|
|
832
|
-
});
|
|
833
|
-
afterEach(() => {
|
|
834
|
-
pin.remove();
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
when("user `click` on `div` with class `icon`", () => {
|
|
838
|
-
let onpin: jasmine.Spy;
|
|
839
|
-
beforeEach(() => {
|
|
840
|
-
onpin = jasmine.createSpy("onpin");
|
|
841
|
-
pin.onpin = onpin;
|
|
842
|
-
const icon = pin.root.querySelector(".icon") as HTMLElement;
|
|
843
|
-
icon.click();
|
|
844
|
-
});
|
|
845
|
-
|
|
846
|
-
then("`pin.status` is `Status.PINNED`", () => {
|
|
847
|
-
expect(pin.status).toEqual(Status.PINNED);
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
then("`onpin` is called", () => {
|
|
851
|
-
expect(onpin).toHaveBeenCalled();
|
|
852
|
-
});
|
|
853
|
-
|
|
854
|
-
and("user `click` on `div` with class `icon`", () => {
|
|
855
|
-
let onunpin: jasmine.Spy;
|
|
856
|
-
beforeEach(() => {
|
|
857
|
-
onunpin = jasmine.createSpy("onunpin");
|
|
858
|
-
pin.onunpin = onunpin;
|
|
859
|
-
const icon = pin.root.querySelector(".icon") as HTMLElement;
|
|
860
|
-
icon.click();
|
|
861
|
-
});
|
|
862
|
-
|
|
863
|
-
then("`pin.status` is `Status.UNPINNED`", () => {
|
|
864
|
-
expect(pin.status).toEqual(Status.UNPINNED);
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
then("`onunpin` is called", () => {
|
|
868
|
-
expect(onunpin).toHaveBeenCalled();
|
|
869
|
-
});
|
|
870
|
-
});
|
|
871
|
-
});
|
|
872
|
-
});
|
|
873
|
-
});
|
|
874
|
-
});
|
|
875
|
-
});
|
|
876
|
-
```
|