@effect.healthcare/messenger 0.4.2 → 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.
@@ -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
- ```