create-foldkit-app 0.32.1 → 0.33.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.
Files changed (117) hide show
  1. package/dist/templates/base/.oxfmtrc.json +28 -0
  2. package/dist/templates/base/.oxlintrc.json +4 -0
  3. package/dist/templates/base/FOLDKIT.md +1 -1
  4. package/dist/templates/base/package.json +1 -1
  5. package/dist/templates/examples/api-cache/package.json +0 -2
  6. package/dist/templates/examples/api-cache/src/scene.test.ts +1 -2
  7. package/dist/templates/examples/api-cache/src/story.test.ts +3 -4
  8. package/dist/templates/examples/auth/package.json +0 -2
  9. package/dist/templates/examples/canvas-art/package.json +0 -2
  10. package/dist/templates/examples/charting/package.json +0 -2
  11. package/dist/templates/examples/charting/src/scene.test.ts +1 -2
  12. package/dist/templates/examples/charting/src/story.test.ts +3 -4
  13. package/dist/templates/examples/counter/package.json +0 -2
  14. package/dist/templates/examples/counters/package.json +0 -2
  15. package/dist/templates/examples/crash-view/package.json +0 -2
  16. package/dist/templates/examples/embedding/package.json +0 -2
  17. package/dist/templates/examples/form/package.json +0 -2
  18. package/dist/templates/examples/generative-art/package.json +0 -2
  19. package/dist/templates/examples/interrupting-commands/package.json +0 -2
  20. package/dist/templates/examples/job-application/package.json +0 -2
  21. package/dist/templates/examples/job-application/src/main.fixture.ts +69 -0
  22. package/dist/templates/examples/job-application/src/main.ts +1 -1
  23. package/dist/templates/examples/job-application/src/scene.test.ts +6 -66
  24. package/dist/templates/examples/job-application/src/step/attachments/index.ts +2 -0
  25. package/dist/templates/examples/job-application/src/step/attachments/scene.test.ts +19 -0
  26. package/dist/templates/examples/job-application/src/step/attachments/story.test.ts +46 -0
  27. package/dist/templates/examples/job-application/src/{view/attachments.ts → step/attachments/view.ts} +4 -11
  28. package/dist/templates/examples/job-application/src/{view → step/coverLetter}/coverLetter.ts +36 -7
  29. package/dist/templates/examples/job-application/src/step/coverLetter/index.ts +1 -0
  30. package/dist/templates/examples/job-application/src/step/coverLetter/scene.test.ts +24 -0
  31. package/dist/templates/examples/job-application/src/step/coverLetter/story.test.ts +19 -0
  32. package/dist/templates/examples/job-application/src/step/education/{entry.ts → entry/entry.ts} +2 -2
  33. package/dist/templates/examples/job-application/src/step/education/entry/index.ts +11 -0
  34. package/dist/templates/examples/job-application/src/step/education/entry/story.test.ts +39 -0
  35. package/dist/templates/examples/job-application/src/step/education/entry/view.ts +160 -0
  36. package/dist/templates/examples/job-application/src/step/education/index.ts +11 -1
  37. package/dist/templates/examples/job-application/src/step/education/scene.test.ts +20 -0
  38. package/dist/templates/examples/job-application/src/step/education/story.test.ts +87 -0
  39. package/dist/templates/examples/job-application/src/{view/education.ts → step/education/view.ts} +6 -10
  40. package/dist/templates/examples/job-application/src/step/personalInfo/index.ts +11 -0
  41. package/dist/templates/examples/job-application/src/step/{personalInfo.ts → personalInfo/personalInfo.ts} +2 -2
  42. package/dist/templates/examples/job-application/src/step/personalInfo/scene.test.ts +38 -0
  43. package/dist/templates/examples/job-application/src/step/personalInfo/story.test.ts +87 -0
  44. package/dist/templates/examples/job-application/src/{view/personalInfo.ts → step/personalInfo/view.ts} +24 -34
  45. package/dist/templates/examples/job-application/src/step/skills/{entry.ts → entry/entry.ts} +2 -2
  46. package/dist/templates/examples/job-application/src/step/skills/entry/index.ts +11 -0
  47. package/dist/templates/examples/job-application/src/step/skills/entry/story.test.ts +27 -0
  48. package/dist/templates/examples/job-application/src/{view/skillEntry.ts → step/skills/entry/view.ts} +9 -12
  49. package/dist/templates/examples/job-application/src/step/skills/index.ts +11 -1
  50. package/dist/templates/examples/job-application/src/step/skills/scene.test.ts +16 -0
  51. package/dist/templates/examples/job-application/src/step/skills/story.test.ts +85 -0
  52. package/dist/templates/examples/job-application/src/step/skills/view.ts +47 -0
  53. package/dist/templates/examples/job-application/src/step/workHistory/{entry.ts → entry/entry.ts} +1 -1
  54. package/dist/templates/examples/job-application/src/step/workHistory/entry/index.ts +11 -0
  55. package/dist/templates/examples/job-application/src/step/workHistory/entry/story.test.ts +42 -0
  56. package/dist/templates/examples/job-application/src/{view/workEntry.ts → step/workHistory/entry/view.ts} +27 -42
  57. package/dist/templates/examples/job-application/src/step/workHistory/index.ts +11 -1
  58. package/dist/templates/examples/job-application/src/step/workHistory/scene.test.ts +20 -0
  59. package/dist/templates/examples/job-application/src/step/workHistory/story.test.ts +87 -0
  60. package/dist/templates/examples/job-application/src/{view/workHistory.ts → step/workHistory/view.ts} +4 -8
  61. package/dist/templates/examples/job-application/src/story.test.ts +28 -419
  62. package/dist/templates/examples/job-application/src/view/datePicker.ts +4 -4
  63. package/dist/templates/examples/job-application/src/view/field.ts +3 -3
  64. package/dist/templates/examples/job-application/src/view/index.ts +3 -358
  65. package/dist/templates/examples/job-application/src/view/stepNav.ts +2 -2
  66. package/dist/templates/examples/job-application/src/view/view.ts +359 -0
  67. package/dist/templates/examples/kanban/package.json +0 -2
  68. package/dist/templates/examples/kanban/src/scene.test.ts +1 -1
  69. package/dist/templates/examples/kanban/src/story.test.ts +22 -23
  70. package/dist/templates/examples/kanban/src/view/index.ts +1 -71
  71. package/dist/templates/examples/kanban/src/view/view.ts +71 -0
  72. package/dist/templates/examples/managed-resource-layer/package.json +0 -2
  73. package/dist/templates/examples/map/package.json +0 -2
  74. package/dist/templates/examples/pixel-art/package.json +0 -2
  75. package/dist/templates/examples/pixel-art/src/scene.test.ts +16 -9
  76. package/dist/templates/examples/pixel-art/src/story.test.ts +12 -7
  77. package/dist/templates/examples/query-sync/package.json +0 -2
  78. package/dist/templates/examples/query-sync/src/story.test.ts +4 -5
  79. package/dist/templates/examples/route-transitions/package.json +0 -2
  80. package/dist/templates/examples/routing/package.json +0 -2
  81. package/dist/templates/examples/routing/src/page/people.scene.test.ts +41 -0
  82. package/dist/templates/examples/routing/src/page/people.story.test.ts +73 -0
  83. package/dist/templates/examples/routing/src/scene.test.ts +2 -17
  84. package/dist/templates/examples/routing/src/story.test.ts +4 -34
  85. package/dist/templates/examples/shopping-cart/package.json +0 -2
  86. package/dist/templates/examples/shopping-cart/src/page/products.scene.test.ts +20 -0
  87. package/dist/templates/examples/shopping-cart/src/page/products.story.test.ts +56 -0
  88. package/dist/templates/examples/shopping-cart/src/page/products.ts +1 -1
  89. package/dist/templates/examples/shopping-cart/src/scene.test.ts +1 -4
  90. package/dist/templates/examples/shopping-cart/src/story.test.ts +3 -4
  91. package/dist/templates/examples/slow-warnings/package.json +0 -2
  92. package/dist/templates/examples/snake/package.json +0 -2
  93. package/dist/templates/examples/ssg/package.json +0 -2
  94. package/dist/templates/examples/ssr/package.json +0 -2
  95. package/dist/templates/examples/state-machine/package.json +0 -2
  96. package/dist/templates/examples/state-machine/src/main.ts +79 -87
  97. package/dist/templates/examples/state-machine/src/scene.test.ts +1 -2
  98. package/dist/templates/examples/stopwatch/package.json +0 -2
  99. package/dist/templates/examples/todo/package.json +0 -2
  100. package/dist/templates/examples/ui-showcase/package.json +0 -2
  101. package/dist/templates/examples/ui-showcase/src/story.test.ts +1 -2
  102. package/dist/templates/examples/view-transitions/package.json +0 -2
  103. package/dist/templates/examples/view-transitions/src/styles.css +3 -3
  104. package/dist/templates/examples/weather/package.json +0 -2
  105. package/dist/templates/examples/web-components/package.json +0 -2
  106. package/dist/templates/examples/websocket-chat/package.json +0 -2
  107. package/dist/templates/release.json +9 -10
  108. package/dist/templates/rendering/ssg/package.json +1 -1
  109. package/dist/templates/rendering/ssr/package.json +1 -1
  110. package/dist/utils/packages.js +1 -2
  111. package/package.json +3 -3
  112. package/dist/templates/base/.prettierignore +0 -5
  113. package/dist/templates/base/.prettierrc +0 -13
  114. package/dist/templates/examples/job-application/src/step/coverLetter.ts +0 -34
  115. package/dist/templates/examples/job-application/src/view/educationEntry.ts +0 -165
  116. package/dist/templates/examples/job-application/src/view/skills.ts +0 -49
  117. /package/dist/templates/examples/job-application/src/step/{attachments.ts → attachments/attachments.ts} +0 -0
