@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.
- package/README.md +0 -1
- package/content/actions/link.md +0 -17
- package/content/actions/login.md +5 -23
- package/content/actions/logout.md +5 -5
- package/content/actions/pdfmake.md +2 -2
- package/content/actions/setdarkmode.md +2 -2
- package/content/agents/agent-properties.md +2 -4
- package/content/agents/agentchat.md +6 -21
- package/content/agents/agentconversations.md +2 -2
- package/content/concepts/ai-agent-docs.md +101 -313
- package/content/concepts/cli.md +3 -34
- package/content/concepts/connections-and-requests.md +0 -402
- package/content/concepts/custom-html.md +0 -40
- package/content/concepts/custom-styling.md +5 -50
- package/content/concepts/events-and-actions.md +2 -64
- package/content/concepts/i18n.md +2 -7
- package/content/concepts/keyboard-shortcuts.md +2 -2
- package/content/concepts/layout-overview.md +1 -1
- package/content/concepts/logger.md +0 -81
- package/content/concepts/lowdefy-schema.md +1 -4
- package/content/concepts/menus.md +7 -7
- package/content/concepts/module-authoring.md +2 -2
- package/content/connections/anthropic.md +2 -162
- package/content/connections/awss3.md +0 -54
- package/content/connections/google.md +2 -162
- package/content/connections/mongodb.md +3 -121
- package/content/connections/openai-connection.md +2 -162
- package/content/container-blocks/affix.md +7 -47
- package/content/container-blocks/alert.md +24 -76
- package/content/container-blocks/badge.md +24 -100
- package/content/container-blocks/box.md +9 -9
- package/content/container-blocks/card.md +25 -80
- package/content/container-blocks/carousel.md +7 -60
- package/content/container-blocks/collapse.md +9 -87
- package/content/container-blocks/configprovider.md +13 -35
- package/content/container-blocks/confirmmodal.md +23 -69
- package/content/container-blocks/content.md +98 -62
- package/content/container-blocks/descriptions.md +4 -45
- package/content/container-blocks/drawer.md +28 -92
- package/content/container-blocks/dropdownmenu.md +31 -119
- package/content/container-blocks/flex.md +62 -40
- package/content/container-blocks/footer.md +11 -15
- package/content/container-blocks/googlemapsheatmap.md +1 -1
- package/content/container-blocks/googlemapsscript.md +2 -2
- package/content/container-blocks/header.md +36 -71
- package/content/container-blocks/label.md +7 -34
- package/content/container-blocks/layout.md +34 -52
- package/content/container-blocks/masonry.md +71 -17
- package/content/container-blocks/mobilemenu.md +88 -131
- package/content/container-blocks/modal.md +26 -114
- package/content/container-blocks/pageheadermenu.md +31 -81
- package/content/container-blocks/pagesidebarlayout.md +26 -169
- package/content/container-blocks/pagesidermenu.md +42 -95
- package/content/container-blocks/popover.md +31 -34
- package/content/container-blocks/result.md +21 -32
- package/content/container-blocks/sider.md +122 -85
- package/content/container-blocks/span.md +7 -7
- package/content/container-blocks/spinner.md +3 -3
- package/content/container-blocks/splitter.md +68 -150
- package/content/container-blocks/tabs.md +12 -64
- package/content/container-blocks/tooltip.md +46 -22
- package/content/container-blocks/watermark.md +4 -22
- package/content/controls/reject.md +2 -4
- package/content/controls/throw.md +0 -1
- package/content/controls/try.md +2 -3
- package/content/deployment/deployment-environments.md +6 -8
- package/content/deployment/docker.md +7 -22
- package/content/deployment/node-server.md +0 -4
- package/content/deployment/vercel.md +6 -13
- package/content/display-blocks/aggrid.md +11 -26
- package/content/display-blocks/anchor.md +15 -15
- package/content/display-blocks/avatar.md +37 -57
- package/content/display-blocks/breadcrumb.md +21 -70
- package/content/display-blocks/button.md +18 -92
- package/content/display-blocks/calendar.md +0 -12
- package/content/display-blocks/dangeroushtml.md +1 -1
- package/content/display-blocks/difflist.md +1 -1
- package/content/display-blocks/difftimeline.md +1 -1
- package/content/display-blocks/divider.md +3 -64
- package/content/display-blocks/dropdownbutton.md +28 -79
- package/content/display-blocks/echart.md +1 -35
- package/content/display-blocks/floatbutton.md +42 -129
- package/content/display-blocks/html.md +1 -61
- package/content/display-blocks/icon.md +67 -250
- package/content/display-blocks/menu.md +73 -145
- package/content/display-blocks/message.md +124 -38
- package/content/display-blocks/notification.md +222 -94
- package/content/display-blocks/paragraph.md +9 -59
- package/content/display-blocks/progress.md +17 -96
- package/content/display-blocks/qrcode.md +4 -19
- package/content/display-blocks/s3download.md +1 -1
- package/content/display-blocks/search.md +7 -17
- package/content/display-blocks/skeletonbutton.md +0 -1
- package/content/display-blocks/skeletoninput.md +0 -1
- package/content/display-blocks/statistic.md +17 -75
- package/content/display-blocks/steps.md +25 -58
- package/content/display-blocks/tag.md +17 -102
- package/content/display-blocks/title.md +11 -50
- package/content/display-blocks/tour.md +23 -33
- package/content/input-blocks/autocomplete.md +9 -81
- package/content/input-blocks/buttonselector.md +8 -45
- package/content/input-blocks/checkboxselector.md +7 -97
- package/content/input-blocks/checkboxswitch.md +3 -33
- package/content/input-blocks/colorselector.md +14 -63
- package/content/input-blocks/daterangeselector.md +45 -165
- package/content/input-blocks/dateselector.md +41 -139
- package/content/input-blocks/datetimeselector.md +49 -158
- package/content/input-blocks/inputgallery.md +3 -3
- package/content/input-blocks/monthselector.md +52 -120
- package/content/input-blocks/multipleselector.md +39 -150
- package/content/input-blocks/numberinput.md +12 -98
- package/content/input-blocks/pagination.md +14 -70
- package/content/input-blocks/paragraphinput.md +37 -51
- package/content/input-blocks/passwordinput.md +6 -60
- package/content/input-blocks/phonenumberinput.md +26 -59
- package/content/input-blocks/radioselector.md +7 -83
- package/content/input-blocks/ratingslider.md +26 -57
- package/content/input-blocks/s3uploadbutton.md +24 -24
- package/content/input-blocks/s3uploaddragger.md +3 -3
- package/content/input-blocks/s3uploadphoto.md +5 -5
- package/content/input-blocks/segmentedselector.md +32 -72
- package/content/input-blocks/selector.md +21 -109
- package/content/input-blocks/switch.md +17 -65
- package/content/input-blocks/textarea.md +3 -32
- package/content/input-blocks/textinput.md +28 -69
- package/content/input-blocks/tiptapmentioninput.md +1 -1
- package/content/input-blocks/titleinput.md +12 -39
- package/content/input-blocks/treeinput.md +25 -49
- package/content/input-blocks/treemultipleselector.md +10 -127
- package/content/input-blocks/treeselector.md +9 -97
- package/content/input-blocks/upload.md +15 -64
- package/content/input-blocks/uploaddragger.md +1 -17
- package/content/input-blocks/uploadphoto.md +1 -18
- package/content/input-blocks/weekselector.md +55 -120
- package/content/list-blocks/controlledlist.md +15 -24
- package/content/list-blocks/list.md +11 -11
- package/content/list-blocks/masonrylist.md +34 -7
- package/content/migration/upgrade-guide.md +0 -1
- package/content/operators/_app.md +1 -2
- package/content/operators/_error.md +1 -23
- package/content/operators/_get.md +6 -6
- package/content/operators/_operator.md +3 -17
- package/content/operators/_ref.md +0 -258
- package/content/operators/_type.md +3 -14
- package/content/operators/_user.md +3 -3
- package/content/other/introduction.md +1 -1
- package/content/plugins/plugins-dev.md +0 -66
- package/content/plugins/plugins-introduction.md +1 -27
- package/content/testing/e2e-configuration.md +1 -8
- package/content/testing/e2e-getting-started.md +1 -1
- package/content/testing/e2e-introduction.md +1 -1
- package/content/tutorial/tutorial-actions-operators.md +2 -2
- package/content/tutorial/tutorial-add-blocks.md +2 -2
- package/content/tutorial/tutorial-create-page.md +2 -48
- package/content/tutorial/tutorial-display-data-page.md +3 -3
- package/content/tutorial/tutorial-requests-sql.md +0 -153
- package/content/tutorial/tutorial-start.md +1 -66
- package/content/user-authentication/login-and-logout.md +8 -158
- package/content/user-authentication/users-introduction.md +0 -8
- package/index.json +2 -6065
- package/package.json +1 -7
- package/scripts/extractAgentDocs.js +4 -36
- package/content/actions/emailotpsend.md +0 -58
- package/content/actions/emailotpverify.md +0 -77
- package/content/actions/getlocalstorage.md +0 -41
- package/content/actions/magiclinkverify.md +0 -113
- package/content/actions/posthogalias.md +0 -29
- package/content/actions/posthogcapture.md +0 -49
- package/content/actions/posthogcapturepageleave.md +0 -38
- package/content/actions/posthogfeatureflag.md +0 -50
- package/content/actions/posthoggroup.md +0 -37
- package/content/actions/posthogidentify.md +0 -54
- package/content/actions/posthoginit.md +0 -66
- package/content/actions/posthogoptin.md +0 -29
- package/content/actions/posthogoptout.md +0 -29
- package/content/actions/posthogpageview.md +0 -37
- package/content/actions/posthogreloadfeatureflags.md +0 -47
- package/content/actions/posthogreset.md +0 -28
- package/content/actions/posthogsetpersonproperties.md +0 -36
- package/content/actions/removelocalstorage.md +0 -26
- package/content/actions/setlocalstorage.md +0 -32
- package/content/adapter-reference/mongodbauthadapter.md +0 -46
- package/content/agents/agents-introduction.md +0 -107
- package/content/callback-reference/auth0logoutcallback.md +0 -54
- package/content/concepts/blocks.md +0 -150
- package/content/concepts/dynamic-page-content.md +0 -304
- package/content/concepts/html-attributes.md +0 -285
- package/content/concepts/lowdefy-api.md +0 -1110
- package/content/concepts/notifications.md +0 -155
- package/content/concepts/page-and-app-state.md +0 -244
- package/content/concepts/theming.md +0 -582
- package/content/connections/aigateway.md +0 -368
- package/content/connections/axioshttp.md +0 -181
- package/content/controls/while.md +0 -46
- package/content/display-blocks/clickablehtml.md +0 -239
- package/content/display-blocks/eventlog.md +0 -363
- package/content/display-blocks/pollingtimer.md +0 -263
- package/content/input-blocks/captcha.md +0 -40
- package/content/input-blocks/placesautocomplete.md +0 -319
- package/content/input-blocks/tagmultipleselector.md +0 -114
- package/content/input-blocks/tagselector.md +0 -114
- package/content/migration/auth-upgrade.md +0 -327
- package/content/migration/v3-to-v4.md +0 -417
- package/content/migration/v6-to-v7.md +0 -222
- package/content/operators/_cron.md +0 -292
- package/content/other/mongodbadapter.md +0 -3
- package/content/plugins/plugins-icon-sets.md +0 -267
- package/content/plugins/posthog.md +0 -275
- package/content/provider-reference/auth-js-providers.md +0 -107
- package/content/provider-reference/openidconnectprovider.md +0 -44
- package/content/testing/config-tests.md +0 -278
- package/content/tutorial/tutorial-requests-api.md +0 -155
- package/content/user-authentication/auth-configuration.md +0 -266
- package/content/user-authentication/auth-steps.md +0 -138
- package/content/user-authentication/mcp-oauth.md +0 -292
- package/content/user-authentication/organizations.md +0 -221
- package/content/user-authentication/protected-pages-apis.md +0 -97
- package/content/user-authentication/roles.md +0 -49
- package/content/user-authentication/two-factor.md +0 -229
- package/content/user-authentication/user-object.md +0 -58
- package/content/websockets/websocket-publish.md +0 -108
- package/content/websockets/websocket-subscriptions.md +0 -100
- package/content/websockets/websockets-introduction.md +0 -97
- package/hazards.json +0 -44
- package/scripts/hashDocSources.js +0 -38
- 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.
|