@dopame/acde-linux-x64 0.0.0-stage → 0.0.10

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 (165) hide show
  1. package/LICENSE +8 -0
  2. package/bin/acde +66 -0
  3. package/bin/acde.cmd +45 -0
  4. package/bin/acdebin-dbg +4 -0
  5. package/bin/app/brand/art.mjs +53 -0
  6. package/bin/app/brand/brand.css +39 -0
  7. package/bin/app/brand/lockup.mjs +17 -0
  8. package/bin/app/brand/mark.mjs +124 -0
  9. package/bin/app/dist/broker.js +1 -0
  10. package/bin/app/dist/web.js +1 -0
  11. package/bin/app/greet/index.html +177 -0
  12. package/bin/app/greet/package.json +16 -0
  13. package/bin/app/package.json +35 -0
  14. package/lib/acde/LICENSE +8 -0
  15. package/lib/acde/dist/chunk-0n69bv5q.js +1 -0
  16. package/lib/acde/dist/chunk-1cw92x19.js +1 -0
  17. package/lib/acde/dist/chunk-458e14zb.js +1 -0
  18. package/lib/acde/dist/chunk-88mxgp54.js +1 -0
  19. package/lib/acde/dist/chunk-8dv09ven.js +1 -0
  20. package/lib/acde/dist/chunk-8fxsj5h6.js +1 -0
  21. package/lib/acde/dist/chunk-8pf3kk8v.js +1 -0
  22. package/lib/acde/dist/chunk-8r65bz1e.js +1 -0
  23. package/lib/acde/dist/chunk-9hpnkmxn.js +1 -0
  24. package/lib/acde/dist/chunk-a7w41xr9.js +1 -0
  25. package/lib/acde/dist/chunk-db7qqt8a.js +1 -0
  26. package/lib/acde/dist/chunk-e3rzr5fj.js +1 -0
  27. package/lib/acde/dist/chunk-ehms6xjp.js +1 -0
  28. package/lib/acde/dist/chunk-ggykhab6.js +1 -0
  29. package/lib/acde/dist/chunk-gt70e8h2.js +1 -0
  30. package/lib/acde/dist/chunk-hcf0xd5c.js +1 -0
  31. package/lib/acde/dist/chunk-hdrvz9sa.js +1 -0
  32. package/lib/acde/dist/chunk-jjq2mn94.js +1 -0
  33. package/lib/acde/dist/chunk-k8f4m94e.js +7 -0
  34. package/lib/acde/dist/chunk-mtgy1bm0.js +1 -0
  35. package/lib/acde/dist/chunk-mtw6p7kv.js +1 -0
  36. package/lib/acde/dist/chunk-nwkq7cpp.js +1 -0
  37. package/lib/acde/dist/chunk-nyr54266.js +1 -0
  38. package/lib/acde/dist/chunk-p0h5kspx.js +1 -0
  39. package/lib/acde/dist/chunk-pdj3h86e.js +1 -0
  40. package/lib/acde/dist/chunk-q9dbzac5.js +1 -0
  41. package/lib/acde/dist/chunk-qrhbrjzk.js +1 -0
  42. package/lib/acde/dist/chunk-s8p0y5ap.js +1 -0
  43. package/lib/acde/dist/chunk-t0xqxe00.js +1 -0
  44. package/lib/acde/dist/chunk-tgn5c2mw.js +1 -0
  45. package/lib/acde/dist/chunk-wmj6zwvn.js +1 -0
  46. package/lib/acde/dist/chunk-wnk81fkc.js +1 -0
  47. package/lib/acde/dist/chunk-x208z38w.js +1 -0
  48. package/lib/acde/dist/chunk-xq76vbg6.js +1 -0
  49. package/lib/acde/dist/chunk-xr9gh5x2.js +1 -0
  50. package/lib/acde/dist/chunk-yytnhz6e.js +1 -0
  51. package/lib/acde/dist/chunk-z0ts25s1.js +1 -0
  52. package/lib/acde/dist/chunk-zdyw1ckt.js +1 -0
  53. package/lib/acde/dist/cli.js +1 -0
  54. package/lib/acde/dist/driver/index.js +1 -0
  55. package/lib/acde/dist/e2e/index.js +1 -0
  56. package/lib/acde/dist/types/configs.d.ts +1 -0
  57. package/lib/acde/dist/types/driver/acde.d.ts +106 -0
  58. package/lib/acde/dist/types/driver/clock.d.ts +32 -0
  59. package/lib/acde/dist/types/driver/expect.d.ts +60 -0
  60. package/lib/acde/dist/types/driver/format.d.ts +16 -0
  61. package/lib/acde/dist/types/driver/index.d.ts +13 -0
  62. package/lib/acde/dist/types/driver/input.d.ts +75 -0
  63. package/lib/acde/dist/types/driver/locator.d.ts +151 -0
  64. package/lib/acde/dist/types/driver/logs.d.ts +26 -0
  65. package/lib/acde/dist/types/driver/png.d.ts +31 -0
  66. package/lib/acde/dist/types/driver/poll.d.ts +4 -0
  67. package/lib/acde/dist/types/driver/screenshot.d.ts +37 -0
  68. package/lib/acde/dist/types/driver/trace.d.ts +26 -0
  69. package/lib/acde/dist/types/driver/types.d.ts +213 -0
  70. package/lib/acde/dist/types/driver/video.d.ts +14 -0
  71. package/lib/acde/dist/types/driver/window.d.ts +77 -0
  72. package/lib/acde/dist/types/e2e/index.d.ts +78 -0
  73. package/lib/acde/dist/types/e2e/settings.d.ts +39 -0
  74. package/lib/acde/dist/types/ipc.d.ts +24 -0
  75. package/lib/acde/dist/types/paths.d.ts +5 -0
  76. package/lib/acde/dist/types/pm.d.ts +20 -0
  77. package/lib/acde/dist/types/runtime.d.ts +49 -0
  78. package/lib/acde/dist/types/sessions.d.ts +80 -0
  79. package/lib/acde/dist/types/sockets.d.ts +4 -0
  80. package/lib/acde/launcher.d.mts +25 -0
  81. package/lib/acde/launcher.mjs +1 -0
  82. package/lib/acde/package.json +31 -0
  83. package/lib/acde/skills/acde/SKILL.md +114 -0
  84. package/lib/acde/skills/acde/references/guide/agents.md +50 -0
  85. package/lib/acde/skills/acde/references/guide/assets.md +40 -0
  86. package/lib/acde/skills/acde/references/guide/backend.md +94 -0
  87. package/lib/acde/skills/acde/references/guide/capabilities.md +157 -0
  88. package/lib/acde/skills/acde/references/guide/config.md +73 -0
  89. package/lib/acde/skills/acde/references/guide/desktop.md +165 -0
  90. package/lib/acde/skills/acde/references/guide/dev.md +66 -0
  91. package/lib/acde/skills/acde/references/guide/doctor.md +144 -0
  92. package/lib/acde/skills/acde/references/guide/files.md +181 -0
  93. package/lib/acde/skills/acde/references/guide/frameworks.md +74 -0
  94. package/lib/acde/skills/acde/references/guide/install.md +34 -0
  95. package/lib/acde/skills/acde/references/guide/mcp.md +64 -0
  96. package/lib/acde/skills/acde/references/guide/open.md +107 -0
  97. package/lib/acde/skills/acde/references/guide/services.md +147 -0
  98. package/lib/acde/skills/acde/references/guide/testing.md +378 -0
  99. package/lib/acde/skills/acde/references/guide/tools.md +291 -0
  100. package/lib/acde/skills/acde/references/guide/window.md +167 -0
  101. package/lib/acde/skills/acde/references/reference/cli.md +221 -0
  102. package/lib/acde/skills/acde/references/reference/config.md +51 -0
  103. package/lib/acde/skills/acde/references/reference/env.md +44 -0
  104. package/lib/acde/skills/acde/references/reference/manifest.md +92 -0
  105. package/lib/acde/skills/acde/references/reference/modules.md +586 -0
  106. package/lib/acde/skills/acde/references/reference/platforms.md +23 -0
  107. package/lib/acde/skills/acde/references/reference/service.md +47 -0
  108. package/lib/acde/skills/acde/references/reference/testing.md +265 -0
  109. package/lib/acde/skills/acde/references/reference/web.md +94 -0
  110. package/lib/acde/skills/acde/references/start/first-app.md +87 -0
  111. package/lib/acde/skills/acde/references/start/install.md +232 -0
  112. package/lib/acde/skills/acde/references/start/project.md +77 -0
  113. package/lib/acde/templates/app/acde.config.jsonc +27 -0
  114. package/lib/acde/templates/app/gitignore +2 -0
  115. package/lib/acde/templates/app/index.html +22 -0
  116. package/lib/acde/templates/app/package.json +5 -0
  117. package/lib/acde/templates/app/src/i18n.js +26 -0
  118. package/lib/acde/templates/app/src/locales/en.js +4 -0
  119. package/lib/acde/templates/app/src/locales/zh.js +4 -0
  120. package/lib/acde/templates/app/src/main.js +5 -0
  121. package/lib/acde/templates/backend/server.ts +7 -0
  122. package/lib/acde/templates/backend/src/main.js +6 -0
  123. package/lib/acde/templates/desktop/acde.config.jsonc +17 -0
  124. package/lib/acde/templates/desktop/gitignore +2 -0
  125. package/lib/acde/templates/desktop/index.html +19 -0
  126. package/lib/acde/templates/desktop/package.json +5 -0
  127. package/lib/acde/templates/desktop/src/desktop.js +16 -0
  128. package/lib/acde/templates/desktop/src/i18n.js +26 -0
  129. package/lib/acde/templates/desktop/src/locales/en.js +4 -0
  130. package/lib/acde/templates/desktop/src/locales/zh.js +4 -0
  131. package/lib/acde/templates/framework/acde.config.jsonc +26 -0
  132. package/lib/acde-api/LICENSE +5 -0
  133. package/lib/acde-api/config.schema.json +563 -0
  134. package/lib/acde-api/package.json +51 -0
  135. package/lib/acde-api/src/app.d.ts +36 -0
  136. package/lib/acde-api/src/frame.d.ts +15 -0
  137. package/lib/acde-api/src/frame.js +1 -0
  138. package/lib/acde-api/src/index.d.ts +11 -0
  139. package/lib/acde-api/src/index.js +1 -0
  140. package/lib/acde-api/src/modules/app-modules.d.ts +81 -0
  141. package/lib/acde-api/src/modules/modules.d.ts +113 -0
  142. package/lib/acde-api/src/modules/shell-modules.d.ts +32 -0
  143. package/lib/acde-api/src/runtime/level.d.ts +1 -0
  144. package/lib/acde-api/src/runtime/level.js +1 -0
  145. package/lib/acde-api/src/runtime/load.d.ts +8 -0
  146. package/lib/acde-api/src/runtime/load.js +1 -0
  147. package/lib/acde-api/src/runtime/places.d.ts +3 -0
  148. package/lib/acde-api/src/runtime/places.js +1 -0
  149. package/lib/acde-api/src/runtime/platforms.d.ts +5 -0
  150. package/lib/acde-api/src/runtime/platforms.js +1 -0
  151. package/lib/acde-api/src/shell.d.ts +14 -0
  152. package/lib/acde-api/src/types/capabilities.d.ts +34 -0
  153. package/lib/acde-api/src/types/config.d.ts +55 -0
  154. package/lib/acde-api/src/types/events.d.ts +151 -0
  155. package/lib/acde-api/src/types/manifest.d.ts +83 -0
  156. package/lib/acde-api/src/types/office.d.ts +118 -0
  157. package/lib/acde-api/src/types/page.d.ts +1090 -0
  158. package/lib/acde-api/src/types/permissions.d.ts +50 -0
  159. package/lib/acde-api/src/types/service.d.ts +35 -0
  160. package/lib/acde-api/src/validate.d.ts +64 -0
  161. package/lib/acde-api/src/validate.js +1 -0
  162. package/lib/acde-api/src/vite.d.ts +3 -0
  163. package/lib/acde-api/src/vite.js +9 -0
  164. package/package.json +11 -4
  165. package/README.md +0 -3