@@ -0,0 +1,87 @@
1
+ import { Array, Option, pipe } from 'effect'
2
+ import { Calendar } from 'foldkit'
3
+ import { Command, given, message, model, story } from 'foldkit/story'
4
+ import { describe, expect, test } from 'vitest'
5
+
6
+ import * as Entry from './entry'
7
+ import { GenerateEntryId, Message, init, update } from './workHistory'
8
+
9
+ const today = Calendar.make(2026, 4, 16)
10
+ const givenInitial = given(init(today, 'work-history-entry-1'))
11
+
12
+ describe('workHistory', () => {
13
+ test('ClickedAddEntry generates an id and appends an entry', () => {
14
+ story(
15
+ update,
16
+ givenInitial,
17
+ message(Message.ClickedAddEntry()),
18
+ Command.expectHas(GenerateEntryId),
19
+ Command.resolve(
20
+ GenerateEntryId,
21
+ Message.SucceededGenerateEntryId({ entryId: 'test-work-1' }),
22
+ ),
23
+ model(model => {
24
+ expect(model.entries).toHaveLength(2)
25
+ }),
26
+ )
27
+ })
28
+
29
+ test('FailedGenerateEntryId leaves the entries unchanged', () => {
30
+ story(
31
+ update,
32
+ givenInitial,
33
+ message(Message.FailedGenerateEntryId()),
34
+ Command.expectNone(),
35
+ model(model => {
36
+ expect(model).toEqual(init(today, 'work-history-entry-1'))
37
+ }),
38
+ )
39
+ })
40
+
41
+ test('an entry OutMessage.Removed removes that entry', () => {
42
+ const firstEntry = Option.getOrThrow(
43
+ Array.head(init(today, 'work-history-entry-1').entries),
44
+ )
45
+
46
+ story(
47
+ update,
48
+ givenInitial,
49
+ message(
50
+ Message.GotEntryMessage({
51
+ entryId: firstEntry.id,
52
+ message: Entry.Message.ClickedRemoveSelf(),
53
+ }),
54
+ ),
55
+ model(model => {
56
+ expect(model.entries).toHaveLength(0)
57
+ }),
58
+ )
59
+ })
60
+
61
+ test('GotEntryMessage folds a company update into the matching entry', () => {
62
+ const firstEntry = Option.getOrThrow(
63
+ Array.head(init(today, 'work-history-entry-1').entries),
64
+ )
65
+
66
+ story(
67
+ update,
68
+ givenInitial,
69
+ message(
70
+ Message.GotEntryMessage({
71
+ entryId: firstEntry.id,
72
+ message: Entry.Message.UpdatedCompany({ value: 'Foldkit Inc.' }),
73
+ }),
74
+ ),
75
+ model(model => {
76
+ expect(
77
+ pipe(
78
+ model.entries,
79
+ Array.head,
80
+ Option.map(entry => entry.company.value),
81
+ Option.getOrThrow,
82
+ ),
83
+ ).toBe('Foldkit Inc.')
84
+ }),
85
+ )
86
+ })
87
+ })
@@ -3,14 +3,10 @@ import type { Html } from 'foldkit/html'
3
3
 
