@lowdefy/docs-content 0.0.0-experimental-20260928120643 → 6.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.
Files changed (226) hide show
  1. package/README.md +0 -1
  2. package/content/actions/link.md +0 -17
  3. package/content/actions/login.md +5 -23
  4. package/content/actions/logout.md +5 -5
  5. package/content/actions/pdfmake.md +2 -2
  6. package/content/actions/setdarkmode.md +2 -2
  7. package/content/agents/agent-properties.md +2 -4
  8. package/content/agents/agentchat.md +6 -21
  9. package/content/agents/agentconversations.md +2 -2
  10. package/content/concepts/ai-agent-docs.md +101 -313
  11. package/content/concepts/cli.md +3 -34
  12. package/content/concepts/connections-and-requests.md +0 -402
  13. package/content/concepts/custom-html.md +0 -40
  14. package/content/concepts/custom-styling.md +5 -50
  15. package/content/concepts/events-and-actions.md +2 -64
  16. package/content/concepts/i18n.md +2 -7
  17. package/content/concepts/keyboard-shortcuts.md +2 -2
  18. package/content/concepts/layout-overview.md +1 -1
  19. package/content/concepts/logger.md +0 -81
  20. package/content/concepts/lowdefy-schema.md +1 -4
  21. package/content/concepts/menus.md +7 -7
  22. package/content/concepts/module-authoring.md +2 -2
  23. package/content/connections/anthropic.md +2 -162
  24. package/content/connections/awss3.md +0 -54
  25. package/content/connections/google.md +2 -162
  26. package/content/connections/mongodb.md +3 -121
  27. package/content/connections/openai-connection.md +2 -162
  28. package/content/container-blocks/affix.md +7 -47
  29. package/content/container-blocks/alert.md +24 -76
  30. package/content/container-blocks/badge.md +24 -100
  31. package/content/container-blocks/box.md +9 -9
  32. package/content/container-blocks/card.md +25 -80
  33. package/content/container-blocks/carousel.md +7 -60
  34. package/content/container-blocks/collapse.md +9 -87
  35. package/content/container-blocks/configprovider.md +13 -35
  36. package/content/container-blocks/confirmmodal.md +23 -69
  37. package/content/container-blocks/content.md +98 -62
  38. package/content/container-blocks/descriptions.md +4 -45
  39. package/content/container-blocks/drawer.md +28 -92
  40. package/content/container-blocks/dropdownmenu.md +31 -119
  41. package/content/container-blocks/flex.md +62 -40
  42. package/content/container-blocks/footer.md +11 -15
  43. package/content/container-blocks/googlemapsheatmap.md +1 -1
  44. package/content/container-blocks/googlemapsscript.md +2 -2
  45. package/content/container-blocks/header.md +36 -71
  46. package/content/container-blocks/label.md +7 -34
  47. package/content/container-blocks/layout.md +34 -52
  48. package/content/container-blocks/masonry.md +71 -17
  49. package/content/container-blocks/mobilemenu.md +88 -131
  50. package/content/container-blocks/modal.md +26 -114
  51. package/content/container-blocks/pageheadermenu.md +31 -81
  52. package/content/container-blocks/pagesidebarlayout.md +26 -169
  53. package/content/container-blocks/pagesidermenu.md +42 -95
  54. package/content/container-blocks/popover.md +31 -34
  55. package/content/container-blocks/result.md +21 -32
  56. package/content/container-blocks/sider.md +122 -85
  57. package/content/container-blocks/span.md +7 -7
  58. package/content/container-blocks/spinner.md +3 -3
  59. package/content/container-blocks/splitter.md +68 -150
  60. package/content/container-blocks/tabs.md +12 -64
  61. package/content/container-blocks/tooltip.md +46 -22
  62. package/content/container-blocks/watermark.md +4 -22
  63. package/content/controls/reject.md +2 -4
  64. package/content/controls/throw.md +0 -1
  65. package/content/controls/try.md +2 -3
  66. package/content/deployment/deployment-environments.md +6 -8
  67. package/content/deployment/docker.md +7 -22
  68. package/content/deployment/node-server.md +0 -4
  69. package/content/deployment/vercel.md +6 -13
  70. package/content/display-blocks/aggrid.md +11 -26
  71. package/content/display-blocks/anchor.md +15 -15
  72. package/content/display-blocks/avatar.md +37 -57
  73. package/content/display-blocks/breadcrumb.md +21 -70
  74. package/content/display-blocks/button.md +18 -92
  75. package/content/display-blocks/calendar.md +0 -12
  76. package/content/display-blocks/dangeroushtml.md +1 -1
  77. package/content/display-blocks/difflist.md +1 -1
  78. package/content/display-blocks/difftimeline.md +1 -1
  79. package/content/display-blocks/divider.md +3 -64
  80. package/content/display-blocks/dropdownbutton.md +28 -79
  81. package/content/display-blocks/echart.md +1 -35
  82. package/content/display-blocks/floatbutton.md +42 -129
  83. package/content/display-blocks/html.md +1 -61
  84. package/content/display-blocks/icon.md +67 -250
  85. package/content/display-blocks/menu.md +73 -145
  86. package/content/display-blocks/message.md +124 -38
  87. package/content/display-blocks/notification.md +222 -94
  88. package/content/display-blocks/paragraph.md +9 -59
  89. package/content/display-blocks/progress.md +17 -96
  90. package/content/display-blocks/qrcode.md +4 -19
  91. package/content/display-blocks/s3download.md +1 -1
  92. package/content/display-blocks/search.md +7 -17
  93. package/content/display-blocks/skeletonbutton.md +0 -1
  94. package/content/display-blocks/skeletoninput.md +0 -1
  95. package/content/display-blocks/statistic.md +17 -75
  96. package/content/display-blocks/steps.md +25 -58
  97. package/content/display-blocks/tag.md +17 -102
  98. package/content/display-blocks/title.md +11 -50
  99. package/content/display-blocks/tour.md +23 -33
  100. package/content/input-blocks/autocomplete.md +9 -81
  101. package/content/input-blocks/buttonselector.md +8 -45
  102. package/content/input-blocks/checkboxselector.md +7 -97
  103. package/content/input-blocks/checkboxswitch.md +3 -33
  104. package/content/input-blocks/colorselector.md +14 -63
  105. package/content/input-blocks/daterangeselector.md +45 -165
  106. package/content/input-blocks/dateselector.md +41 -139
  107. package/content/input-blocks/datetimeselector.md +49 -158
  108. package/content/input-blocks/inputgallery.md +3 -3
  109. package/content/input-blocks/monthselector.md +52 -120
  110. package/content/input-blocks/multipleselector.md +39 -150
  111. package/content/input-blocks/numberinput.md +12 -98
  112. package/content/input-blocks/pagination.md +14 -70
  113. package/content/input-blocks/paragraphinput.md +37 -51
  114. package/content/input-blocks/passwordinput.md +6 -60
  115. package/content/input-blocks/phonenumberinput.md +26 -59
  116. package/content/input-blocks/radioselector.md +7 -83
  117. package/content/input-blocks/ratingslider.md +26 -57
  118. package/content/input-blocks/s3uploadbutton.md +24 -24
  119. package/content/input-blocks/s3uploaddragger.md +3 -3
  120. package/content/input-blocks/s3uploadphoto.md +5 -5
  121. package/content/input-blocks/segmentedselector.md +32 -72
  122. package/content/input-blocks/selector.md +21 -109
  123. package/content/input-blocks/switch.md +17 -65
  124. package/content/input-blocks/textarea.md +3 -32
  125. package/content/input-blocks/textinput.md +28 -69
  126. package/content/input-blocks/tiptapmentioninput.md +1 -1
  127. package/content/input-blocks/titleinput.md +12 -39
  128. package/content/input-blocks/treeinput.md +25 -49
  129. package/content/input-blocks/treemultipleselector.md +10 -127
  130. package/content/input-blocks/treeselector.md +9 -97
  131. package/content/input-blocks/upload.md +15 -64
  132. package/content/input-blocks/uploaddragger.md +1 -17
  133. package/content/input-blocks/uploadphoto.md +1 -18
  134. package/content/input-blocks/weekselector.md +55 -120
  135. package/content/list-blocks/controlledlist.md +15 -24
  136. package/content/list-blocks/list.md +11 -11
  137. package/content/list-blocks/masonrylist.md +34 -7
  138. package/content/migration/upgrade-guide.md +0 -1
  139. package/content/operators/_app.md +1 -2
  140. package/content/operators/_error.md +1 -23
  141. package/content/operators/_get.md +6 -6
  142. package/content/operators/_operator.md +3 -17
  143. package/content/operators/_ref.md +0 -258
  144. package/content/operators/_type.md +3 -14
  145. package/content/operators/_user.md +3 -3
  146. package/content/other/introduction.md +1 -1
  147. package/content/plugins/plugins-dev.md +0 -66
  148. package/content/plugins/plugins-introduction.md +1 -27
  149. package/content/testing/e2e-configuration.md +1 -8
  150. package/content/testing/e2e-getting-started.md +1 -1
  151. package/content/testing/e2e-introduction.md +1 -1
  152. package/content/tutorial/tutorial-actions-operators.md +2 -2
  153. package/content/tutorial/tutorial-add-blocks.md +2 -2
  154. package/content/tutorial/tutorial-create-page.md +2 -48
  155. package/content/tutorial/tutorial-display-data-page.md +3 -3
  156. package/content/tutorial/tutorial-requests-sql.md +0 -153
  157. package/content/tutorial/tutorial-start.md +1 -66
  158. package/content/user-authentication/login-and-logout.md +8 -158
  159. package/content/user-authentication/users-introduction.md +0 -8
  160. package/index.json +2 -6065
  161. package/package.json +1 -7
  162. package/scripts/extractAgentDocs.js +4 -36
  163. package/content/actions/emailotpsend.md +0 -58
  164. package/content/actions/emailotpverify.md +0 -77
  165. package/content/actions/getlocalstorage.md +0 -41
  166. package/content/actions/magiclinkverify.md +0 -113
  167. package/content/actions/posthogalias.md +0 -29
  168. package/content/actions/posthogcapture.md +0 -49
  169. package/content/actions/posthogcapturepageleave.md +0 -38
  170. package/content/actions/posthogfeatureflag.md +0 -50
  171. package/content/actions/posthoggroup.md +0 -37
  172. package/content/actions/posthogidentify.md +0 -54
  173. package/content/actions/posthoginit.md +0 -66
  174. package/content/actions/posthogoptin.md +0 -29
  175. package/content/actions/posthogoptout.md +0 -29
  176. package/content/actions/posthogpageview.md +0 -37
  177. package/content/actions/posthogreloadfeatureflags.md +0 -47
  178. package/content/actions/posthogreset.md +0 -28
  179. package/content/actions/posthogsetpersonproperties.md +0 -36
  180. package/content/actions/removelocalstorage.md +0 -26
  181. package/content/actions/setlocalstorage.md +0 -32
  182. package/content/adapter-reference/mongodbauthadapter.md +0 -46
  183. package/content/agents/agents-introduction.md +0 -107
  184. package/content/callback-reference/auth0logoutcallback.md +0 -54
  185. package/content/concepts/blocks.md +0 -150
  186. package/content/concepts/dynamic-page-content.md +0 -304
  187. package/content/concepts/html-attributes.md +0 -285
  188. package/content/concepts/lowdefy-api.md +0 -1110
  189. package/content/concepts/notifications.md +0 -155
  190. package/content/concepts/page-and-app-state.md +0 -244
  191. package/content/concepts/theming.md +0 -582
  192. package/content/connections/aigateway.md +0 -368
  193. package/content/connections/axioshttp.md +0 -181
  194. package/content/controls/while.md +0 -46
  195. package/content/display-blocks/clickablehtml.md +0 -239
  196. package/content/display-blocks/eventlog.md +0 -363
  197. package/content/display-blocks/pollingtimer.md +0 -263
  198. package/content/input-blocks/captcha.md +0 -40
  199. package/content/input-blocks/placesautocomplete.md +0 -319
  200. package/content/input-blocks/tagmultipleselector.md +0 -114
  201. package/content/input-blocks/tagselector.md +0 -114
  202. package/content/migration/auth-upgrade.md +0 -327
  203. package/content/migration/v3-to-v4.md +0 -417
  204. package/content/migration/v6-to-v7.md +0 -222
  205. package/content/operators/_cron.md +0 -292
  206. package/content/other/mongodbadapter.md +0 -3
  207. package/content/plugins/plugins-icon-sets.md +0 -267
  208. package/content/plugins/posthog.md +0 -275
  209. package/content/provider-reference/auth-js-providers.md +0 -107
  210. package/content/provider-reference/openidconnectprovider.md +0 -44
  211. package/content/testing/config-tests.md +0 -278
  212. package/content/tutorial/tutorial-requests-api.md +0 -155
  213. package/content/user-authentication/auth-configuration.md +0 -266
  214. package/content/user-authentication/auth-steps.md +0 -138
  215. package/content/user-authentication/mcp-oauth.md +0 -292
  216. package/content/user-authentication/organizations.md +0 -221
  217. package/content/user-authentication/protected-pages-apis.md +0 -97
  218. package/content/user-authentication/roles.md +0 -49
  219. package/content/user-authentication/two-factor.md +0 -229
  220. package/content/user-authentication/user-object.md +0 -58
  221. package/content/websockets/websocket-publish.md +0 -108
  222. package/content/websockets/websocket-subscriptions.md +0 -100
  223. package/content/websockets/websockets-introduction.md +0 -97
  224. package/hazards.json +0 -44
  225. package/scripts/hashDocSources.js +0 -38
  226. package/scripts/recordSourceHashes.js +0 -88
