@lowdefy/docs-content 0.0.0-experimental-20260910082708 → 6.0.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/agents/agentchat.md +1 -13
- package/content/concepts/ai-agent-docs.md +49 -232
- package/content/concepts/cli.md +0 -13
- package/content/concepts/custom-styling.md +5 -50
- package/content/concepts/i18n.md +1 -1
- package/content/concepts/layout-overview.md +1 -1
- package/content/connections/mongodb.md +3 -121
- package/content/container-blocks/collapse.md +2 -2
- package/content/container-blocks/layout.md +0 -1
- package/content/container-blocks/modal.md +1 -1
- package/content/container-blocks/pagesidebarlayout.md +0 -1
- package/content/container-blocks/sider.md +1 -1
- package/content/container-blocks/spinner.md +1 -1
- package/content/controls/reject.md +1 -3
- package/content/controls/throw.md +0 -1
- package/content/controls/try.md +2 -3
- package/content/display-blocks/aggrid.md +18 -18
- package/content/display-blocks/avatar.md +2 -2
- package/content/display-blocks/difflist.md +1 -1
- package/content/display-blocks/difftimeline.md +1 -1
- package/content/display-blocks/echart.md +1 -35
- package/content/display-blocks/menu.md +1 -1
- package/content/display-blocks/qrcode.md +2 -2
- package/content/display-blocks/s3download.md +1 -1
- package/content/display-blocks/search.md +0 -1
- package/content/display-blocks/skeletonbutton.md +0 -1
- package/content/display-blocks/skeletoninput.md +0 -1
- package/content/input-blocks/buttonselector.md +1 -1
- package/content/input-blocks/checkboxselector.md +5 -81
- package/content/input-blocks/colorselector.md +0 -14
- package/content/input-blocks/multipleselector.md +1 -1
- package/content/input-blocks/phonenumberinput.md +1 -2
- package/content/input-blocks/radioselector.md +4 -66
- package/content/input-blocks/s3uploadbutton.md +1 -1
- package/content/input-blocks/s3uploaddragger.md +1 -1
- package/content/input-blocks/s3uploadphoto.md +1 -1
- package/content/input-blocks/segmentedselector.md +1 -1
- package/content/input-blocks/selector.md +1 -1
- package/content/input-blocks/tiptapmentioninput.md +1 -1
- package/content/input-blocks/treeinput.md +1 -1
- package/content/input-blocks/treemultipleselector.md +1 -1
- package/content/input-blocks/treeselector.md +1 -1
- package/content/list-blocks/masonrylist.md +1 -1
- package/content/operators/_type.md +3 -14
- package/content/plugins/plugins-introduction.md +0 -15
- package/index.json +1 -31
- package/package.json +1 -3
- package/content/controls/while.md +0 -46
- package/content/input-blocks/tagmultipleselector.md +0 -101
- package/content/input-blocks/tagselector.md +0 -101
- package/content/testing/config-tests.md +0 -164
- package/hazards.json +0 -44
|
@@ -1,164 +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
|
-
## Steps
|
|
55
|
-
|
|
56
|
-
Blocks are addressed by their `blockId`. Every step has a 5 second timeout by default; a step that does not complete in time fails the journey.
|
|
57
|
-
|
|
58
|
-
| Step | Meaning |
|
|
59
|
-
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
60
|
-
| `click: target` | Click the block, or the control a [target](#targets) narrows to. |
|
|
61
|
-
| `fill: { blockId, value }` | Type `value` into the input inside the block (or the grid cell a target names). |
|
|
62
|
-
| `select: { blockId, value }` | Open the selector block (or grid cell) and choose the option whose text is `value`. |
|
|
63
|
-
| `press: Enter` | Press a key or chord. `Mod` in a chord (`Mod+k`) resolves to Cmd on macOS and Ctrl elsewhere. |
|
|
64
|
-
| `wait: { ms }` | Pause for `ms` milliseconds. |
|
|
65
|
-
| `wait: { request: requestId }` | Wait until the request has finished loading. |
|
|
66
|
-
| `wait: { state: path }` | Wait until the state value at `path` is defined. |
|
|
67
|
-
| `screenshot: name` | Capture a screenshot. Screenshots are returned to agents using the MCP tool; the CLI runner ignores them. |
|
|
68
|
-
| `expect: { state: { path, equals } }` | The page state at `path` deep-equals `equals`. |
|
|
69
|
-
| `expect: { visible: target }` | The block, or the control a target narrows to, is visible. |
|
|
70
|
-
| `expect: { text: { blockId, contains } }` | The block's rendered text (or a grid row's or cell's) contains the string. |
|
|
71
|
-
| `expect: { url: { contains } }` | The browser URL contains the string. |
|
|
72
|
-
|
|
73
|
-
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.
|
|
74
|
-
|
|
75
|
-
### Targets
|
|
76
|
-
|
|
77
|
-
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:
|
|
78
|
-
|
|
79
|
-
| Key | Meaning |
|
|
80
|
-
| --------- | ------------------------------------------------------------------------------------------------ |
|
|
81
|
-
| `blockId` | The block to search inside. |
|
|
82
|
-
| `row` | A grid row, zero-based as displayed (`AgGrid*` blocks). Needs `blockId`. |
|
|
83
|
-
| `column` | A grid cell in that row, by the column's `field` or `colId`. Needs `blockId`. |
|
|
84
|
-
| `text` | The interactive control whose visible text is exactly this (a button label, a tab, a menu item). |
|
|
85
|
-
| `nth` | When several controls match, the zero-based one to use. |
|
|
86
|
-
|
|
87
|
-
`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.
|
|
88
|
-
|
|
89
|
-
```yaml
|
|
90
|
-
- name: member deletes a control from the grid
|
|
91
|
-
pageId: controls
|
|
92
|
-
user: { roles: [admin] }
|
|
93
|
-
steps:
|
|
94
|
-
- click: { blockId: controls_grid, row: 1, text: Delete } # the row's Delete cell button
|
|
95
|
-
- expect: { visible: { text: Are you sure? } }
|
|
96
|
-
- click: { text: Delete } # the confirm dialog's OK, not the grid's
|
|
97
|
-
- wait: { request: delete_control }
|
|
98
|
-
- expect: { text: { blockId: controls_grid, row: 1, column: title, contains: Key rotation } }
|
|
99
|
-
- click: { blockId: controls_grid, row: 0, column: actions } # the cell's first control
|
|
100
|
-
- click: { blockId: controls_grid, row: 0, column: more, nth: 0 } # an icon-only menu trigger
|
|
101
|
-
- click: { text: Archive } # the open menu's item
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
`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.
|
|
105
|
-
|
|
106
|
-
## Running
|
|
107
|
-
|
|
108
|
-
```
|
|
109
|
-
pnpx lowdefy@5 test
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
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.
|
|
113
|
-
|
|
114
|
-
```
|
|
115
|
-
PASS member creates a control (5 steps, 1840ms)
|
|
116
|
-
FAIL guest sees the empty state
|
|
117
|
-
file: /my-app/tests/journeys/controls.yaml
|
|
118
|
-
step 0: { expect: { visible: empty_state } }
|
|
119
|
-
expected: block "empty_state" to be visible
|
|
120
|
-
actual: Timeout 5000ms exceeded.
|
|
121
|
-
1 passed, 1 failed of 2 journeys
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
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.
|
|
125
|
-
|
|
126
|
-
### Options
|
|
127
|
-
|
|
128
|
-
- `--filter <name>`: Only run journeys whose `name` contains the string (case-insensitive). `lowdefy test --filter control` runs every journey with "control" in its name.
|
|
129
|
-
- `--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.
|
|
130
|
-
- `--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`.
|
|
131
|
-
- `--config-directory`, `--dev-directory`, `--ref-resolver`, `--log-level`, `--disable-telemetry`: As for [`lowdefy dev`](/cli#dev).
|
|
132
|
-
|
|
133
|
-
### Exit codes
|
|
134
|
-
|
|
135
|
-
| Exit code | Meaning |
|
|
136
|
-
| --------- | ------------------------------------------------------------------------------------------------------ |
|
|
137
|
-
| `0` | Every journey passed, or `tests/journeys/` has no journeys (a note is printed). |
|
|
138
|
-
| `1` | At least one journey failed, a journey file was invalid, or an explicit `--filter` matched no journey. |
|
|
139
|
-
|
|
140
|
-
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.
|
|
141
|
-
|
|
142
|
-
## Continuous integration
|
|
143
|
-
|
|
144
|
-
`lowdefy test` needs only Node.js, pnpm and a Chromium the dev server can launch. A GitHub Actions job looks like:
|
|
145
|
-
|
|
146
|
-
```yaml
|
|
147
|
-
name: Config tests
|
|
148
|
-
on: [push, pull_request]
|
|
149
|
-
jobs:
|
|
150
|
-
test:
|
|
151
|
-
runs-on: ubuntu-latest
|
|
152
|
-
steps:
|
|
153
|
-
- uses: actions/checkout@v4
|
|
154
|
-
- uses: pnpm/action-setup@v4
|
|
155
|
-
- uses: actions/setup-node@v4
|
|
156
|
-
with:
|
|
157
|
-
node-version: 22
|
|
158
|
-
- run: pnpx playwright install --with-deps chromium
|
|
159
|
-
- run: pnpx lowdefy@5 test
|
|
160
|
-
env:
|
|
161
|
-
LOWDEFY_DISABLE_TELEMETRY: true
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
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.
|
package/hazards.json
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"id": "state-in-request-properties",
|
|
4
|
-
"appliesTo": { "kinds": ["requests"] },
|
|
5
|
-
"message": "_state in request properties is always undefined — a request is evaluated on the server with an empty state. Pass the value in the payload and read it with _payload.",
|
|
6
|
-
"see": "concepts/connections-and-requests"
|
|
7
|
-
},
|
|
8
|
-
{
|
|
9
|
-
"id": "visible-false-prunes-state",
|
|
10
|
-
"appliesTo": { "kinds": ["blocks"] },
|
|
11
|
-
"message": "visible: false removes the block and its state. Style it hidden (for example style: { display: none }) to keep the value.",
|
|
12
|
-
"see": "concepts/lowdefy-schema"
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
"id": "link-same-page-no-onmount",
|
|
16
|
-
"appliesTo": { "types": ["Link"] },
|
|
17
|
-
"message": "A Link to the page already open does not remount it, so onMount does not fire again. Trigger the reload chain from the same action list.",
|
|
18
|
-
"see": "actions/link"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"id": "ref-njk-renders-at-build",
|
|
22
|
-
"appliesTo": { "types": ["_ref"] },
|
|
23
|
-
"message": "A _ref to a .njk file renders at build, so _state, _request and _user are undefined inside it. Use _nunjucks over a .txt file for a template that needs runtime values.",
|
|
24
|
-
"see": "operators/_ref"
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"id": "ref-js-imported-as-module",
|
|
28
|
-
"appliesTo": { "types": ["_ref"] },
|
|
29
|
-
"message": "A _ref to a .js file imports it as a module rather than reading it as text. Name the file .js.txt to read its source.",
|
|
30
|
-
"see": "operators/_ref"
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
"id": "tenant-wall-lookup",
|
|
34
|
-
"appliesTo": { "kinds": ["requests"], "when": "connectionTenantNotNone" },
|
|
35
|
-
"message": "This request runs over a tenant-walled connection. The wall injects { $match: { <tenant field> } } into the top-level pipeline and into every $lookup / $unionWith sub-pipeline, so a lookup into a collection with no tenant field returns [] with no error. Run the request over the shared connection and pass tenant facts in the payload instead.",
|
|
36
|
-
"see": "concepts/connections-and-requests"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"id": "reject-not-caught",
|
|
40
|
-
"appliesTo": { "types": [":reject"] },
|
|
41
|
-
"message": ":reject is the routine's reply, not an exception. A :catch never runs for it, :finally does, and :throw is the caught path.",
|
|
42
|
-
"see": "controls/reject"
|
|
43
|
-
}
|
|
44
|
-
]
|