@pikku/core 0.12.95 → 0.12.96

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.
@@ -63,7 +63,7 @@ describe('composeStepProse', () => {
63
63
  input: { packageName: '@pikku/addon-todos' },
64
64
  actor: 'admin',
65
65
  }),
66
- 'Then the admin sees @pikku/addon-todos'
66
+ 'Then admin sees @pikku/addon-todos'
67
67
  )
68
68
  })
69
69
 
@@ -75,7 +75,121 @@ describe('composeStepProse', () => {
75
75
  input: { packageName: '@pikku/addon-todos' },
76
76
  actor: 'admin',
77
77
  }),
78
- 'Then the admin sees an addon in the gallery'
78
+ 'Then admin sees an addon in the gallery'
79
+ )
80
+ })
81
+ })
82
+
83
+ describe('composeStepProse with an actor role', () => {
84
+ test('the role renders as an apposition after the actor', () => {
85
+ assert.equal(
86
+ composeStepProse({
87
+ phase: 'given',
88
+ description: 'signs in',
89
+ actor: 'yasser',
90
+ actorRole: 'founder',
91
+ }),
92
+ 'Given yasser (the founder) signs in'
93
+ )
94
+ })
95
+
96
+ test('no role renders exactly as it did before', () => {
97
+ assert.equal(
98
+ composeStepProse({
99
+ phase: 'given',
100
+ description: 'signs in',
101
+ actor: 'yasser',
102
+ }),
103
+ 'Given yasser signs in'
104
+ )
105
+ })
106
+
107
+ test('a role without an actor has nothing to qualify, so it is dropped', () => {
108
+ assert.equal(
109
+ composeStepProse({
110
+ phase: 'then',
111
+ description: 'the receipt totals 4.50',
112
+ actorRole: 'founder',
113
+ }),
114
+ 'Then the receipt totals 4.50'
115
+ )
116
+ })
117
+
118
+ test('the role sits inside the padded sentence, not the keyword column', () => {
119
+ assert.equal(
120
+ composeStepProse({
121
+ phase: 'when',
122
+ description: 'opens /app',
123
+ actor: 'nadia',
124
+ actorRole: 'head of ops',
125
+ keywordWidth: 5,
126
+ }),
127
+ 'When nadia (the head of ops) opens /app'
128
+ )
129
+ })
130
+ })
131
+
132
+ describe('composeStepProse continuations', () => {
133
+ test('a repeated phase reads as And', () => {
134
+ assert.equal(
135
+ composeStepProse({
136
+ phase: 'given',
137
+ description: 'is invited',
138
+ actor: 'nadia',
139
+ continuesPhase: true,
140
+ }),
141
+ 'And nadia is invited'
142
+ )
143
+ })
144
+
145
+ test('the same actor continuing drops the repeated subject', () => {
146
+ assert.equal(
147
+ composeStepProse({
148
+ phase: 'when',
149
+ description: 'sees the audit log',
150
+ actor: 'yasser',
151
+ continuesPhase: true,
152
+ continuesActor: true,
153
+ }),
154
+ 'And sees the audit log'
155
+ )
156
+ })
157
+
158
+ test('a new actor keeps their name even under And', () => {
159
+ assert.equal(
160
+ composeStepProse({
161
+ phase: 'given',
162
+ description: 'is invited',
163
+ actor: 'nadia',
164
+ continuesPhase: true,
165
+ continuesActor: false,
166
+ }),
167
+ 'And nadia is invited'
168
+ )
169
+ })
170
+
171
+ test('the same actor across a phase change keeps the subject', () => {
172
+ assert.equal(
173
+ composeStepProse({
174
+ phase: 'when',
175
+ description: 'opens the dashboard',
176
+ actor: 'yasser',
177
+ continuesActor: true,
178
+ }),
179
+ 'When yasser opens the dashboard'
180
+ )
181
+ })
182
+
183
+ test('an introduction is never dropped by a continuation', () => {
184
+ assert.equal(
185
+ composeStepProse({
186
+ phase: 'given',
187
+ description: 'is invited',
188
+ actor: 'nadia',
189
+ actorRole: 'reviewer',
190
+ continuesPhase: true,
191
+ }),
192
+ 'And nadia (the reviewer) is invited'
79
193
  )
80
194
  })
81
195
  })
@@ -88,7 +202,7 @@ describe('composeStepProse basics', () => {
88
202
  description: 'buys an apple',
89
203
  actor: 'shopper',
90
204
  }),
91
- 'Given the shopper buys an apple'
205
+ 'Given shopper buys an apple'
92
206
  )
93
207
  assert.equal(
94
208
  composeStepProse({
@@ -96,7 +210,18 @@ describe('composeStepProse basics', () => {
96
210
  description: 'sees a receipt',
97
211
  actor: 'shopper',
98
212
  }),
99
- 'Then the shopper sees a receipt'
213
+ 'Then shopper sees a receipt'
214
+ )
215
+ })
216
+
217
+ test('a persona named after a person reads as that person', () => {
218
+ assert.equal(
219
+ composeStepProse({
220
+ phase: 'when',
221
+ description: 'opens /app',
222
+ actor: 'nadia',
223
+ }),
224
+ 'When nadia opens /app'
100
225
  )
101
226
  })
