@company-semantics/contracts 35.0.0 → 35.1.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/package.json +1 -1
- package/src/notifications/README.md +21 -19
- package/src/notifications/__tests__/README.md +42 -47
- package/src/notifications/__tests__/__snapshots__/README.md +15 -8
- package/src/notifications/__tests__/__snapshots__/registry.test.ts.snap +31 -0
- package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +2 -2
- package/src/notifications/__tests__/context.test.ts +5 -5
- package/src/notifications/__tests__/definition.test.ts +2 -2
- package/src/notifications/__tests__/fixtures.ts +226 -0
- package/src/notifications/__tests__/kinds.test.ts +2 -3
- package/src/notifications/__tests__/registry.test.ts +27 -27
- package/src/notifications/__tests__/render-snapshot.test.ts +10 -6
- package/src/notifications/kinds/README.md +2 -2
- package/src/notifications/render.ts +6 -6
- package/src/notifications/renderers/email/README.md +10 -6
- package/src/notifications/renderers/email/__tests__/README.md +19 -16
- package/src/notifications/renderers/email/__tests__/render.test.ts +44 -36
- package/src/notifications/renderers/email/chat.ts +3 -2
- package/src/notifications/renderers/email/constants.ts +17 -0
- package/src/notifications/renderers/email/index.ts +4 -4
- package/src/notifications/renderers/email/render.ts +15 -2
- package/src/notifications/renderers/email/shells.ts +3 -2
- package/src/notifications/text.ts +2 -2
- package/src/notifications/__tests__/output-parity.golden.ts +0 -363
- package/src/notifications/__tests__/output-parity.test.ts +0 -122
|
@@ -13,11 +13,15 @@
|
|
|
13
13
|
* every snapshot key still resolves. That is the whole claim of the move: the
|
|
14
14
|
* keys still match, so the bytes are still the bytes.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* This file used to be the reviewable companion to `./output-parity.test.ts`,
|
|
17
|
+
* which was the load-bearing one: a snapshot can be regenerated with a
|
|
18
|
+
* keystroke, a committed golden cannot, so the golden was the proof and this was
|
|
19
|
+
* the documentation. ADR-CONTRACTS-087 retired that golden once its migration
|
|
20
|
+
* proof shipped in 35.0.0 — so this file is now BOTH. There is no longer a
|
|
21
|
+
* second oracle to catch a snapshot refreshed on autopilot; the diff you read
|
|
22
|
+
* before typing `vitest -u` is the control.
|
|
19
23
|
*
|
|
20
|
-
* The clock is pinned to `
|
|
24
|
+
* The clock is pinned to `FIXTURE_CLOCK`'s year rather than left to run.
|
|
21
25
|
* These snapshots used to be captured under the wall clock — with the copyright
|
|
22
26
|
* year baked into every one of them — so they were due to fail on the next New
|
|
23
27
|
* Year's Day for calendar reasons alone. The year is a value in `RenderContext`
|
|
@@ -32,11 +36,11 @@ import type { NotificationPayloads } from "../payloads";
|
|
|
32
36
|
import { renderEmail } from "../render";
|
|
33
37
|
import { NOTIFICATION_DEFINITIONS } from "../registry";
|
|
34
38
|
import type { RenderedEmail } from "../renderers/email";
|
|
35
|
-
import {
|
|
39
|
+
import { FIXTURE_CLOCK } from "./fixtures";
|
|
36
40
|
|
|
37
41
|
/** Pinned so no wall-clock year can reach a snapshot. */
|
|
38
42
|
const CONTEXT: RenderContextOverrides = {
|
|
39
|
-
brand: { copyrightYear: new Date(
|
|
43
|
+
brand: { copyrightYear: new Date(FIXTURE_CLOCK).getUTCFullYear() },
|
|
40
44
|
};
|
|
41
45
|
|
|
42
46
|
interface Fixture {
|
|
@@ -50,8 +50,8 @@ template literal the compiler checks.
|
|
|
50
50
|
- Definitions take `formatExpiry`, `titleCase`, `NOTICE` and `ACCESS_PHRASE` from
|
|
51
51
|
`../text` — never from a renderer. They are content (phrasing and display
|
|
52
52
|
format), not markup, so they sit beside the kinds rather than inside the
|
|
53
|
-
channel that used to own them.
|
|
54
|
-
|
|
53
|
+
channel that used to own them. `../__tests__/render-snapshot.test.ts` locks
|
|
54
|
+
these strings; a second copy of one is the exact failure this migration exists
|
|
55
55
|
to prevent.
|
|
56
56
|
- A file per kind, named after its old `email/render` counterpart. Kinds do not
|
|
57
57
|
share a module — the point is that a kind is self-contained.
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* `renderToChannel` is the whole pipeline in one call: look the kind up in
|
|
5
5
|
* `NOTIFICATION_DEFINITIONS`, `compose` its payload into channel-agnostic
|
|
6
6
|
* `NotificationContent`, hand that to a `Renderer`. Every piece already existed
|
|
7
|
-
*
|
|
8
|
-
* module is the wiring, not a rendering change.
|
|
7
|
+
* when this was written — the module is the wiring, not a rendering change.
|
|
9
8
|
*
|
|
10
9
|
* What it deliberately does NOT do is dispatch. The old `renderEmail` was a
|
|
11
10
|
* twelve-arm `switch` that threw on the one arm it did not have; here the
|
|
@@ -30,10 +29,11 @@
|
|
|
30
29
|
* only to build one when the caller supplies none, and only via
|
|
31
30
|
* `createRenderContext` — the single sanctioned clock read in this domain.
|
|
32
31
|
* Pass a context to get reproducible bytes.
|
|
33
|
-
* - Output
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
32
|
+
* - Output is locked by `__tests__/render-snapshot.test.ts`, across every fixture
|
|
33
|
+
* in `__tests__/fixtures.ts`. A diff there means a real sent email changed:
|
|
34
|
+
* review it, then regenerate. This rewiring was originally proven safe by
|
|
35
|
+
* byte-equality with the old email layer; that proof shipped in 35.0.0 and was
|
|
36
|
+
* retired with the golden (ADR-CONTRACTS-087).
|
|
37
37
|
* - A kind's `defaults` layer UNDER the caller's wishes, never over them. A full
|
|
38
38
|
* `RenderContext` has no gaps left to fill, so it wins outright; partial
|
|
39
39
|
* overrides are filled from `defaults` first and the package default last.
|
|
@@ -48,12 +48,16 @@ expect to style it properly rather than trust the placeholder.
|
|
|
48
48
|
|
|
49
49
|
## Invariants
|
|
50
50
|
|
|
51
|
-
- Output is
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
- Output is locked by `../../__tests__/render-snapshot.test.ts`, across every
|
|
52
|
+
fixture in `../../__tests__/fixtures.ts` — a diff there means a real sent email
|
|
53
|
+
changed, so review it before regenerating. Byte-identity with the old
|
|
54
|
+
`src/email/render` was the whole constraint of the migration; it held, shipped
|
|
55
|
+
in 35.0.0, and ADR-CONTRACTS-087 retired the golden that proved it, because
|
|
56
|
+
that proof could not double as permission to change the design.
|
|
57
|
+
- `__tests__/render.test.ts` asserts the rules that outlive any redesign — every
|
|
58
|
+
element type supported, subject from the content's title, year and signer from
|
|
59
|
+
context, the `:` restored, user content escaped. Markup belongs in the
|
|
60
|
+
snapshot, not there.
|
|
57
61
|
- PURE. `render` is a function of `(content, context)`: no clock, no environment,
|
|
58
62
|
no I/O. The copyright year comes from `context.brand.copyrightYear`, which is
|
|
59
63
|
what retires the `new Date().getFullYear()` hidden inside the old
|
|
@@ -4,26 +4,29 @@
|
|
|
4
4
|
|
|
5
5
|
Tests for the email channel (ADR-CONTRACTS-086).
|
|
6
6
|
|
|
7
|
-
`render.test.ts`
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
`render.test.ts` pins the renderer's behavioural rules — the claims that hold no
|
|
8
|
+
matter how the email is styled: that `supports` is total, that the subject comes
|
|
9
|
+
from the content's title, that the year and signer come from `RenderContext`
|
|
10
|
+
rather than a clock, that the `:` separator the content model drops is put back,
|
|
11
|
+
and that user-controlled fields are escaped. It also renders every fixture in
|
|
12
|
+
`../../../__tests__/fixtures.ts` through `NOTIFICATION_DEFINITIONS`, which proves
|
|
13
|
+
each kind composes and renders on both surfaces at all.
|
|
14
|
+
|
|
15
|
+
The MARKUP is not here. `../../../__tests__/render-snapshot.test.ts` locks that.
|
|
13
16
|
|
|
14
17
|
## Invariants
|
|
15
18
|
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
promise. It is never regenerated to make this file pass — and since that layer
|
|
20
|
-
is now deleted, the golden is the only record of those bytes, so regenerating
|
|
21
|
-
it destroys the evidence rather than updating it.
|
|
19
|
+
- Rules here, markup there. A test in this file that asserts what the HTML looks
|
|
20
|
+
like is in the wrong file: it belongs in the snapshot, where a redesign shows
|
|
21
|
+
up as a reviewable diff instead of a wall of broken assertions.
|
|
22
22
|
- NO fake timers. The old layer's `signature()` read the wall clock, which is why
|
|
23
23
|
every render test had to freeze it; here the year is a field on
|
|
24
24
|
`RenderContext`, and keeping this file clock-free is the improvement being
|
|
25
25
|
locked in.
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
- This file used to assert every fixture byte-for-byte against a frozen golden of
|
|
27
|
+
what `src/email/render` emitted at 32.0.0 — the migration's "no rendered email
|
|
28
|
+
changed" promise, checked here at the RENDERER and in a companion suite at the
|
|
29
|
+
ENTRY POINT (`renderToChannel`). The promise held and shipped in 35.0.0;
|
|
30
|
+
ADR-CONTRACTS-087 retired both, because byte-equality with a deleted layer
|
|
31
|
+
cannot survive email design being deliberately changed. The bytes are in git at
|
|
32
|
+
`v35.0.0`.
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The email renderer's
|
|
2
|
+
* The email renderer's behavioural rules (ADR-CONTRACTS-086) — the claims that
|
|
3
|
+
* hold no matter how the email is styled: it supports every element type, takes
|
|
4
|
+
* the subject from the content's title, takes the year and signer from context
|
|
5
|
+
* rather than a clock, restores the `:` the content model does not carry, and
|
|
6
|
+
* escapes user-controlled text.
|
|
3
7
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* This does NOT replace `../../../__tests__/output-parity.test.ts`. That one
|
|
11
|
-
* pins the ENTRY POINT (`renderToChannel`, the whole pipeline) against the same
|
|
12
|
-
* golden; this pins the RENDERER alone, so a failure here says the markup moved
|
|
13
|
-
* rather than that the wiring did. Two levels, one golden: either drifting is a
|
|
14
|
-
* failure.
|
|
8
|
+
* The MARKUP is not asserted here. `../../../__tests__/render-snapshot.test.ts`
|
|
9
|
+
* locks that, as a reviewable regenerable diff. Until ADR-CONTRACTS-087 this
|
|
10
|
+
* file also asserted every fixture byte-for-byte against the 32.0.0 golden, to
|
|
11
|
+
* prove the multi-channel generalisation moved no bytes; that proof shipped in
|
|
12
|
+
* 35.0.0 and was retired with the golden, because email design is now allowed to
|
|
13
|
+
* change and byte-equality to a deleted layer cannot survive it being exercised.
|
|
15
14
|
*
|
|
16
15
|
* There is no fake timer here, and that is the improvement being locked in: the
|
|
17
|
-
* year comes from `RenderContext`, so the
|
|
16
|
+
* year comes from `RenderContext`, so the fixtures' clock is just a number.
|
|
18
17
|
*/
|
|
19
18
|
|
|
20
19
|
import { describe, expect, it } from "vitest";
|
|
@@ -26,23 +25,21 @@ import type {
|
|
|
26
25
|
import type { RenderContext } from "../../../context";
|
|
27
26
|
import { NOTIFICATION_DEFINITIONS } from "../../../registry";
|
|
28
27
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
OUTPUT_PARITY_GOLDEN,
|
|
28
|
+
FIXTURE_CLOCK,
|
|
29
|
+
NOTIFICATION_FIXTURES,
|
|
32
30
|
fixtureKey,
|
|
33
|
-
} from "../../../__tests__/
|
|
31
|
+
} from "../../../__tests__/fixtures";
|
|
34
32
|
import { emailRenderer } from "../index";
|
|
35
33
|
|
|
36
34
|
/**
|
|
37
|
-
* The
|
|
35
|
+
* The fixtures' context, stated rather than ticked. The old layer reached for
|
|
38
36
|
* `new Date().getFullYear()` mid-render, which is why every render test had to
|
|
39
|
-
* freeze the clock; here the year
|
|
40
|
-
* value.
|
|
37
|
+
* freeze the clock; here the year is simply a value.
|
|
41
38
|
*/
|
|
42
|
-
const
|
|
39
|
+
const FIXTURE_CONTEXT: RenderContext = {
|
|
43
40
|
brand: {
|
|
44
41
|
name: "Company Semantics",
|
|
45
|
-
copyrightYear: new Date(
|
|
42
|
+
copyrightYear: new Date(FIXTURE_CLOCK).getUTCFullYear(),
|
|
46
43
|
},
|
|
47
44
|
};
|
|
48
45
|
|
|
@@ -77,7 +74,7 @@ describe("emailRenderer", () => {
|
|
|
77
74
|
metadata: { kind: "org.invite", title: "Join Acme" },
|
|
78
75
|
sections: [{ elements: [{ type: "body", text: "Hello." }] }],
|
|
79
76
|
};
|
|
80
|
-
expect(emailRenderer.render(content,
|
|
77
|
+
expect(emailRenderer.render(content, FIXTURE_CONTEXT).subject).toBe(
|
|
81
78
|
"Join Acme",
|
|
82
79
|
);
|
|
83
80
|
});
|
|
@@ -100,7 +97,7 @@ describe("emailRenderer", () => {
|
|
|
100
97
|
metadata: { kind: "org.invite", title: "t" },
|
|
101
98
|
sections: [{ elements: [{ type: "signature", signer: "Grace" }] }],
|
|
102
99
|
};
|
|
103
|
-
expect(emailRenderer.render(content,
|
|
100
|
+
expect(emailRenderer.render(content, FIXTURE_CONTEXT).text).toContain(
|
|
104
101
|
"• Grace",
|
|
105
102
|
);
|
|
106
103
|
});
|
|
@@ -126,7 +123,7 @@ describe("emailRenderer", () => {
|
|
|
126
123
|
},
|
|
127
124
|
],
|
|
128
125
|
};
|
|
129
|
-
const { text } = emailRenderer.render(content,
|
|
126
|
+
const { text } = emailRenderer.render(content, FIXTURE_CONTEXT);
|
|
130
127
|
expect(text).toContain("Status: Valid");
|
|
131
128
|
expect(text).toContain("Request details:");
|
|
132
129
|
expect(text).toContain("IP address: 203.0.113.1");
|
|
@@ -141,7 +138,7 @@ describe("emailRenderer", () => {
|
|
|
141
138
|
},
|
|
142
139
|
],
|
|
143
140
|
};
|
|
144
|
-
const { html } = emailRenderer.render(content,
|
|
141
|
+
const { html } = emailRenderer.render(content, FIXTURE_CONTEXT);
|
|
145
142
|
expect(html).toContain("<script>");
|
|
146
143
|
expect(html).not.toContain("<script>");
|
|
147
144
|
});
|
|
@@ -153,27 +150,38 @@ describe("emailRenderer", () => {
|
|
|
153
150
|
details: "5 codes in 2 minutes",
|
|
154
151
|
timestamp: "2026-07-15T00:00:00.000Z",
|
|
155
152
|
},
|
|
156
|
-
|
|
153
|
+
FIXTURE_CONTEXT,
|
|
157
154
|
);
|
|
158
|
-
expect(emailRenderer.render(content,
|
|
159
|
-
emailRenderer.render(content,
|
|
155
|
+
expect(emailRenderer.render(content, FIXTURE_CONTEXT)).toEqual(
|
|
156
|
+
emailRenderer.render(content, FIXTURE_CONTEXT),
|
|
160
157
|
);
|
|
161
158
|
});
|
|
162
159
|
|
|
163
|
-
describe("
|
|
164
|
-
for
|
|
160
|
+
describe("renders every fixture", () => {
|
|
161
|
+
// Was a byte-for-byte assertion against the 32.0.0 golden until
|
|
162
|
+
// ADR-CONTRACTS-087 retired it. The markup itself is locked by
|
|
163
|
+
// `../../../__tests__/render-snapshot.test.ts`; what is worth proving HERE
|
|
164
|
+
// is narrower and survives a redesign — that every kind in the registry
|
|
165
|
+
// composes and renders through this channel at all, on both surfaces. A kind
|
|
166
|
+
// whose `compose` reaches for a field its payload lacks fails here, at the
|
|
167
|
+
// renderer, rather than in whichever consumer sends it first.
|
|
168
|
+
for (const fixture of NOTIFICATION_FIXTURES) {
|
|
165
169
|
const key = fixtureKey(fixture.kind, fixture.name);
|
|
166
|
-
it(`${key}
|
|
170
|
+
it(`${key}`, () => {
|
|
167
171
|
const definition = NOTIFICATION_DEFINITIONS[fixture.kind];
|
|
168
172
|
// `payload as never`: the fixture call sites are type-checked in the
|
|
169
|
-
//
|
|
173
|
+
// fixtures module; this only bridges compose's per-kind payload.
|
|
170
174
|
const content = definition.compose(
|
|
171
175
|
fixture.payload as never,
|
|
172
|
-
|
|
176
|
+
FIXTURE_CONTEXT,
|
|
173
177
|
);
|
|
174
|
-
|
|
175
|
-
|
|
178
|
+
const { subject, text, html } = emailRenderer.render(
|
|
179
|
+
content,
|
|
180
|
+
FIXTURE_CONTEXT,
|
|
176
181
|
);
|
|
182
|
+
expect(subject).toBeTruthy();
|
|
183
|
+
expect(text).toBeTruthy();
|
|
184
|
+
expect(html).toContain("<!DOCTYPE html>");
|
|
177
185
|
});
|
|
178
186
|
}
|
|
179
187
|
});
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* the "⋮" dots.
|
|
9
9
|
*
|
|
10
10
|
* INVARIANTS:
|
|
11
|
-
* - Pure, and
|
|
12
|
-
*
|
|
11
|
+
* - Pure. The box art is real output, and every column of it is locked by
|
|
12
|
+
* `../../__tests__/render-snapshot.test.ts` — a stray space is a visibly
|
|
13
|
+
* broken email, not a whitespace nit.
|
|
13
14
|
* - Both surfaces truncate at the same point — `clampMessage` is the one
|
|
14
15
|
* truncation authority, and HTML and plain text both run content through it.
|
|
15
16
|
*/
|
|
@@ -33,6 +33,23 @@ export const MONO = `font-family: ${MONO_FONT_STACK};`;
|
|
|
33
33
|
*/
|
|
34
34
|
export const FONT_SIZE = "13px";
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The colour for a `warning` — the product's destructive token, resolved.
|
|
38
|
+
*
|
|
39
|
+
* This is `--destructive` from the app's `globals.css` `:root`
|
|
40
|
+
* (`oklch(0.577 0.245 27.325)`) converted to sRGB hex. It is COPIED, not
|
|
41
|
+
* imported, and it has to be: contracts sits ABOVE the app in the dependency
|
|
42
|
+
* flow, so this package cannot read the app's stylesheet — and email cannot read
|
|
43
|
+
* one either. There is no CSS to cascade from and no custom properties to
|
|
44
|
+
* resolve, so every colour in this channel is a literal in an inline `style`.
|
|
45
|
+
* If the token moves, this does not follow it; that divergence is the cost of
|
|
46
|
+
* the arrow pointing one way.
|
|
47
|
+
*
|
|
48
|
+
* The `:root` value rather than `.dark`'s, because this channel is light-only by
|
|
49
|
+
* construction — `./shells.ts` states `color: #1a1a1a` on the body.
|
|
50
|
+
*/
|
|
51
|
+
export const DESTRUCTIVE = "#e7000b";
|
|
52
|
+
|
|
36
53
|
/**
|
|
37
54
|
* The gap AFTER a rendered line: `"normal"` is a blank line in plain text,
|
|
38
55
|
* anything else is none.
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
* - Pure. `render` is a function of `(content, context)` — no clock, no
|
|
12
12
|
* environment, no I/O. The copyright year comes from
|
|
13
13
|
* `context.brand.copyrightYear`.
|
|
14
|
-
* - Output is
|
|
15
|
-
* the whole constraint of
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* - Output is locked by `../../__tests__/render-snapshot.test.ts`. Byte-identity
|
|
15
|
+
* with the old `src/email/render` was the whole constraint of the migration
|
|
16
|
+
* and it held; ADR-CONTRACTS-087 discharged that proof once it shipped, so
|
|
17
|
+
* this channel's markup is now free to change — reviewably, via the snapshot.
|
|
18
18
|
* - `supports` answers true for every element type: email is the rich channel,
|
|
19
19
|
* and there is nothing in the content model it cannot depict.
|
|
20
20
|
*/
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* module offered templates a component per intent (`greeting()`, `keyValue()`,
|
|
6
6
|
* `footer()`) and each template chose which to call; here the CONTENT names the
|
|
7
7
|
* intent and this module is the only thing that knows the markup. The strings
|
|
8
|
-
*
|
|
8
|
+
* started as the same strings, byte-for-byte; they are free to diverge now
|
|
9
|
+
* (ADR-CONTRACTS-087) and `../../__tests__/render-snapshot.test.ts` is what
|
|
10
|
+
* makes each divergence a reviewed one.
|
|
9
11
|
*
|
|
10
12
|
* **Spacing is derived, because the content model does not carry it.** The old
|
|
11
13
|
* layer passed a `Spacing` per block, and `../../kinds` deliberately dropped it:
|
|
@@ -45,6 +47,7 @@ import type { RenderContext } from "../../context";
|
|
|
45
47
|
|
|
46
48
|
import {
|
|
47
49
|
COMPANY_URL,
|
|
50
|
+
DESTRUCTIVE,
|
|
48
51
|
FONT_SIZE,
|
|
49
52
|
MONO,
|
|
50
53
|
SPACING,
|
|
@@ -151,8 +154,18 @@ export function renderElement(
|
|
|
151
154
|
case "warning": {
|
|
152
155
|
// Fieldless by design — the banner IS the content, and its wording is the
|
|
153
156
|
// renderer's (see `../../content.ts`).
|
|
157
|
+
//
|
|
158
|
+
// The colour is this channel's answer to "this is a security notice", and
|
|
159
|
+
// only the HTML surface can give it: plain text has no colour, so there
|
|
160
|
+
// the banner's own glyphs carry the whole signal.
|
|
154
161
|
const text = "🆆🅰🆁🅽🅸🅽🅶";
|
|
155
|
-
return [
|
|
162
|
+
return [
|
|
163
|
+
paragraph(
|
|
164
|
+
`<span style="color: ${DESTRUCTIVE};">${escapeHtml(text)}</span>`,
|
|
165
|
+
text,
|
|
166
|
+
trailing,
|
|
167
|
+
),
|
|
168
|
+
];
|
|
156
169
|
}
|
|
157
170
|
|
|
158
171
|
case "notice":
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* INVARIANTS:
|
|
10
10
|
* - Pure functions of their input lines. No clock, no environment, no I/O.
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
11
|
+
* - These strings are the email's actual markup, and
|
|
12
|
+
* `../../__tests__/render-snapshot.test.ts` asserts them
|
|
13
|
+
* character-for-character.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
import { MONO, type Spacing } from "./constants";
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* returns a tag, the seam ADR-CONTRACTS-086 draws is back where it started.
|
|
15
15
|
* - Pure. `formatExpiry` builds a `Date` from its ARGUMENT, which is not a clock
|
|
16
16
|
* read; nothing here may add one.
|
|
17
|
-
* - These strings are output.
|
|
18
|
-
*
|
|
17
|
+
* - These strings are output. `__tests__/render-snapshot.test.ts` locks them, so
|
|
18
|
+
* a wording change here changes a real sent email.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/** The standard "no action required" reassurance line. */
|