@@ -0,0 +1,147 @@
1
+ # Services
2
+
3
+ A desktop service is a JS module that starts and stops with the desktop and runs in the background. It exposes its methods as `acde:<namespace>` modules, one for each key of its `api` (usually just one, named after the service), callable by the desktop and by apps that list the service. It is a good home for global state and for logic that talks to external processes (an AI assistant or a sync service, for example).
4
+
5
+ ## Write a service
6
+
7
+ ```ts
8
+ // services/clock.ts
9
+ import type { ServiceFactory } from '@dopame/acde-api'
10
+
11
+ const clock: ServiceFactory = (ctx) => {
12
+ ctx.log('clock started')
13
+ return {
14
+ api: {
15
+ clock: {
16
+ now: () => Date.now(),
17
+ async uptime() {
18
+ return new Promise((resolve) => {
19
+ ctx.spawn(['uptime'], { onLine: resolve })
20
+ })
21
+ },
22
+ },
23
+ },
24
+ close() {
25
+ ctx.log('clock stopped')
26
+ },
27
+ }
28
+ }
29
+ export default clock
30
+ ```
31
+
32
+ Register it in the desktop's config, under the service's name:
33
+
34
+ ```jsonc
35
+ { "type": "desktop", "services": { "clock": "services/clock.ts" } }
36
+ ```
37
+
38
+ A service that uses shell modules is written as an object that also lists them; see [Shell modules in a service](#shell-modules-in-a-service).
39
+
40
+ ## Call it
41
+
42
+ The desktop imports it directly; an app must list the **service name** in its `services`:
43
+
44
+ ```jsonc
45
+ { "services": ["clock"] }
46
+ ```
47
+
48
+ ```js
49
+ import { clock } from 'acde:clock'
50
+
51
+ console.log(await clock.now())
52
+ ```
53
+
54
+ A service an app lists is required, and cannot be made optional: where the service is not running the app does not start (`Notes needs "clock" (service not running)`). That includes `acde dev` of the app project alone, which runs no desktop; run the app in the desktop that has the service: `acde dev ./my-app --desktop ./my-desktop`. Listing a service under `capabilities` instead is an error in `acde check` (`capability "clock" is not a system capability; list desktop services under services`).
55
+
56
+ Each key of `api` is a module name (a namespace), and its value is a set of methods. All calls are asynchronous (except the methods a service lists in `sync`, see [Service API](../reference/service.md)), and arguments and return values must be structured-cloneable; callback functions can be passed across.
57
+
58
+ Names:
59
+
60
+ - An app lists the **service's name**, and gets every namespace of that service: with `api: { clock: …, timer: … }` in the service `clock`, an app that lists `clock` imports both `acde:clock` and `acde:timer`. Never list a namespace: an app that lists `timer` requires a service of that name and does not start (`Notes needs "timer" (service not running)`).
61
+ - `acde check` and `acde build` cannot know a service's namespaces (they are what the factory returns at run time). An import of the service's own name passes silently. An import of any other name that is not a system module passes with a note in an app that lists a service, and the exit code stays 0:
62
+
63
+ ```text
64
+ note: src/main.js:1: app "notes": acde:timer resolves only if the service Notes lists ("clock") provides the "timer" namespace
65
+ ```
66
+
67
+ If no listed service provides it, the import throws a `TypeError` when the page loads (`'acde:timer' is not given to …`). In an app that lists no service, the same import is an error: `error: src/main.js:1: app "notes": acde:timer is not a system module; if a desktop service provides it, list that service's name in the services of Notes`. A service with one namespace, named after the service, gets no note.
68
+ - A service cannot have the name of a system module or capability (`fs`, `open`, `window`, `windows`, `apps`, `launch`, `desktop`, `session`, `capabilities`, `notifications`, ...): `acde check` says `service "fs" has the name of a system module (acde:fs); system module names are reserved`. A namespace with such a name is left out when the service starts, and the runtime logs a line that ends, word for word, `[clock] acde.fs is taken; left out` (it writes the namespace as `acde.fs`, with a dot).
69
+
70
+ ## ServiceContext
71
+
72
+ The `ctx` passed to the factory function:
73
+
74
+ | Field | What it is |
75
+ | --- | --- |
76
+ | `name` | The service name |
77
+ | `package` | The desktop package's directory |
78
+ | `state` | This service's own state directory (`$XDG_STATE_HOME/acde/<package name>/<service>`, the desktop package's `name` without `@` and with `/` as `-`), already created |
79
+ | `env` | Environment variables |
80
+ | `log(line)` | Writes a log line, prefixed with `[<service name>]` |
81
+ | `spawn(argv, options?)` | Starts a child process |
82
+ | `inspect` | The `acde:inspect` interface |
83
+ | `import(specifier)` | A shell module the desktop grants this service; see below |
84
+
85
+ `spawn` takes the options `{cwd, env, onLine(line), onExit(code, signal)}` and returns `{pid, write(text), end(), kill(signal?)}`. The child process runs in its own process group and its stderr goes to the log; when the service closes, it gets SIGTERM, then SIGKILL 3 seconds later.
86
+
87
+ A service should only use `ServiceContext` and standard Web APIs. Put heavy work in a process started with `spawn`.
88
+
89
+ ## Shell modules in a service
90
+
91
+ A service cannot `import` an `acde:` module; the build rejects it:
92
+
93
+ ```text
94
+ acde: service clock: services/clock.ts imports acde:windows: a service gets shell modules from ctx.import('acde:<name>'), listed in its modules in acde.config.jsonc ("services": { "clock": { "module": "<file>", "modules": ["<name>"] } })
95
+ ```
96
+
97
+ It asks its context for the shell modules the desktop grants it. Grant them in the desktop's config: write the service as an object with its module file and the shell modules it may use.
98
+
99
+ ```jsonc
100
+ {
101
+ "type": "desktop",
102
+ "services": {
103
+ "clock": { "module": "services/clock.ts", "modules": ["windows"] }
104
+ }
105
+ }
106
+ ```
107
+
108
+ ```ts
109
+ // services/clock.ts
110
+ import type { ServiceFactory } from '@dopame/acde-api'
111
+
112
+ const clock: ServiceFactory = async (ctx) => {
113
+ const { windows } = await ctx.import('acde:windows')
114
+ windows.on('open', (event) => ctx.log(`window ${event.window} opened`))
115
+ return {
116
+ api: {
117
+ clock: {
118
+ now: () => Date.now(),
119
+ windows: () => windows.list(),
120
+ },
121
+ },
122
+ }
123
+ }
124
+ export default clock
125
+ ```
126
+
127
+ With one app window open, `clock.windows()` answers `[{ "id": 1048576, "appId": "tools", "title": "Tools", "web": true, "state": "normal" }]`.
128
+
129
+ `modules` takes the names of shell modules. Anything else is an error in `acde check` and `acde build`:
130
+
131
+ ```text
132
+ error: services in acde.config.jsonc: service "clock": modules lists shell modules (windows, packages, session, desktops, inspect, desktop, setDefault, permissions)
133
+ ```
134
+
135
+ `acde build` writes the grant into the desktop's entry of the package manifest as `{ "name": "clock", "modules": ["windows"] }`; a handwritten package writes that entry itself (see [Manifest](../reference/manifest.md#desktopmanifest)).
136
+
137
+ What `ctx.import()` gives:
138
+
139
+ | Module | In a service |
140
+ | --- | --- |
141
+ | `acde:windows` | The whole module, as the desktop page has it: `list()`, the events, `requestState()`, and the rest |
142
+ | `acde:packages` | The whole module, with its `changed` event |
143
+ | `acde:session`, `acde:desktops`, `acde:inspect`, `acde:setDefault` | The whole module |
144
+ | `acde:desktop` | `apps()` only: a service has no page to pin panels from |
145
+ | `acde:permissions` | Not given: `acde:permissions is not available to services here` |
146
+
147
+ `ctx.inspect` needs no grant. `ctx.import()` rejects with a `TypeError` for a shell module the service is not granted (`acde:session is a shell module: list it in the modules of service "clock" in the desktop manifest`) and for anything that is not a shell module (`import(acde:fs): a service imports shell modules (acde:windows, acde:packages, …)`). A factory that lets the rejection through does not start its service; the runtime logs `[clock] not started: TypeError: …`.
@@ -0,0 +1,378 @@
1
+ # Testing
2
+
3
+ `acde test` tests your app in the real runtime: each test gets a fresh, isolated `acde dev` session, and a Playwright-like API finds elements, clicks, types, asserts, and compares screenshots. Requires Bun, even when the project itself runs on Node.js.
4
+
5
+ ## A first test
6
+
7
+ Test files are named `*.e2e.ts` (or `.tsx`, `.js`, `.mjs`, `.mts`) and can live anywhere in the project:
8
+
9
+ ```ts
10
+ // e2e/tasks.e2e.ts
11
+ import { test, expect } from 'acde/test'
12
+
13
+ test('adds a task', async ({ app }) => {
14
+ await app!.getByRole('textbox', { name: 'New task' }).fill('Buy milk')
15
+ await app!.getByRole('button', { name: 'Add' }).click()
16
+ await expect(app!.getByRole('listitem')).toHaveCount(1)
17
+ })
18
+
19
+ test('starts empty again', async ({ app }) => {
20
+ await expect(app!.getByRole('listitem')).toHaveCount(0)
21
+ })
22
+ ```
23
+
24
+ ```sh
25
+ acde test
26
+ ```
27
+
28
+ Plain `bun test` does not match `*.e2e.*`, so unit tests and end-to-end tests stay out of each other's way. `acde test` resolves `acde/test` and `acde/driver` to the acde that is running it, so the project does not need to install anything.
29
+
30
+ ## Fixtures
31
+
32
+ The test function receives:
33
+
34
+ | Name | What it is |
35
+ | --- | --- |
36
+ | `app` | For an app project, its window (`AppWindow`); for a desktop project, `null` |
37
+ | `acde` | The whole session (`Acde`): open apps, take screenshots, emulate, fake requests, and more |
38
+ | `home` | This test's temporary `HOME` |
39
+ | `settings` | The settings for this run |
40
+
41
+ ## Isolation
42
+
43
+ Each test gets:
44
+
45
+ - Temporary `HOME` and `XDG_*` directories, and in-memory storage only;
46
+ - A 1280 × 800 screen, light mode, and no autostart items;
47
+ - Headless by default (`--headed` to watch in a window), with a 60-second timeout per test (`--timeout`); the time the session takes to start counts. Headless still needs a GPU, so Linux CI machines without one can't run tests; see [Platform support](../reference/platforms.md).
48
+
49
+ Builds go into `.acde/test`, results into `.acde/test-results`.
50
+
51
+ After 0.0.9, a test's session does not count as the project's `acde dev`: an `acde dev` you run on the same project while tests run starts and keeps running, and commands such as `acde eval` and the MCP tools use yours, never a test's (only `--session <pid>` reaches a test's session).
52
+
53
+ ## Locators
54
+
55
+ The same locators as Playwright, matched against the **semantic tree** (the accessibility tree):
56
+
57
+ ```ts
58
+ app.getByRole('button', { name: 'Save' })
59
+ app.getByText('Saved')
60
+ app.getByLabel('Email')
61
+ app.getByPlaceholder('Search')
62
+ app.getByTitle('Close')
63
+ app.getByAltText('Avatar')
64
+ app.getByTestId('submit') // data-testid
65
+ app.locator('ul.tasks > li') // CSS selector
66
+ app.getByRole('listitem').filter({ hasText: 'Milk' }).first()
67
+ app.getByRole('listitem').filter({ has: app.getByRole('checkbox', { checked: true }) })
68
+ app.getByRole('button', { name: 'Save' }).or(app.getByRole('button', { name: 'Retry' }))
69
+ app.locator('li').and(app.getByText('Milk'))
70
+ ```
71
+
72
+ Locators are lazy: they are resolved again against the latest tree every time they are used. Actions require **exactly one** match; with several, they fail instead of guessing.
73
+
74
+ Adjacent text in one element is one name, as written: JSX `{rate}%` is `100%`. In 0.0.9 the tree puts a space between text nodes (`100 %`), so `getByText('100%')` finds nothing there; match `/100\s?%/` for 0.0.9.
75
+
76
+ After 0.0.9, an app run on its own shows its window menus on the title bar ([Windows](window.md#menus)), and they are in the tree: click the menu, then the item.
77
+
78
+ ```ts
79
+ await app.getByRole('menuitem', { name: 'File' }).click()
80
+ await app.getByRole('menuitem', { name: 'New' }).click()
81
+ await app.keyboard.press('Control+N') // an item's shortcut runs it too
82
+ ```
83
+
84
+ `filter` takes `hasText`, `hasNotText`, `has` and `hasNot` (another locator, looked for inside each element). `or` matches what either locator matches, in document order; `and` what both match.
85
+
86
+ A CSS locator (`locator`, `getByTestId`, `getByPlaceholder`, …) also matches elements that are not rendered, as in Playwright: `toHaveCount` counts them, `toBeVisible` / `toBeHidden` / `isVisible()` / `waitFor({ state })` tell whether an element is rendered, and actions wait until it is. Elements found through the semantic tree (`getByRole`, `getByText`, `getByLabel`) are always rendered ones.
87
+
88
+ ## Actions
89
+
90
+ ```ts
91
+ await locator.click()
92
+ await locator.dblclick()
93
+ await locator.hover()
94
+ await locator.fill('text') // replaces
95
+ await locator.type('text') // appends
96
+ await locator.clear()
97
+ await locator.press('ctrl+a backspace') // key names: see "Key names" in Debugging
98
+ await locator.selectOption('beta')
99
+ await locator.check() / uncheck() / setChecked(true)
100
+ await locator.dragTo(other)
101
+ await locator.scroll(400)
102
+ ```
103
+
104
+ `click`, `fill`, and similar actions wait for the element to be enabled (not `disabled`), and retry until the timeout when they fail. The keys `press` takes are listed under [Key names](tools.md#key-names).
105
+
106
+ `click`, `dblclick` and `hover` take `modifiers`, the keys held meanwhile: `Shift`, `Control`, `Alt`, `Meta`, or `ControlOrMeta`, which is `Control` on every system because pages see a Linux environment everywhere (see [Web platform support](../reference/web.md)).
107
+
108
+ ```ts
109
+ await locator.click({ modifiers: ['Shift'] })
110
+ const size = await locator.evaluate((element, unit) => element.getBoundingClientRect().width + unit, 'px')
111
+ ```
112
+
113
+ `locator.evaluate(fn, arg)` runs a function in the page with the element and one JSON argument, and returns its JSON result.
114
+
115
+ For input that is not aimed at one element, `app.mouse` and `app.keyboard` send raw events at page coordinates (CSS pixels, as `getBoundingClientRect()` gives them):
116
+
117
+ ```ts
118
+ await app.mouse.move(40, 120)
119
+ await app.mouse.down()
120
+ await app.mouse.move(240, 120, { steps: 8 }) // a drag, eight moves a frame apart
121
+ await app.mouse.up()
122
+ await app.mouse.click(300, 200, { button: 'right' })
123
+ await app.mouse.wheel(0, 400)
124
+
125
+ await app.keyboard.press('Control+a')
126
+ await app.keyboard.type('Hello') // key presses; characters without a key go in as text
127
+ await app.keyboard.insertText('你好') // as the input method commits it
128
+ await app.keyboard.down('Shift')
129
+ await app.keyboard.up('Shift')
130
+ ```
131
+
132
+ ## Assertions
133
+
134
+ ```ts
135
+ await expect(locator).toBeVisible()
136
+ await expect(locator).toHaveText('Done')
137
+ await expect(locator).toContainText('Done')
138
+ await expect(locator).toHaveValue('42')
139
+ await expect(locator).toHaveCount(3)
140
+ await expect(locator).toBeChecked()
141
+ await expect(locator).toBeDisabled()
142
+ await expect(locator).toBeFocused()
143
+ await expect(locator).toHaveAttribute('aria-expanded', 'true')
144
+ await expect(locator).not.toBeVisible()
145
+
146
+ await expect(locator).toHaveClass(/active/)
147
+ await expect(locator).toHaveCSS('color', 'rgb(255, 0, 0)') // the computed style
148
+ await expect(locator).toHaveJSProperty('checked', true)
149
+ await expect(locator).toBeEmpty()
150
+ await expect(locator).toBeEditable()
151
+ await expect(locator).toBeInViewport({ ratio: 0.5 })
152
+
153
+ await expect(app).toHaveTitle('Notes')
154
+ await expect(app).toBeOpen()
155
+ ```
156
+
157
+ `toHaveText` and `toContainText` compare the element's text content, as in Playwright: the text inside it, whitespace collapsed, not its accessible name. An `<output aria-label="Display">18</output>` has the text `18`; locators such as `getByText` and `getByRole(…, { name })` match the accessible name (`Display`). `locator.textContent()` reads the text content, `locator.name()` the accessible name.
158
+
159
+ UI assertions retry until the timeout (5 seconds by default). `expect` on any other value is the `expect` from `bun:test`.
160
+
161
+ `expect.poll` retries a `bun:test` matcher on a value the test reads itself, until it passes or the timeout runs out. `expect.soft` records a failure and lets the test go on; the test fails at its end with every soft failure listed.
162
+
163
+ ```ts
164
+ await expect.poll(() => app.evaluate<number>('window.saved'), { timeout: 2000 }).toBe(3)
165
+ await expect.soft(app.getByRole('heading')).toHaveText('Notes')
166
+ expect.soft(total).toBe(42)
167
+ ```
168
+
169
+ ## Screenshots
170
+
171
+ ```ts
172
+ await expect(app).toHaveScreenshot()
173
+ await expect(app.getByRole('textbox')).toHaveScreenshot('field', { maxDiffPixels: 20, mask: [app.getByTestId('clock')] })
174
+ ```
175
+
176
+ - Baselines live next to the test file in `__screenshots__/<file name>/<name>-<platform>.png`. If there is no baseline, one is written; if the `CI` environment variable is set, that is an error instead.
177
+ - Screenshots are taken until two in a row are identical, and only then compared; on a mismatch, it keeps retaking until the timeout.
178
+ - On failure, three images, `-actual`, `-expected`, and `-diff`, are written to the results directory.
179
+ - `acde test -u` rewrites the baselines.
180
+ - Options: `maxDiffPixels`, `maxDiffPixelRatio`, `threshold` (default 0.2), `mask`, `timeout`.
181
+
182
+ ## Network, file pickers, clipboard
183
+
184
+ ```ts
185
+ // Fake responses
186
+ await app.route('/api/todos', { json: [{ id: 1, title: 'mocked' }] })
187
+ await app.route('**/avatar.png', { abort: true })
188
+
189
+ // Wait for a request or a log entry (call first, then trigger)
190
+ const request = app.waitForRequest('/api/save', { method: 'POST' })
191
+ await app.getByRole('button', { name: 'Save' }).click()
192
+ await request
193
+
194
+ // Answer file pickers in advance (null: as if the user cancelled)
195
+ await acde.chooseFiles(['/tmp/a.txt'])
196
+ await acde.chooseFolder('/tmp/project')
197
+ await acde.chooseSaveFile('/tmp/out.md')
198
+ await app.getByRole('button', { name: 'Open folder' }).click()
199
+
200
+ // Dark mode
201
+ await acde.emulate({ colorScheme: 'dark' })
202
+
203
+ // Clipboard (headless sessions have their own)
204
+ await acde.setClipboard('hello')
205
+ ```
206
+
207
+ Tests run headless unless `acde test --headed`, and a headless session shows no picker dialog: a picker the page opens without an answer fails and the page sees it as cancelled. So call `chooseFolder`, `chooseFiles` or `chooseSaveFile` before the action that opens the picker; each answer serves the next picker of its kind.
208
+
209
+ To check a style or a box, evaluate in the page: `await app.locator('h1').evaluate((element) => getComputedStyle(element).color)`, or `getBoundingClientRect()` as in [Actions](#actions).
210
+
211
+ ## Time, dialogs and page setup
212
+
213
+ `app.clock` takes over the page's time, so a test of a timer does not wait for it:
214
+
215
+ ```ts
216
+ await app.clock.install({ time: '2026-01-01T09:00:00Z' }) // Date, performance.now, timers and animation frames
217
+ await app.clock.fastForward('25:00') // jump ahead; each timer that is due fires once
218
+ await app.clock.runFor(1000) // step ahead; every timer fires at its own time
219
+ await app.clock.pauseAt('2026-01-01T10:00:00Z')
220
+ await app.clock.resume()
221
+ await app.clock.setFixedTime(0) // Date stands still; timers go on
222
+ await app.clock.setSystemTime('2030-06-01') // moves Date only
223
+ ```
224
+
225
+ Times are milliseconds since 1970, a `Date` or a date string; durations are milliseconds, `"mm:ss"` or `"hh:mm:ss"`. An installed clock stays until the page reloads. CSS animations and transitions keep running on real time.
226
+
227
+ acdert has no dialog for the user to click: an `alert` is drawn in the window, `confirm` answers false and `prompt` null. A test answers the next one in advance:
228
+
229
+ ```ts
230
+ await app.answerDialog({ accept: true }) // the next confirm answers true
231
+ await app.answerDialog({ text: 'Untitled' }) // the next prompt answers this text
232
+ const shown = app.waitForDialog({ type: 'confirm' }) // call first, then trigger
233
+ await app.getByRole('button', { name: 'Delete' }).click()
234
+ expect((await shown).message).toBe('Delete this note?')
235
+ await app.dialogs() // every dialog the page showed, and how it was answered
236
+ ```
237
+
238
+ To start a test from a prepared page:
239
+
240
+ ```ts
241
+ test('opens the saved draft', { size: [480, 640], localStorage: { draft: 'Hello' } }, async ({ app }) => {
242
+ await expect(app!.getByRole('textbox')).toHaveValue('Hello')
243
+ })
244
+ ```
245
+
246
+ The options come second or third (`test(name, options, body)` or `test(name, body, options)`): `timeout`, `size` (the page's width and height) and `localStorage` (written before the page loads). They do what these calls do inside a test:
247
+
248
+ ```ts
249
+ await app.seedLocalStorage({ draft: 'Hello' }) // writes the entries, then reloads the page
250
+ await app.resize(480, 640) // the page's size, without the title bar
251
+ await app.addInitScript('window.testing = true') // runs before the page's own scripts, on every load
252
+ await app.reload()
253
+ ```
254
+
255
+ `size` and `localStorage` need the `app` fixture, so they work in an app project, not with `--desktop`.
256
+
257
+ ## Approvals
258
+
259
+ Declared capabilities need no answer. Only the camera and the microphone ask the user (see [Capabilities](capabilities.md#camera-and-microphone)): after 0.0.9, the first time the page calls `getUserMedia()`; in 0.0.9, when an app that declares one starts, so there the test answers before it uses the window. Every test starts with an empty home, so nothing is decided yet and no prompt is answered by itself: the call (in 0.0.9, the window) waits until the test answers. The example is for after 0.0.9:
260
+
261
+ ```ts
262
+ import { expect, test } from 'acde/test'
263
+
264
+ test('records once the user allows the microphone', async ({ app, acde }) => {
265
+ await app!.getByRole('button', { name: 'Record' }).click()
266
+ await acde.allow({ capability: 'microphone' })
267
+ await expect(app!.getByText('Recording')).toBeVisible()
268
+ })
269
+ ```
270
+
271
+ `acde.allow()` and `acde.deny()` wait for a matching prompt (`{ app, capability }`, both optional) and answer it; `acde.permissions()` lists what waits and what was decided; `acde.revoke(app, capability)` takes a decision back. To test how the app behaves when the user says no, `deny` the prompt: `getUserMedia()` then rejects with `NotAllowedError`.
272
+
273
+ ## Failures
274
+
275
+ A failed test leaves these in `.acde/test-results/<file>/<test>/`:
276
+
277
+ `error.txt` `output.log` `screen.png` `snapshot.txt` `console.txt` `network.txt` `trace.txt`
278
+
279
+ A test that runs out of time fails with `TimeoutError: the test did not finish within <ms> ms (its timeout: --timeout, or the one given to test())` and leaves the same files, taken at that moment.
280
+
281
+ A test whose session does not start has no screen to save: it leaves `error.txt` and `output.log`, the whole output of that test's `acde dev`. The error says how far the start got, then quotes the last 40 lines of that output (an app that cannot start says why there, for example `Notes needs "clock" (service not running)`):
282
+
283
+ | The error starts with | What happened |
284
+ | --- | --- |
285
+ | `acde dev exited (<code>) before it was ready:` | `acde dev` ended with that exit code: the build failed, or acdert refused the app |
286
+ | `acde dev (pid <pid>) did not register a session within <n> s in <dir>`, then `no session yet: its build did not finish, or acdert did not start listening; last output:` | The build or the start of acdert took longer than the test's timeout allows |
287
+ | `acde dev did not start within <n> s:`, then `acdert runs and answers, but no window appeared (pages: 0, windows: 0) and no approval prompt waits; last output:` | acdert runs, but the app opened no window |
288
+
289
+ With `--trace`, every test leaves a `trace.html`: each action and assertion, with its duration and result, and a full-screen screenshot after each step.
290
+
291
+ With `--video`, a failing test also leaves `video.png`, an animated PNG of the screen (four frames a second, the last 600 that differ); `--video=on` keeps one for every test. With `--retries`, the directory holds what the last attempt left.
292
+
293
+ ## Commands
294
+
295
+ ```sh
296
+ acde test # all *.e2e.* in the project
297
+ acde test e2e/tasks.e2e.ts # specific files
298
+ acde test -t "adds" # only tests whose names match this regular expression
299
+ acde test --headed # watch in a window
300
+ acde test --desktop dopame # run the app in the dopame desktop
301
+ acde test --bail --timeout 30000
302
+ acde test --trace
303
+ acde test -u # update screenshot baselines
304
+ acde test --workers 2 # two files at a time
305
+ acde test --retries 1 --video # run a failing test once more; keep a video of failing tests
306
+ ```
307
+
308
+ Several files run at once: half the machine's cores, at most 4, and one at a time with `--headed` or `--bail` (`--workers` sets it). Each worker builds into its own directory (`.acde/test`, `.acde/test-2`, …). A test that fails is run again `--retries` times, each time on a fresh session with its own timeout; when a later attempt passes, the test passes and a line says `passed on attempt 2 of 2 (flaky)`.
309
+
310
+ With `-t`, a file in which no test matches is skipped and does not count as failed (`acde test: e2e/approval.e2e.ts: no test matches -t "saves", skipped`); if no test matches in any file, `acde test` prints `acde test: no test matches -t "<pattern>" in <n> files` and exits with code 1.
311
+
312
+ Each file prints the output of its own `bun test` run as it goes; when several run at once, a file's output is printed in one piece when the file ends. The run ends with one summary line (after 0.0.9; 0.0.9 prints two, `acde test: 5 tests ran: 4 passed, 1 failed, …` and `acde test: 2 files, 1 passed, 1 failed: e2e/notes.e2e.ts`):
313
+
314
+ ```text
315
+ acde test: 5 tests in 2 files: 4 passed, 1 failed, 1 not run (skipped, todo, or not matched by -t); failed: e2e/notes.e2e.ts
316
+ what the failing tests left: /path/to/notes/.acde/test-results
317
+ ```
318
+
319
+ - It counts the tests of the files that ran: how many ran, in how many files, how many passed and failed, and, when there are any, how many did not run (`test.skip`, `test.todo`, and tests that `-t` left out). The numbers come from the report `bun test` writes for each file.
320
+ - `failed:` names the files with a failing test (a file passes when all its tests did). A file that left no report (it failed to load, or its run was killed) is named instead of counted: `; not counted: e2e/broken.e2e.ts (bun test left no report: the file did not load, or its run was killed)`. `; <n> files skipped (no test matches -t)` and `; <n> files not run (--bail)` are added when that applies. The indented line under it appears when a failing test left files.
321
+
322
+ The exit code is 0 when nothing failed and 1 otherwise. A project with no e2e test yet (a new one) is not a failure: `acde test` says so, points here, and exits with code 0.
323
+
324
+ ## Editor types
325
+
326
+ `acde/test` exports the types of everything it hands you, so helpers can be typed: `Fixtures` (the test body's argument), `AppWindow` (the `app` fixture), `Locator`, `Acde`, and the option and result types listed in the [Testing API](../reference/testing.md).
327
+
328
+ ```ts
329
+ import { test, expect, type AppWindow, type Locator } from 'acde/test'
330
+
331
+ async function enter(app: AppWindow, keys: string[]): Promise<Locator> {
332
+ for (const key of keys) await app.getByRole('button', { name: key }).click()
333
+ return app.locator('#display')
334
+ }
335
+
336
+ test('adds', async ({ app }) => {
337
+ await expect(await enter(app!, ['1', '+', '2', '='])).toHaveText('3')
338
+ })
339
+ ```
340
+
341
+ `acde test` resolves `acde/test` on its own at run time, but your editor does not know where it is. For now, add `paths` to the project's `tsconfig.json` pointing at the type declarations in the install directory, with the absolute path of your home directory (`tsconfig.json` does not expand `~` or `%USERPROFILE%`):
342
+
343
+ ```jsonc
344
+ {
345
+ "compilerOptions": {
346
+ "skipLibCheck": true,
347
+ "paths": {
348
+ "acde/test": ["/home/me/.acde/current/lib/acde/dist/types/e2e/index.d.ts"],
349
+ "acde/driver": ["/home/me/.acde/current/lib/acde/dist/types/driver/index.d.ts"]
350
+ // Windows: "C:/Users/me/.acde/current/lib/acde/dist/types/e2e/index.d.ts"
351
+ }
352
+ }
353
+ }
354
+ ```
355
+
356
+ `acde test` reads the same `paths` and replaces these two entries with its own, so they do not change how tests run. A separate types package will follow.
357
+
358
+ ## The driver alone
359
+
360
+ `acde/driver` also works in any Node.js or Bun script. Outside `acde test` nothing resolves `acde/driver` by itself: link the installed acde into the script's project once (it is a package named `acde`; types come with it):
361
+
362
+ ```sh
363
+ cd ~/.acde/current/lib/acde && bun link # npm link with Node.js
364
+ cd ~/my-scripts && bun link acde # npm link acde
365
+ ```
366
+
367
+ The link points at the version that is installed now; link it again after `acde` is upgraded.
368
+
369
+ ```ts
370
+ import { Acde } from 'acde/driver'
371
+
372
+ const acde = await Acde.launch({ dirs: ['./my-app'], headless: true })
373
+ await acde.open('notes')
374
+ await acde.window('notes').getByRole('button', { name: 'New' }).click()
375
+ await acde.close()
376
+ ```
377
+
378
+ `Acde.attach()` connects to an `acde dev` session that is already running. For the full API, see [Reference: Testing API](../reference/testing.md).