102
227
 
@@ -117,7 +242,7 @@ describe('composeStepProse basics', () => {
117
242
  description: 'refreshes the dashboard',
118
243
  actor: 'admin',
119
244
  }),
120
- 'When the admin refreshes the dashboard'
245
+ 'When admin refreshes the dashboard'
121
246
  )
122
247
  })
123
248
 
@@ -133,11 +258,11 @@ describe('composeStepProse basics', () => {
133
258
  )
134
259
 
135
260
  assert.deepEqual(rendered, [
136
- 'Given the shopper buys an apple',
137
- 'When the shopper checks out',
138
- 'Then the shopper sees a receipt',
261
+ 'Given shopper buys an apple',
262
+ 'When shopper checks out',
263
+ 'Then shopper sees a receipt',
139
264
  ])
140
- const columns = new Set(rendered.map((line) => line.indexOf('the shopper')))
265
+ const columns = new Set(rendered.map((line) => line.indexOf('shopper')))
141
266
  assert.equal(columns.size, 1, 'every sentence starts in the same column')
142
267
  })
143
268
 
@@ -28,6 +28,9 @@ export const composeStepProse = ({
28
28
  template,
29
29
  input,
30
30
  actor,
31
+ actorRole,
32
+ continuesPhase,
33
+ continuesActor,
31
34
  keywordWidth,
32
35
  }: {
33
36
  phase: ScenarioStepPhase
@@ -35,10 +38,37 @@ export const composeStepProse = ({
35
38
  template?: string
36
39
  input?: unknown
37
40
  actor?: string
41
+ /**
42
+ * What this actor is, rendered as an apposition after their key — "yasser
43
+ * (the founder)". Only pass it where the actor has not been named yet: an
44
+ * ordinary run repeats one actor for a dozen steps, and repeating the role
45
+ * with them turns the one piece of context into the noise around it.
46
+ */
47
+ actorRole?: string
48
+ /**
49
+ * This step repeats the phase of the one before it, so it reads as `And`
50
+ * rather than saying `Given` three times — the same thing Gherkin does.
51
+ */
52
+ continuesPhase?: boolean
53
+ /**
54
+ * The step before this one had the same actor. Combined with `continuesPhase`
55
+ * the subject is dropped, because English drops a repeated subject in a
56
+ * compound predicate: "yasser opens the dashboard / and sees the audit log".
57
+ *
58
+ * It takes both. Dropping the subject across a phase change gives "When opens
59
+ * the dashboard", and a pronoun instead of a name would give "they sees",
60
+ * since step templates are authored in the third person singular.
61
+ */
62
+ continuesActor?: boolean
38
63
  keywordWidth?: number
39
64
  }): string => {
40
- const keyword = capitalise(phase)
41
- const subject = actor ? `the ${actor}` : ''
65
+ const keyword = capitalise(continuesPhase ? 'and' : phase)
66
+ // The actor key is the subject verbatim, with no article in front of it.
67
+ // "the ${actor}" only reads as English when the key happens to be a role
68
+ // noun — it turns a persona named after a person into "the nadia", which
69
+ // is the reporter quietly imposing a naming convention on the author.
70
+ const subject =
71
+ continuesPhase && continuesActor ? '' : composeSubject(actor, actorRole)
42
72
  const rendered = template ? renderStepTemplate(template, input) : description
43
73
  const sentence = [subject, rendered].filter(Boolean).join(' ')
44
74
  if (keywordWidth === undefined) {
@@ -47,5 +77,10 @@ export const composeStepProse = ({
47
77
  return `${keyword.padEnd(keywordWidth)} ${sentence}`
48
78
  }
49
79
 
80
+ const composeSubject = (actor?: string, actorRole?: string): string => {
81
+ if (!actor) return ''
82
+ return actorRole ? `${actor} (the ${actorRole})` : actor
83
+ }
84
+
50
85
  const capitalise = (value: string) =>
51
86
  value.charAt(0).toUpperCase() + value.slice(1)
@@ -38,6 +38,13 @@ export interface ScenarioArtifact {
38
38
  /** One step of a run, already joined to the prose that declared it. */
39
39
  export interface ScenarioStepRow {
40
40
  sentence: string
41
+ /**
42
+ * The same sentence with the actor's role in it — "yasser (the founder)
43
+ * signs in". Set only on the step that first names each actor, and only
44
+ * when a persona declares a job title or a role, so a reader who wants the
45
+ * context picks this and one who wants the bare run picks `sentence`.
46
+ */
47
+ sentenceWithRole?: string
41
48
  status: string
42
49
  durationMs?: number
43
50
  error?: string