@@ -1,278 +0,0 @@
1
- # Config Tests
2
-
3
- Config tests let you test a Lowdefy app in the same language you build it in. A **journey** is a YAML file that names a page, a user to act as, and a list of steps — click this block, fill that input, wait for a request, expect this state — and `lowdefy test` runs every journey in your app and reports which passed.
4
-
5
- Journeys need no JavaScript, no Playwright setup and no separate test build. The runner boots your app's development server headless, drives each journey through the dev server's [journey route](/ai-agent-docs) (the same one the `lowdefy_run_journey` MCP tool uses), and prints a pass/fail line per journey. Because the format is declarative, an AI coding agent can write a journey for a page it just changed, run it, and read the failing step.
6
-
7
- > Config tests run against the **development server**. For tests against a production build, or tests that need custom browser code, use the Playwright based [e2e testing](/e2e-introduction) toolkit instead.
8
-
9
- ## Layout
10
-
11
- Journeys live in `tests/journeys/` inside your config directory, one `.yaml` file per journey or a top-level list of journeys per file:
12
-
13
- ```
14
- my-app/
15
- ├── lowdefy.yaml
16
- ├── pages/
17
- └── tests/
18
- └── journeys/
19
- ├── controls.yaml
20
- └── sign-up.yaml
21
- ```
22
-
23
- Files run in file-name order, and journeys run one at a time — each journey opens its own browser context, but they all share your app's database, so parallel journeys would interfere with one another.
24
-
25
- ## A journey
26
-
27
- ```yaml
28
- # tests/journeys/controls.yaml
29
- - name: member creates a control
30
- pageId: controls
31
- user: { roles: [admin] }
32
- urlQuery: { status: open }
33
- steps:
34
- - click: new_control
35
- - fill: { blockId: title, value: Access reviews }
36
- - click: submit
37
- - wait: { request: get_controls }
38
- - expect: { state: { path: controls.0.title, equals: Access reviews } }
39
-
40
- - name: guest sees the empty state
41
- pageId: controls
42
- steps:
43
- - expect: { visible: empty_state }
44
- ```
45
-
46
- | Field | Required | Description |
47
- | ---------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
48
- | `name` | Yes | A short description. `--filter` matches against it, and it is printed in the results. |
49
- | `pageId` | Yes | The page to open. |
50
- | `user` | No | The user to act as, as an inline user object such as `{ sub: u1, roles: [admin] }`. Leave it out to run as the default roleless headless user. |
51
- | `urlQuery` | No | An object appended to the page URL as a query string, for pages that read `_url_query`. |
52
- | `steps` | Yes | At least one step. Each step is an object with exactly one key from the step grammar below. |
53
-
54
- `user: none` injects no user at all, so the journey signs in through the app's own auth — see [Testing sign-up and sign-in](#testing-sign-up-and-sign-in).
55
-
56
- `timeout` sets how long each step may wait, in milliseconds (a whole number from 1 to 60000, default 5000). Raise it on a slow machine or CI runner rather than adding `wait: { ms }` steps.
57
-
58
- ### Timeouts
59
-
60
- The journey's `timeout` (the step timeout) bounds every wait a step makes for something to happen, and a step moves on as soon as it has:
61
-
62
- | Wait | Bound |
63
- | -------------------------------------------------------------------------- | ------------------------------------------ |
64
- | A control becoming actionable for `click`, `fill`, `select` | The step timeout. |
65
- | `expect` (`state`, `visible`, `text`, `url`, `title`) matching | The step timeout. |
66
- | `wait: { request }` and `wait: { state }` | The step timeout. |
67
- | `back` loading the previous page | The step timeout. |
68
- | An email arriving for `email` or `fill.fromEmail` | The step timeout. |
69
- | Opening a page: the journey's page, `goto`, the first `as` for a name | The step timeout, but at least 15 seconds. |
70
- | Settling after an interaction (`click`, `fill`, `select`, `press`, `back`) | The step timeout, but at most 5 seconds. |
71
- | `wait: { ms }` | Exactly `ms`; the timeout does not apply. |
72
-
73
- The settle after an interaction lets the page's own events and requests finish before the next step. It never fails a step: a page still busy after 5 seconds (an event that ends in a resend cooldown, for example) moves on, and the next step waits for what it needs itself. So raising `timeout` makes a slow journey pass without making a passing one slower.
74
-
75
- ## Steps
76
-
77
- Blocks are addressed by their `blockId`. A step that does not complete within the step timeout (5 seconds, or the journey's [`timeout`](#timeouts)) fails the journey. An `expect` step waits, up to that timeout, for what it checks to become true, so a value a click leads to can arrive a moment later.
78
-
79
- | Step | Meaning |
80
- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
81
- | `click: target` | Click the block, or the control a [target](#targets) narrows to. |
82
- | `fill: { blockId, value }` | Type `value` into the input inside the block (or the grid cell a target names). |
83
- | `fill: { blockId, fromEmail }` | Type text read from an [email](#emails) instead of a fixed value, such as a one-time sign-in code. The actor stays on the page. |
84
- | `select: { blockId, value }` | Open the selector block (or grid cell) and choose the option whose text is `value`. A radio, button or segmented selector's option is clicked by its label. |
85
- | `press: Enter` | Press a key or chord. `Mod` in a chord (`Mod+k`) resolves to Cmd on macOS and Ctrl elsewhere. |
86
- | `back: true` | Go back one page, like the browser's Back button. Fails when the journey has not navigated from an earlier page. |
87
- | `goto: pageId` | Load a page the way a typed URL does; `{ pageId, urlQuery }` adds a query string. A protected page may redirect (to sign in), so assert where it landed. |
88
- | `email: { to, subject }` | Open the newest [email](#emails) to `to` — with a subject containing `subject`, when given — that arrived during the journey, waiting for it if needed. |
89
- | `as: name` | Act as [another person](#several-people), each in their own browser. The journey starts as `main`. |
90
- | `wait: { ms }` | Pause for `ms` milliseconds. |
91
- | `wait: { request: requestId }` | Wait until the request has finished loading. |
92
- | `wait: { state: path }` | Wait until the state value at `path` is defined. |
93
- | `screenshot: name` | Capture a screenshot. Screenshots are returned to agents using the MCP tool; the CLI runner ignores them. |
94
- | `expect: { state: { path, equals } }` | The page state at `path` deep-equals `equals`. A path that does not exist reads as `null`, so `equals: null` also passes for a misspelt path. |
95
- | `expect: { visible: target }` | The block, or the control a target narrows to, is visible. |
96
- | `expect: { text: { blockId, contains } }` | The block's rendered text (or a grid row's or cell's) contains the string. |
97
- | `expect: { url: { contains } }` | The browser URL contains the string. |
98
- | `expect: { title: { equals } }` | The document title (the browser tab's text) is exactly the string; `{ contains }` checks part of it. |
99
-
100
- The full grammar, including the failure shape the route returns, is documented with the [journey tool](/ai-agent-docs). The CLI and the MCP tool share one implementation, so a journey an agent verifies interactively can be committed as-is.
101
-
102
- ### Targets
103
-
104
- A `blockId` reaches a block's own control — its button, input or link. Some controls are not blocks: the Edit and Delete buttons a grid renders in every row, the OK and Cancel of a confirm dialog, the items of a dropdown menu. Wherever a step takes a `blockId`, it also takes a target object that narrows the search:
105
-
106
- | Key | Meaning |
107
- | ------------ | ----------------------------------------------------------------------------------------------------------- |
108
- | `blockId` | The block to search inside. |
109
- | `row` | A grid row, zero-based as displayed (`AgGrid*` blocks). Needs `blockId`. |
110
- | `column` | A grid cell in that row, by the column's `field` or `colId`. Needs `blockId`. |
111
- | `text` | The interactive control whose visible text is exactly this (a button label, a tab, a menu item). |
112
- | `containing` | The element whose visible text contains this: a row of a list a person picks by the name or email it shows. |
113
- | `nth` | When several controls match, the zero-based one to use. |
114
-
115
- `text` on its own, with no `blockId`, searches the whole page — front-most layer first: an open dropdown menu, then an open dialog, then the page. That is how a confirm dialog's button is clicked while the grid behind its mask has a button with the same label.
116
-
117
- ```yaml
118
- - name: member deletes a control from the grid
119
- pageId: controls
120
- user: { roles: [admin] }
121
- steps:
122
- - click: { blockId: controls_grid, row: 1, text: Delete } # the row's Delete cell button
123
- - expect: { visible: { text: Are you sure? } }
124
- - click: { text: Delete } # the confirm dialog's OK, not the grid's
125
- - wait: { request: delete_control }
126
- - expect: { text: { blockId: controls_grid, row: 1, column: title, contains: Key rotation } }
127
- - click: { blockId: controls_grid, row: 0, column: actions } # the cell's first control
128
- - click: { blockId: controls_grid, row: 0, column: more, nth: 0 } # an icon-only menu trigger
129
- - click: { text: Archive } # the open menu's item
130
- ```
131
-
132
- Some rows are neither a block nor a control, such as the cards of a `ListSelector`. `containing` clicks the text a person would click on, and the click reaches the row's own handler:
133
-
134
- ```yaml
135
- - click: { blockId: members_list, containing: ada@example.test } # opens Ada's row
136
- ```
137
-
138
- `fill`, `select` and `expect.text` always need a `blockId`; a value is typed into a block's input, never into a page-wide control. A target with a key the grammar does not know (`colum`) is rejected before the browser opens, so a typo cannot pass as a step that happened to find nothing.
139
-
140
- ## Testing sign-up and sign-in
141
-
142
- A journey with `user: none` injects no user: it starts signed out, and the app's own auth decides who it is, exactly as in a real browser. Sign-up, email verification, sign-in, sign-out and organization switching all run for real, and the session cookie a sign-in sets carries through every later step.
143
-
144
- ```yaml
145
- # tests/journeys/sign-up.yaml
146
- - name: a new user signs up, verifies by email and signs in
147
- pageId: signup
148
- user: none
149
- timeout: 30000
150
- steps:
151
- - fill: { blockId: email, value: ada@example.test }
152
- - fill: { blockId: password, value: correct-horse-battery }
153
- - click: signup_button
154
- - email: { to: ada@example.test, subject: Verify your email address }
155
- - click: { text: Verify email address }
156
- - goto: login
157
- - fill: { blockId: email, value: ada@example.test }
158
- - fill: { blockId: password, value: correct-horse-battery }
159
- - click: login_button
160
- - goto: dashboard
161
- - expect: { text: { blockId: user_details, contains: ada@example.test } }
162
- ```
163
-
164
- A journey that opens a protected page signed out lands on the sign-in page, the way a visitor would; assert where it landed with `expect: { url: ... }`.
165
-
166
- Give journeys that sign people in a `timeout` of about 30000. They load many pages, reload the app after each sign-in and wait for real emails, so on a busy machine or a CI runner the 5 second default fails them at random steps.
167
-
168
- ### Emails
169
-
170
- The `email` step reads the mail the development server captured. Start the server with `LOWDEFY_DEV_SMTP_PORT` set to a free port (in the shell or the app's `.env`; the mail sink starts with the server, so restart it after adding the variable), and point the app's SMTP connection at `127.0.0.1` on that port through its secrets. The server then receives the app's mail over SMTP and keeps it instead of delivering it: every message the app sends — verification, magic link, invitation, your own `SMTPMailSend` requests — is written to `.lowdefy/mail/` in the app directory, and the directory is emptied each time the server starts. Only the development server does this, and only with the variable set; nothing in a production build captures mail.
171
-
172
- ```yaml
173
- connections:
174
- - id: email
175
- type: SMTP
176
- properties:
177
- from: app@example.com
178
- host:
179
- _secret: SMTP_HOST # 127.0.0.1 for tests
180
- port:
181
- _number.parseInt:
182
- on:
183
- _secret: SMTP_PORT # the LOWDEFY_DEV_SMTP_PORT value for tests
184
- ```
185
-
186
- `email` opens the newest matching message that arrived since the journey started, waiting up to the step timeout for one to arrive. The email is shown in the actor's tab, so `click: { text: Verify email address }` follows its button the way a person does, and `expect: { visible: { text: ... } }` checks its content. Opening the same email again (following an invitation link a second time after signing up) opens the same message. When nothing matches, the step fails with the messages that did arrive.
187
-
188
- A one-time code is typed, not clicked. `fill` with `fromEmail: { to, subject, match }` reads the same newest matching message, waiting for it the same way, and types the first match of the regular expression `match` (or its first capture group, when it has one) into the block. The actor stays on the page they are on, as a person reads the code on their phone and types it into the tab they started from. The match runs on the email's text, not its HTML markup. Anchor the pattern with `\b`, since the text can also hold the sign-in link, whose address may contain digits.
189
-
190
- ```yaml
191
- - click: login_magic_send
192
- - fill:
193
- blockId: otp
194
- fromEmail:
195
- to: ada@example.test
196
- subject: Your sign-in link
197
- match: '\b\d{6}\b'
198
- - click: login_code_verify
199
- ```
200
-
201
- ### Several people
202
-
203
- `as: invitee` switches the journey to another person with their own browser and cookies: an owner and the person they invite, or a member whose session stays open while the owner removes them. The journey starts as `main`. The first `as` for a name opens the journey's page in a new browser, as the journey's `user`; switching back returns to that person's tab as they left it.
204
-
205
- Each person also sends requests from their own client address, so auth rate limits (a few sign-in attempts per address every few seconds) count each person's attempts apart, as they would for people on different devices, instead of one budget for the whole run.
206
-
207
- ### The database
208
-
209
- Journeys perform real sign-ups, so they need a database that starts empty and is never a real one. Run them against a fresh test database each time: a sign-up journey run a second time finds its address already registered, and no verification email is sent. Give each journey its own addresses, so journeys in one run do not collide.
210
-
211
- Keep journeys that write, like these, out of `tests/journeys/` when your everyday `lowdefy test` runs against a shared database. Put them in their own directory, such as `tests/auth-journeys/`, and run them only from a script that starts the test database, the mail sink and the server they need:
212
-
213
- ```
214
- lowdefy test --journeys-directory tests/auth-journeys --url http://localhost:3290
215
- ```
216
-
217
- A plain `lowdefy test`, and the `lowdefy_run_tests` agent tool, never read that directory.
218
-
219
- ## Running
220
-
221
- ```
222
- pnpx lowdefy@5 test
223
- ```
224
-
225
- With no options, `lowdefy test` prepares `.lowdefy/dev` exactly as `lowdefy dev` does, starts the development server on a free port without opening a browser, runs every journey, prints the results and stops the server.
226
-
227
- ```
228
- PASS member creates a control (5 steps, 1840ms)
229
- FAIL guest sees the empty state
230
- file: /my-app/tests/journeys/controls.yaml
231
- step 0: { expect: { visible: empty_state } }
232
- expected: block "empty_state" to be visible
233
- actual: Timeout 5000ms exceeded.
234
- 1 passed, 1 failed of 2 journeys
235
- ```
236
-
237
- A failing journey stops at its first failing step and prints the step's index, the step itself, and the `expected` and `actual` values. Steps after the failure are not run.
238
-
239
- ### Options
240
-
241
- - `--filter <name>`: Only run journeys whose `name` contains the string (case-insensitive). `lowdefy test --filter control` runs every journey with "control" in its name.
242
- - `--journeys-directory <path>`: Read journeys from this directory instead of `tests/journeys/`, for journeys that need a server set up for them, such as [auth journeys](#the-database). A relative path is resolved from the current directory. The run fails when the directory holds no journeys.
243
- - `--url <url>`: Run against a development server that is already running instead of starting one, for example `lowdefy test --url http://localhost:3000` while `lowdefy dev` is open in another terminal. This is the fastest way to iterate on a journey.
244
- - `--port <port>`: The port to start the development server on. If it is in use the next free port is taken. The default is `3000`.
245
- - `--config-directory`, `--dev-directory`, `--ref-resolver`, `--log-level`, `--disable-telemetry`: As for [`lowdefy dev`](/cli#dev).
246
-
247
- ### Exit codes
248
-
249
- | Exit code | Meaning |
250
- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
251
- | `0` | Every journey passed, or `tests/journeys/` has no journeys (a note is printed). |
252
- | `1` | At least one journey failed, a journey file was invalid, an explicit `--filter` matched no journey, or a `--journeys-directory` held no journeys. |
253
-
254
- A journey file that is not valid YAML, or does not match the journey format (a missing `name`, a step with two keys, an unknown step key) is reported as a failed journey with the validation message and the file path. It never aborts the run, so one broken file cannot hide the results of the others.
255
-
256
- ## Continuous integration
257
-
258
- `lowdefy test` needs only Node.js, pnpm and a Chromium the dev server can launch. A GitHub Actions job looks like:
259
-
260
- ```yaml
261
- name: Config tests
262
- on: [push, pull_request]
263
- jobs:
264
- test:
265
- runs-on: ubuntu-latest
266
- steps:
267
- - uses: actions/checkout@v4
268
- - uses: pnpm/action-setup@v4
269
- - uses: actions/setup-node@v4
270
- with:
271
- node-version: 22
272
- - run: pnpx playwright install --with-deps chromium
273
- - run: pnpx lowdefy@5 test
274
- env:
275
- LOWDEFY_DISABLE_TELEMETRY: true
276
- ```
277
-
278
- Set the same environment variables (`.env` values, connection secrets) the app needs in dev, and make sure the database the journeys write to is a test database — journeys perform real actions against real requests.
@@ -1,155 +0,0 @@
1
- # 5. API / HTTP requests
2
-
3
- If you have been following along, you can continue with your current config. Else, you can find the config from the previous section [here](https://github.com/lowdefy/lowdefy-example-tutorial/tree/main/04-interactive-pages).
4
-
5
- We will now add a `AxiosHttp` [connection and request](/connections-and-requests) to get the list of products that tickets can be logged for from an external API endpoint.
6
-
7
- ## Configuring the AxiosHttp Connection and Request
8
-
9
- We want to add a [`Selector`](/Selector) block to our form that will allow the user to select which product they are logging a ticket about, from a preset list of options. This list of preset product options will be obtained from the [DummyJSON](https://dummyjson.com/) API.
10
-
11
- For this, we will make use of an [`AxiosHttp`](/AxiosHttp) connection as described in the following steps:
12
-
13
- #### 5.1. Adding a new connection to the app
14
-
15
- In your `lowdefy.yaml` file, add the following:
16
-
17
- ##### `lowdefy.yaml`
18
-
19
- ```yaml
20
- name: lowdefy-project-template
21
- version: 6.0.0
22
-
23
- ################ -------- Copy from here -------- ################
24
- connections:
25
- - id: dummy_api
26
- type: AxiosHttp
27
- properties:
28
- baseURL: https://dummyjson.com/
29
- ################ ------- Copy to here ----------- ################
30
-
31
- menus:
32
- # ...
33
- ```
34
-
35
- #### 5.2. Adding a request to the page
36
-
37
- In your `new-ticket.yaml` file, add the following request:
38
-
39
- ##### `pages/new-ticket.yaml`
40
-
41
- ```yaml
42
- id: new-ticket
43
- type: PageHeaderMenu
44
- ################ -------- Copy from here -------- ################
45
- requests:
46
- - id: get_products
47
- type: AxiosHttp
48
- connectionId: dummy_api
49
- properties:
50
- url: /products
51
- ################ ------- Copy to here ----------- ################
52
-
53
- properties:
54
- title: New ticket # The title in the browser tab.
55
- layout:
56
- justify: center # Center the contents of the page
57
- blocks:
58
- # ...
59
- ```
60
-
61
- #### 5.3. Triggering the request on page load
62
-
63
- In your `new-ticket.yaml` file, add an onMount event that calls `get_products` request:
64
-
65
- ##### `pages/new-ticket.yaml`
66
-
67
- ```yaml
68
- requests:
69
- - id: get_products
70
- type: AxiosHttp
71
- connectionId: dummy_api
72
- properties:
73
- url: /products
74
-
75
- ################ -------- Copy from here -------- ################
76
- events:
77
- onMount:
78
- - id: fetch_products
79
- type: Request
80
- params: get_products
81
- ################ ------- Copy to here ----------- ################
82
-
83
- properties:
84
- title: New ticket # The title in the browser tab.
85
- layout:
86
- justify: center # Center the contents of the page
87
- blocks:
88
- # ...
89
- ```
90
-
91
- #### 5.4. Using the request response as selector options
92
-
93
- In your `new-ticket.yaml` file, add the a Selector block and populate it with the results from the `get_products` request. We will make use of [`_array.map`](/_array#map_title) operator to get the product titles from the response obtained from the request. This is done as follows:
94
-
95
- ##### `pages/new-ticket.yaml`
96
-
97
- ```yaml
98
- id: new-ticket
99
- # ...
100
- blocks:
101
- - id: content_card
102
- # ...
103
- blocks:
104
- - id: page_heading
105
- type: Title
106
- properties:
107
- content: Log a ticket # Change the title on the page.
108
- level: 3 # Make the title a little smaller (an html `<h3>`).
109
- - id: ticket_title
110
- type: TextInput
111
- properties:
112
- title: Title
113
- - id: ticket_type
114
- type: ButtonSelector
115
- properties:
116
- title: Ticket type
117
- options: # Set the allowed options
118
- - Suggestion
119
- - Complaint
120
- - Question
121
-
122
- ################ ------- Copy from here -------- ################
123
- - id: product
124
- type: Selector
125
- required: true
126
- properties:
127
- title: Product
128
- options:
129
- # Map over API response to create an array of product titles
130
- _array.map:
131
- # use dot notation to access value in response object properties
132
- - _request: get_products.data.products
133
- - _function:
134
- __args: 0.title
135
- ################ ------- Copy to here -------- ################
136
-
137
- - id: ticket_description
138
- type: TextArea
139
- properties:
140
- title: Description
141
- ```
142
-
143
- ### What happened
144
-
145
- We defined the [`AxiosHttp`](/AxiosHttp) connection to be used to obtain the list of products that tickets can be created for.
146
-
147
- We also defined an [`AxiosHttp`](/AxiosHttp) request, to fetch the products data when the page is mounted. To do this, we used the onMount event to execute the Request action to make the API call.
148
-
149
- We then mapped the request response to the product selector options by using the [_request](/_request) and [_array.map](/_array#map_title) operators.
150
-
151
- > You can find the final configuration files for this section [here](https://github.com/lowdefy/lowdefy-example-tutorial/tree/main/05-requests-api).
152
-
153
- ### Up next
154
-
155
- Next we will save the form data to a SQLite database file.