4
4
  import { Button } from '@foldkit/ui'
5
5
 
6
- import { WorkHistory } from '../step'
7
- import { Message } from '../step/workHistory'
8
- import { workEntryView } from './workEntry'
6
+ import * as Entry from './entry'
7
+ import { Message, type Model } from './workHistory'
9
8
 
10
- export const workHistoryView = Submodel.defineView<
11
- WorkHistory.Model,
12
- WorkHistory.Message
13
- >((model, h): Html =>
9
+ export const view = Submodel.defineView<Model, Message>((model, h): Html =>
14
10
  h.div(
15
11
  [h.Class('space-y-6')],
16
12
  [
@@ -24,7 +20,7 @@ export const workHistoryView = Submodel.defineView<
24
20
  h.submodel({
25
21
  slotId: entry.id,
26
22
  model: entry,
27
- view: workEntryView,
23
+ view: Entry.view,
28
24
  toParentMessage: message =>
29
25
  Message.GotEntryMessage({
30
26
  entryId: entry.id,
@@ -1,17 +1,14 @@
1
1
  import { Array, Option, pipe } from 'effect'
2
- import { Calendar } from 'foldkit'
3
- import { Valid, Validating } from 'foldkit/fieldValidation'
2
+ import { Validating } from 'foldkit/fieldValidation'
4
3
  import { Command, given, message, model, story } from 'foldkit/story'
5
4
  import { describe, expect, test } from 'vitest'
6
5
 
7
- import { Menu, Tabs } from '@foldkit/ui'
8
- import { Message as FileDropMessage } from '@foldkit/ui/fileDrop'
9
- import { Message as MenuMessage } from '@foldkit/ui/menu'
10
- import { Message as TabsMessage } from '@foldkit/ui/tabs'
6
+ import { FileDrop, Menu, Tabs } from '@foldkit/ui'
11
7
 
12
8
  import { SubmitApplication } from './command'
9
+ import { completeModel, initialModel } from './main.fixture'
13
10
  import { Message } from './message'
14
- import { type Model, Submission } from './model'
11
+ import { Submission } from './model'
15
12
  import {
16
13
  Attachments,
17
14
  CoverLetter,
@@ -20,75 +17,18 @@ import {
20
17
  Skills,
21
18
  WorkHistory,
22
19
  } from './step'
23
- import { Message as AttachmentsMessage } from './step/attachments'
24
- import { Message as CoverLetterMessage } from './step/coverLetter'
25
- import { Message as EducationMessage } from './step/education'
26
- import { Message as PersonalInfoMessage } from './step/personalInfo'
27
- import { Message as SkillsMessage } from './step/skills'
28
- import { Message as WorkHistoryMessage } from './step/workHistory'
29
20
  import { update } from './update'
30
21
 
31
- const today = Calendar.make(2026, 4, 16)
32
-
33
- const initialModel: Model = {
34
- currentStep: 'PersonalInfo',
35
- personalInfo: PersonalInfo.init(today),
36
- workHistory: WorkHistory.init(today, 'work-history-entry-1'),
37
- education: Education.init(today, 'education-entry-1'),
38
- skills: Skills.init('skills-entry-1'),
39
- coverLetter: CoverLetter.init(),
40
- attachments: Attachments.init(),
41
- isPreviewVisible: false,
42
- submission: Submission.NotSubmitted(),
43
- stepMenu: Menu.init({ id: 'step-menu' }),
44
- stepTabs: Tabs.init({ id: 'step-tabs' }),
45
- isSubmitAttempted: false,
46
- }
47
-
48
- const completeModel: Model = {
49
- ...initialModel,
50
- personalInfo: {
51
- ...initialModel.personalInfo,
52
- firstName: Valid({ value: 'Jane' }),
53
- lastName: Valid({ value: 'Doe' }),
54
- email: Valid({ value: 'jane@example.com' }),
55
- },
56
- workHistory: {
57
- ...initialModel.workHistory,
58
- entries: initialModel.workHistory.entries.map(entry => ({
59
- ...entry,
60
- company: Valid({ value: 'Foldkit' }),
61
- title: Valid({ value: 'Engineer' }),
62
- })),
63
- },
64
- education: {
65
- ...initialModel.education,
66
- entries: initialModel.education.entries.map(entry => ({
67
- ...entry,
68
- school: Valid({ value: 'MIT' }),
69
- degree: Valid({ value: 'BS' }),
70
- fieldOfStudy: Valid({ value: 'CS' }),
71
- })),
72
- },
73
- skills: {
74
- ...initialModel.skills,
75
- entries: initialModel.skills.entries.map(entry => ({
76
- ...entry,
77
- name: Valid({ value: 'TypeScript' }),
78
- })),
79
- },
80
- }
81
-
82
22
  const givenInitial = given(initialModel)
83
23
 
84
24
  const resolveFocusTab = Command.resolve(
85
25
  Tabs.FocusTab,
86
- TabsMessage.CompletedFocusTab(),
26
+ Tabs.Message.CompletedFocusTab(),
87
27
  )
88
28
 
89
29
  const resolveFocusMenuButton = Command.resolve(
90
30
  Menu.FocusButton,
91
- MenuMessage.CompletedFocusButton(),
31
+ Menu.Message.CompletedFocusButton(),
92
32
  )
93
33
 
94
34
  describe('update', () => {
@@ -155,7 +95,7 @@ describe('update', () => {
155
95
  givenInitial,
156
96
  message(
157
97
  Message.GotStepTabsMessage({
158
- message: TabsMessage.SelectedTab({ index: 6, value: 'Review' }),
98
+ message: Tabs.Message.SelectedTab({ index: 6, value: 'Review' }),
159
99
  }),
160
100
  ),
161
101
  model(model => {
@@ -171,15 +111,15 @@ describe('update', () => {
171
111
  givenInitial,
172
112
  message(
173
113
  Message.GotStepMenuMessage({
174
- message: MenuMessage.Opened({
114
+ message: Menu.Message.Opened({
175
115
  maybeActiveItemIndex: Option.none(),
176
116
  }),
177
117
  }),
178
118
  ),
179
- Command.resolve(Menu.FocusItems, MenuMessage.CompletedFocusItems()),
119
+ Command.resolve(Menu.FocusItems, Menu.Message.CompletedFocusItems()),
180
120
  message(
181
121
  Message.GotStepMenuMessage({
182
- message: MenuMessage.SelectedItem({
122
+ message: Menu.Message.SelectedItem({
183
123
  index: 5,
184
124
  item: 'Attachments',
185
125
  }),
@@ -210,390 +150,80 @@ describe('update', () => {
210
150
  })
211
151
  })
212
152
 
213
- describe('personal info delegation', () => {
214
- test('first name update is delegated to personal info step', () => {
153
+ describe('child folding', () => {
154
+ test('GotPersonalInfoMessage writes through to personalInfo', () => {
215
155
  story(
216
156
  update,
217
157
  givenInitial,
218
158
  message(
219
159
  Message.GotPersonalInfoMessage({
220
- message: PersonalInfoMessage.UpdatedFirstName({ value: 'Jane' }),
160
+ message: PersonalInfo.Message.UpdatedFirstName({ value: 'Jane' }),
221
161
  }),
222
162
  ),
223
163
  model(model => {
224
164
  expect(model.personalInfo.firstName.value).toBe('Jane')
225
- expect(model.personalInfo.firstName._tag).toBe('Valid')
226
165
  }),
227
166
  )
228
167
  })
229
168
 
230
- test('short first name produces validation error', () => {
231
- story(
232
- update,
233
- givenInitial,
234
- message(
235
- Message.GotPersonalInfoMessage({
236
- message: PersonalInfoMessage.UpdatedFirstName({ value: 'J' }),
237
- }),
238
- ),
239
- model(model => {
240
- expect(model.personalInfo.firstName._tag).toBe('Invalid')
241
- }),
242
- )
243
- })
244
-
245
- test('email triggers async validation after passing sync checks', () => {
246
- story(
247
- update,
248
- givenInitial,
249
- message(
250
- Message.GotPersonalInfoMessage({
251
- message: PersonalInfoMessage.UpdatedEmail({
252
- value: 'jane@example.com',
253
- }),
254
- }),
255
- ),
256
- Command.expectHas(PersonalInfo.ValidateEmailAsync),
257
- Command.resolve(
258
- PersonalInfo.ValidateEmailAsync,
259
- PersonalInfoMessage.CompletedValidateEmailAsync({
260
- validationId: 1,
261
- field: Valid({ value: 'jane@example.com' }),
262
- }),
263
- ),
264
- model(model => {
265
- expect(model.personalInfo.email._tag).toBe('Valid')
266
- }),
267
- )
268
- })
269
-
270
- test('malformed email fails sync validation without async command', () => {
271
- story(
272
- update,
273
- givenInitial,
274
- message(
275
- Message.GotPersonalInfoMessage({
276
- message: PersonalInfoMessage.UpdatedEmail({ value: 'not-email' }),
277
- }),
278
- ),
279
- Command.expectNone(),
280
- model(model => {
281
- expect(model.personalInfo.email._tag).toBe('Invalid')
282
- }),
283
- )
284
- })
285
-
286
- test('stale email async result is discarded', () => {
287
- const modelWithInFlightValidation: Model = {
288
- ...initialModel,
289
- personalInfo: {
290
- ...initialModel.personalInfo,
291
- email: Validating({ value: 'jane@example.com' }),
292
- emailValidationId: 5,
293
- },
294
- }
295
-
296
- story(
297
- update,
298
- given(modelWithInFlightValidation),
299
- message(
300
- Message.GotPersonalInfoMessage({
301
- message: PersonalInfoMessage.CompletedValidateEmailAsync({
302
- validationId: 3,
303
- field: Valid({ value: 'old@example.com' }),
304
- }),
305
- }),
306
- ),
307
- model(model => {
308
- expect(model.personalInfo.email._tag).toBe('Validating')
309
- expect(model.personalInfo.email.value).toBe('jane@example.com')
310
- expect(model.personalInfo.emailValidationId).toBe(5)
311
- }),
312
- )
313
- })
314
- })
315
-
316
- describe('work history delegation', () => {
317
- test('adds a new work entry', () => {
169
+ test('GotWorkHistoryMessage writes through to workHistory', () => {
318
170
  story(
319
171
  update,
320
172
  givenInitial,
321
173
  message(
322
174
  Message.GotWorkHistoryMessage({
323
- message: WorkHistoryMessage.SucceededGenerateEntryId({
175
+ message: WorkHistory.Message.SucceededGenerateEntryId({
324
176
  entryId: 'test-work-1',
325
177
  }),
326
178
  }),
327
179
  ),
328
180
  model(model => {
329
- expect(model.workHistory.entries.length).toBe(2)
330
- }),
331
- )
332
- })
333
-
334
- test('keeps work history unchanged when entry ID generation fails', () => {
335
- story(
336
- update,
337
- givenInitial,
338
- message(
339
- Message.GotWorkHistoryMessage({
340
- message: WorkHistoryMessage.FailedGenerateEntryId(),
341
- }),
342
- ),
343
- Command.expectNone(),
344
- model(model => {
345
- expect(model.workHistory).toEqual(initialModel.workHistory)
346
- }),
347
- )
348
- })
349
-
350
- test('removes a work entry', () => {
351
- const firstEntry = Option.getOrThrow(
352
- Array.head(initialModel.workHistory.entries),
353
- )
354
-
355
- story(
356
- update,
357
- givenInitial,
358
- message(
359
- Message.GotWorkHistoryMessage({
360
- message: WorkHistoryMessage.RemovedEntry({
361
- entryId: firstEntry.id,
362
- }),
363
- }),
364
- ),
365
- model(model => {
366
- expect(model.workHistory.entries.length).toBe(0)
367
- }),
368
- )
369
- })
370
-
371
- test('updates company in a work entry', () => {
372
- const firstEntry = Option.getOrThrow(
373
- Array.head(initialModel.workHistory.entries),
374
- )
375
-
376
- story(
377
- update,
378
- givenInitial,
379
- message(
380
- Message.GotWorkHistoryMessage({
381
- message: WorkHistoryMessage.GotEntryMessage({
382
- entryId: firstEntry.id,
383
- message: WorkHistory.Entry.Message.UpdatedCompany({
384
- value: 'Foldkit Inc.',
385
- }),
386
- }),
387
- }),
388
- ),
389
- model(model => {
390
- expect(
391
- pipe(
392
- model.workHistory.entries,
393
- Array.head,
394
- Option.map(entry => entry.company.value),
395
- Option.getOrThrow,
396
- ),
397
- ).toBe('Foldkit Inc.')
181
+ expect(model.workHistory.entries).toHaveLength(2)
398
182
  }),
399
183
  )
400
184
  })
401
- })
402
185
 
403
- describe('education delegation', () => {
404
- test('adds a new education entry', () => {
186
+ test('GotEducationMessage writes through to education', () => {
405
187
  story(
406
188
  update,
407
189
  givenInitial,
408
190
  message(
409
191
  Message.GotEducationMessage({
410
- message: EducationMessage.SucceededGenerateEntryId({
192
+ message: Education.Message.SucceededGenerateEntryId({
411
193
  entryId: 'test-edu-1',
412
194
  }),
413
195
  }),
414
196
  ),
415
197
  model(model => {
416
- expect(model.education.entries.length).toBe(2)
198
+ expect(model.education.entries).toHaveLength(2)
417
199
  }),
418
200
  )
419
201
  })
420
202
 
421
- test('keeps education unchanged when entry ID generation fails', () => {
422
- story(
423
- update,
424
- givenInitial,
425
- message(
426
- Message.GotEducationMessage({
427
- message: EducationMessage.FailedGenerateEntryId(),
428
- }),
429
- ),
430
- Command.expectNone(),
431
- model(model => {
432
- expect(model.education).toEqual(initialModel.education)
433
- }),
434
- )
435
- })
436
-
437
- test('removes an education entry', () => {
438
- const firstEntry = Option.getOrThrow(
439
- Array.head(initialModel.education.entries),
440
- )
441
-
442
- story(
443
- update,
444
- givenInitial,
445
- message(
446
- Message.GotEducationMessage({
447
- message: EducationMessage.RemovedEntry({ entryId: firstEntry.id }),
448
- }),
449
- ),
450
- model(model => {
451
- expect(model.education.entries.length).toBe(0)
452
- }),
453
- )
454
- })
455
-
456
- test('updates school in an education entry', () => {
457
- const firstEntry = Option.getOrThrow(
458
- Array.head(initialModel.education.entries),
459
- )
460
-
461
- story(
462
- update,
463
- givenInitial,
464
- message(
465
- Message.GotEducationMessage({
466
- message: EducationMessage.GotEntryMessage({
467
- entryId: firstEntry.id,
468
- message: Education.Entry.Message.UpdatedSchool({ value: 'MIT' }),
469
- }),
470
- }),
471
- ),
472
- model(model => {
473
- expect(
474
- pipe(
475
- model.education.entries,
476
- Array.head,
477
- Option.map(entry => entry.school.value),
478
- Option.getOrThrow,
479
- ),
480
- ).toBe('MIT')
481
- }),
482
- )
483
- })
484
-
485
- test('empty school is Invalid with required message', () => {
486
- const firstEntry = Option.getOrThrow(
487
- Array.head(initialModel.education.entries),
488
- )
489
-
490
- story(
491
- update,
492
- givenInitial,
493
- message(
494
- Message.GotEducationMessage({
495
- message: EducationMessage.GotEntryMessage({
496
- entryId: firstEntry.id,
497
- message: Education.Entry.Message.UpdatedSchool({ value: 'MIT' }),
498
- }),
499
- }),
500
- ),
501
- message(
502
- Message.GotEducationMessage({
503
- message: EducationMessage.GotEntryMessage({
504
- entryId: firstEntry.id,
505
- message: Education.Entry.Message.UpdatedSchool({ value: '' }),
506
- }),
507
- }),
508
- ),
509
- model(model => {
510
- expect(
511
- pipe(
512
- model.education.entries,
513
- Array.head,
514
- Option.map(entry => entry.school._tag),
515
- Option.getOrThrow,
516
- ),
517
- ).toBe('Invalid')
518
- }),
519
- )
520
- })
521
- })
522
-
523
- describe('skills delegation', () => {
524
- test('adds a new skill entry', () => {
203
+ test('GotSkillsMessage writes through to skills', () => {
525
204
  story(
526
205
  update,
527
206
  givenInitial,
528
207
  message(
529
208
  Message.GotSkillsMessage({
530
- message: SkillsMessage.SucceededGenerateEntryId({
209
+ message: Skills.Message.SucceededGenerateEntryId({
531
210
  entryId: 'test-skill-1',
532
211
  }),
533
212
  }),
534
213
  ),
535
214
  model(model => {
536
- expect(model.skills.entries.length).toBe(2)
537
- }),
538
- )
539
- })
540
-
541
- test('keeps skills unchanged when entry ID generation fails', () => {
542
- story(
543
- update,
544
- givenInitial,
545
- message(
546
- Message.GotSkillsMessage({
547
- message: SkillsMessage.FailedGenerateEntryId(),
548
- }),
549
- ),
550
- Command.expectNone(),
551
- model(model => {
552
- expect(model.skills).toEqual(initialModel.skills)
553
- }),
554
- )
555
- })
556
-
557
- test('updates skill name', () => {
558
- const firstEntry = Option.getOrThrow(
559
- Array.head(initialModel.skills.entries),
560
- )
561
-
562
- story(
563
- update,
564
- givenInitial,
565
- message(
566
- Message.GotSkillsMessage({
567
- message: SkillsMessage.GotEntryMessage({
568
- entryId: firstEntry.id,
569
- message: Skills.Entry.Message.UpdatedName({
570
- value: 'TypeScript',
571
- }),
572
- }),
573
- }),
574
- ),
575
- model(model => {
576
- expect(
577
- pipe(
578
- model.skills.entries,
579
- Array.head,
580
- Option.map(entry => entry.name.value),
581
- Option.getOrThrow,
582
- ),
583
- ).toBe('TypeScript')
215
+ expect(model.skills.entries).toHaveLength(2)
584
216
  }),
585
217
  )
586
218
  })
587
- })
588
219
 
589
- describe('cover letter delegation', () => {
590
- test('updates content', () => {
220
+ test('GotCoverLetterMessage writes through to coverLetter', () => {
591
221
  story(
592
222
  update,
593
223
  givenInitial,
594
224
  message(
595
225
  Message.GotCoverLetterMessage({
596
- message: CoverLetterMessage.UpdatedContent({
226
+ message: CoverLetter.Message.UpdatedContent({
597
227
  value: 'I love the Elm Architecture.',
598
228
  }),
599
229
  }),
@@ -603,45 +233,24 @@ describe('update', () => {
603
233
  }),
604
234
  )
605
235
  })
606
- })
607
236
 
608
- describe('attachments delegation', () => {
609
- test('stores a dropped resume as the maybeResume File', () => {
237
+ test('GotAttachmentsMessage writes through to attachments', () => {
610
238
  const resume = new globalThis.File(['pdf-bytes'], 'resume.pdf', {
611
239
  type: 'application/pdf',
612
240
  })
613
- story(
614
- update,
615
- givenInitial,
616
- message(
617
- Message.GotAttachmentsMessage({
618
- message: AttachmentsMessage.GotResumeDropMessage({
619
- message: FileDropMessage.DroppedFiles({ files: [resume] }),
620
- }),
621
- }),
622
- ),
623
- model(model => {
624
- expect(model.attachments.maybeResume._tag).toBe('Some')
625
- }),
626
- )
627
- })
628
241
 
629
- test('appends dropped additional files to the list', () => {
630
- const file = new globalThis.File(['content'], 'portfolio.pdf', {
631
- type: 'application/pdf',
632
- })
633
242
  story(
634
243
  update,
635
244
  givenInitial,
636
245
  message(
637
246
  Message.GotAttachmentsMessage({
638
- message: AttachmentsMessage.GotAdditionalFilesDropMessage({
639
- message: FileDropMessage.DroppedFiles({ files: [file] }),
247
+ message: Attachments.Message.GotResumeDropMessage({
248
+ message: FileDrop.Message.DroppedFiles({ files: [resume] }),
640
249
  }),
641
250
  }),
642
251
  ),
643
252
  model(model => {
644
- expect(model.attachments.additionalFiles).toHaveLength(1)
253
+ expect(model.attachments.maybeResume._tag).toBe('Some')
645
254
  }),
646
255
  )
647
256
  })