@live-change/frontend-template 0.9.205 → 0.9.207
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/.claude/rules/live-change-backend-actions-views-triggers.md +6 -0
- package/.claude/skills/live-change-design-actions-views-triggers/SKILL.md +6 -0
- package/.claude/skills/live-change-design-models-relations/SKILL.md +32 -4
- package/.claude/skills/live-change-frontend-editor-form/SKILL.md +25 -1
- package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +6 -0
- package/.cursor/rules/live-change-backend-models-and-relations.mdc +25 -1
- package/.cursor/rules/live-change-frontend-vue-primevue.mdc +4 -0
- package/.cursor/skills/live-change-design-actions-views-triggers.md +6 -0
- package/Dockerfile +4 -2
- package/e2e/client-session.test.ts +1 -2
- package/e2e/e2eSuite.ts +8 -12
- package/e2e/env.ts +10 -55
- package/e2e/homepage.test.ts +1 -2
- package/e2e/runner.ts +10 -0
- package/e2e/withBrowser.ts +4 -17
- package/front/src/components/NavBar.vue +1 -1
- package/package.json +57 -55
|
@@ -16,6 +16,12 @@ Use these rules when implementing actions, views, and triggers in LiveChange ser
|
|
|
16
16
|
- use indexes instead of full scans,
|
|
17
17
|
- return a meaningful result (ids, data, etc.).
|
|
18
18
|
|
|
19
|
+
### Validation and `if`
|
|
20
|
+
|
|
21
|
+
- Treat property-level `if` as the condition for both visibility and validation.
|
|
22
|
+
- Backend auto-validation skips validators for fields where `if` resolves to `false`.
|
|
23
|
+
- For conditional required fields, keep validators on the field and model the condition with `if`, instead of duplicating the same guard in multiple action handlers.
|
|
24
|
+
|
|
19
25
|
Example:
|
|
20
26
|
|
|
21
27
|
```js
|
|
@@ -35,6 +35,12 @@ Use this skill to design **actions, views, and triggers** in LiveChange services
|
|
|
35
35
|
- session keys,
|
|
36
36
|
- any data needed for the next step.
|
|
37
37
|
|
|
38
|
+
### Validation and `if`
|
|
39
|
+
|
|
40
|
+
- Define conditional fields with property-level `if` directly on the schema field.
|
|
41
|
+
- Runtime validation skips fields where `if` evaluates to `false` (frontend `validateData` and backend auto-validation).
|
|
42
|
+
- Keep the validator list (`validation`, `softValidation`) on the field itself, and avoid duplicating the same condition in ad-hoc `execute` guards unless business logic requires additional checks.
|
|
43
|
+
|
|
38
44
|
Example:
|
|
39
45
|
|
|
40
46
|
```js
|
|
@@ -20,10 +20,14 @@ For each new model, decide how it relates to the rest of the domain:
|
|
|
20
20
|
- **`userItem`** – the object belongs to the signed-in user (e.g. user’s device).
|
|
21
21
|
- **`itemOf`** – a list of children belonging to a parent model (e.g. device connections).
|
|
22
22
|
- **`propertyOf`** – a single state object with the same id as the parent (e.g. cursor state).
|
|
23
|
+
- **`entity`** – global / role-scoped; do **not** pair with manual **`owner`** when **`userItem`** fits “my rows” for `client.user`.
|
|
24
|
+
- **`sessionOrUserItem`** / **`sessionOrUserProperty`** – owner is Session **or** User (no **`sessionItem`** annotation).
|
|
23
25
|
- **no relation** – for global data or other special cases.
|
|
24
26
|
|
|
25
27
|
Choose one main relation; other associations can be plain fields + indexes.
|
|
26
28
|
|
|
29
|
+
Decision guide: **`docs/docs/server/09-07-owner-selection-useritem.md`**.
|
|
30
|
+
|
|
27
31
|
## Step 2 – Define `properties` clearly
|
|
28
32
|
|
|
29
33
|
1. Use a **multi-line** style for properties, with clear `type`, `default`, `validation`, etc.
|
|
@@ -62,6 +66,30 @@ properties: {
|
|
|
62
66
|
}
|
|
63
67
|
```
|
|
64
68
|
|
|
69
|
+
## Property validation (validators)
|
|
70
|
+
|
|
71
|
+
- Built-in validator names live in `@live-change/framework/lib/utils/validators.js`. Do not invent names that are not there unless you also register them.
|
|
72
|
+
- Common patterns: `validation: ['nonEmpty']`, strings with `{ name: 'maxLength', length: 80 }`, numbers with `['number', 'integer', { name: 'min', value: 0 }, { name: 'max', value: 999 }]`.
|
|
73
|
+
- **Service-defined validators:** `definition.validator('email', factory)` (see `email-service/index.js` + `emailValidator.js`). Validators are merged across services at startup — avoid name clashes.
|
|
74
|
+
- **Frontend:** assign client factories to `api.validators` under the same keys (e.g. `clientEmailValidator.js` in `App.vue`). Match server error codes for i18n.
|
|
75
|
+
- Full reference: server manual page **Property validation** (`docs/docs/server/05a-validation.md`).
|
|
76
|
+
|
|
77
|
+
## Generated CRUD / views (relations-plugin)
|
|
78
|
+
|
|
79
|
+
- The plugin registers **views, actions, events, and triggers** automatically from `entity`, `itemOf`, `propertyOf`, `*Any`, `relatedTo`, `boundTo`, and `saveAuthor` (see `live-change-stack/framework/relations-plugin/src/index.ts`).
|
|
80
|
+
- **Do not** define a manual `definition.view` / `action` / `event` / `trigger` with the **same name** as a generated one (e.g. `entity` on model `Auction` already creates view **`auction`**).
|
|
81
|
+
- Use **`describe`** before adding custom surface API: `fnm exec -- node server/start.js describe --service myService --output yaml`.
|
|
82
|
+
- Technical inventory: **`docs/docs/server/09-00-relations-generated-artifacts.md`** (built docs path `/server/09-00-relations-generated-artifacts.html`).
|
|
83
|
+
|
|
84
|
+
## Owner selection — `userItem`, `entity`, domain relations
|
|
85
|
+
|
|
86
|
+
- **Per logged-in user** (“my X”, owner = `client.user`): use **`userItem`** (or **`userProperty`** for one row per user) with **`use: [ userService, … ]`**. Do **not** hand-declare **`user`** or **`owner`** — user-service injects **`user`** + **`byUser`**. Configure access with **`userReadAccess`**, **`userCreateAccess`**, **`userUpdateAccess`**, **`userDeleteAccess`**, **`userWriteAccess`** (see `live-change-stack/services/user-service/userItem.js`).
|
|
87
|
+
- **Session or User** (guest drafts → sign-in transfer): **`sessionOrUserItem`** / **`sessionOrUserProperty`**. There is no **`sessionItem`** annotation.
|
|
88
|
+
- **Global / role-scoped** catalog entities without a natural “this row belongs to client.user”: often **`entity`**.
|
|
89
|
+
- **Child of a domain parent model** (invoice lines, comments, …): **`itemOf`** / **`propertyOf`** / `*Any`.
|
|
90
|
+
- Typical **`userItem`** API names: **`myUser` + plural(Model)** (range), **`createMyUser` + Model**, **`updateMyUser` + Model**. Relations-plugin may also expose **`create` + Model** from underlying **`itemOf`** — prefer **`createMyUser*`** for consistent **`user`** stamping.
|
|
91
|
+
- Full guide: **`docs/docs/server/09-07-owner-selection-useritem.md`**.
|
|
92
|
+
|
|
65
93
|
## Step 2b – Relation arity rules (critical)
|
|
66
94
|
|
|
67
95
|
Treat arity on two levels:
|
|
@@ -86,13 +114,13 @@ Guardrail:
|
|
|
86
114
|
### `userItem`
|
|
87
115
|
|
|
88
116
|
1. Add a `userItem` block inside the model definition.
|
|
89
|
-
2. Set
|
|
117
|
+
2. Set **`userReadAccess`**, **`userCreateAccess`**, **`userUpdateAccess`**, **`userDeleteAccess`**, or **`userWriteAccess`** (see `live-change-stack/services/user-service/userItem.js`). Do **not** declare **`user`** in `properties`.
|
|
90
118
|
|
|
91
119
|
```js
|
|
92
120
|
userItem: {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
121
|
+
userReadAccess: (params, context) => !!context.client?.user,
|
|
122
|
+
userWriteAccess: (params, context) => !!context.client?.user,
|
|
123
|
+
writableProperties: ['name']
|
|
96
124
|
}
|
|
97
125
|
```
|
|
98
126
|
|
|
@@ -51,9 +51,30 @@ identifiers: {} // creates a new record
|
|
|
51
51
|
|
|
52
52
|
This is the simplest and most readable approach. Use it when identifiers are available at setup time (static values, route params).
|
|
53
53
|
|
|
54
|
+
### `crudSource`, `ownerCrud`, and `allowReadWithoutIdentifiers`
|
|
55
|
+
|
|
56
|
+
Models from **user-service** relations (`userProperty`, `userItem`, `sessionOrUserProperty`, `sessionOrUserItem`, …) expose a second CRUD map on the model: **`ownerCrud`** (see `describe`: `read` is usually **`my…`**, actions are **`setMy…` / `updateMy…` / `setOrUpdateMy…`**). Use:
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
const editor = await editorData({
|
|
60
|
+
service: 'userIdentification',
|
|
61
|
+
model: 'Identification',
|
|
62
|
+
crudSource: 'ownerCrud',
|
|
63
|
+
identifiers: {},
|
|
64
|
+
allowReadWithoutIdentifiers: true,
|
|
65
|
+
draft: true,
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**`allowReadWithoutIdentifiers`** (default `false`): when `true`, `editorData` still subscribes to the **read** view even if `identifiers` is `{}`. Use only when the read view is defined for the current client with no path params (typical `ownerCrud.read`). Keep `false` for normal **`crud.read`** that needs real ids — otherwise you risk unnecessary not-authorized / missing errors.
|
|
70
|
+
|
|
71
|
+
For **`crud`** (default `crudSource: 'crud'`) with a global read view, pass real **`identifiers`** as before.
|
|
72
|
+
|
|
54
73
|
## Step 2 – Build the template with AutoField
|
|
55
74
|
|
|
56
|
-
Use `editor.model.properties.*` as definitions and `editor.data.value` for v-model bindings
|
|
75
|
+
Use `editor.model.properties.*` as definitions and `editor.data.value` for v-model bindings.
|
|
76
|
+
|
|
77
|
+
**Model `if` on properties:** When a property uses `definition.if` (visibility depends on other fields, e.g. `props.portalRole === 'employee'`), pass **`:root-value="editor.data.value"`** on every `AutoField` that relies on that definition. The default `rootValue` is `{}`, so the predicate never sees sibling fields and `if`-gated fields stay hidden. Same rule as `AutoEditor`’s `rootValue` / `ModelEditor`’s `auto-editor` wiring.
|
|
57
78
|
|
|
58
79
|
**Important:** Always wrap in a `<form>` element. `EditorButtons` uses `type="submit"` / `type="reset"` internally — without a parent `<form>`, the buttons do nothing.
|
|
59
80
|
|
|
@@ -62,12 +83,14 @@ Use `editor.model.properties.*` as definitions and `editor.data.value` for v-mod
|
|
|
62
83
|
<form @submit.prevent="editor.save()" @reset.prevent="editor.reset()">
|
|
63
84
|
<div class="space-y-4">
|
|
64
85
|
<AutoField
|
|
86
|
+
:root-value="editor.data.value"
|
|
65
87
|
:definition="editor.model.properties.title"
|
|
66
88
|
v-model="editor.data.value.title"
|
|
67
89
|
:error="editor.propertiesErrors?.title"
|
|
68
90
|
label="Title"
|
|
69
91
|
/>
|
|
70
92
|
<AutoField
|
|
93
|
+
:root-value="editor.data.value"
|
|
71
94
|
:definition="editor.model.properties.body"
|
|
72
95
|
v-model="editor.data.value.body"
|
|
73
96
|
:error="editor.propertiesErrors?.body"
|
|
@@ -76,6 +99,7 @@ Use `editor.model.properties.*` as definitions and `editor.data.value` for v-mod
|
|
|
76
99
|
|
|
77
100
|
<!-- Custom input inside AutoField — gets label + error automatically -->
|
|
78
101
|
<AutoField
|
|
102
|
+
:root-value="editor.data.value"
|
|
79
103
|
:definition="editor.model.properties.category"
|
|
80
104
|
v-model="editor.data.value.category"
|
|
81
105
|
:error="editor.propertiesErrors?.category"
|
|
@@ -15,6 +15,12 @@ alwaysApply: false
|
|
|
15
15
|
- używać indeksów zamiast pełnych skanów,
|
|
16
16
|
- zwracać sensowny wynik (ID obiektu, fragment stanu itp.).
|
|
17
17
|
|
|
18
|
+
### Walidacja i `if`
|
|
19
|
+
|
|
20
|
+
- Traktuj `if` na property jako warunek widoczności i walidacji.
|
|
21
|
+
- Backendowe auto-validation pomija walidatory pól, dla których `if` zwraca `false`.
|
|
22
|
+
- Dla warunkowego required trzymaj walidatory na polu i modeluj warunek przez `if`, zamiast ręcznie dublować guardy w wielu akcjach.
|
|
23
|
+
|
|
18
24
|
### Wzorzec prostej akcji
|
|
19
25
|
|
|
20
26
|
```js
|
|
@@ -36,6 +36,30 @@ properties: {
|
|
|
36
36
|
}
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## Walidacja (`validation` / `softValidation`)
|
|
40
|
+
|
|
41
|
+
- Używaj **tylko nazw walidatorów**, które istnieją w frameworku albo zostały zarejestrowane w serwisie — pełna lista i przykłady: dokumentacja **Property validation** (`live-change-stack/docs/docs/server/05a-validation.md`, po zbudowaniu docs: `/server/05a-validation.html`).
|
|
42
|
+
- Wbudowane m.in.: `nonEmpty`, `minLength`, `maxLength`, `number`, `integer`, **`min`** / **`max`** z obiektem `{ name: 'min', value: 0 }` (granica liczbowa; często łańcuch `['number', { name: 'min', value: 0 }]`).
|
|
43
|
+
- Własny walidator w serwisie: `definition.validator('email', fabryka)` (np. wzorzec z `email-service` / `password-authentication-service`). Nazwy są kopiowane między serwisami przy starcie — unikaj kolizji (pierwsza rejestracja wygrywa).
|
|
44
|
+
- Na froncie dopnij ten sam klucz na `api.validators` (np. import `clientEmailValidator.js` i `api.validators.email = emailValidator` w `App.vue`), żeby `validateData` / auto-form znały reguły po stronie klienta.
|
|
45
|
+
|
|
46
|
+
## Relations plugin — co jest generowane (kolizje nazw)
|
|
47
|
+
|
|
48
|
+
- `@live-change/relations-plugin` sam rejestruje **widoki, akcje, eventy i triggery** według wzorców z `live-change-stack/framework/relations-plugin/src/` (kolejność procesorów: `entity` → `propertyOf`/`itemOf` → … → `saveAuthor`).
|
|
49
|
+
- **Nie** dodawaj ręcznie `definition.view` / `action` / `event` / `trigger` o nazwie już zajętej przez generator (np. model `Auction` z `entity` ma już widok **`auction`** — duplikat kończy się błędem `view auction already exists`).
|
|
50
|
+
- Przed własną rejestracją: `fnm exec -- node server/start.js describe --service … --output yaml`.
|
|
51
|
+
- Inwentarz techniczny: `live-change-stack/docs/docs/server/09-00-relations-generated-artifacts.md` (po buildzie docs: `/server/09-00-relations-generated-artifacts.html`).
|
|
52
|
+
|
|
53
|
+
## Wybór właściciela — `userItem` vs `entity` vs relacje domenowe
|
|
54
|
+
|
|
55
|
+
- Lista „moich” rzeczy dla **zalogowanego User** (tworzenie/edycja pod **`client.user`**) → **`userItem`** lub **`userProperty`** + **`use: [ userService, … ]`**. **Nie** dodawaj ręcznego pola **`user`** ani **`owner`** — dodaje je procesor (`live-change-stack/services/user-service/userItem.js`). Dostęp ustawiaj przez **`userReadAccess`**, **`userCreateAccess`**, **`userUpdateAccess`**, **`userDeleteAccess`**, **`userWriteAccess`** — nie wymyślaj nazwy „ownerAccessControl”.
|
|
56
|
+
- Owner **Session lub User** (draft przed loginem → transfer) → **`sessionOrUserItem`** / **`sessionOrUserProperty`** (09-04). Nie ma adnotacji **`sessionItem`**.
|
|
57
|
+
- Encja **globalna** / dostęp głównie po **rolach**, bez naturalnego „ten rekord = ten user” → często **`entity`** (05-models).
|
|
58
|
+
- Dziecko **konkretnego modelu domenowego** (nie „User jako kontener konta”) → **`itemOf`** / **`propertyOf`** / `*Any` (09-01 / 09-02).
|
|
59
|
+
- Wygenerowane nazwy dla **`userItem`** (np. `myUserAuctions`, `createMyUserAuction`): patrz **`09-07-owner-selection-useritem.md`** w docs serwera.
|
|
60
|
+
|
|
61
|
+
Procesory **user-service** (m.in. `userProperty`, `userItem`, `sessionOrUserProperty`, `sessionOrUserItem`) ustawiają na modelu mapę **`ownerCrud`** obok ewentualnego globalnego **`crud`** — w `describe` widać oba bloki. Na froncie formularze „moje” buduje się przez **`editorData({ crudSource: 'ownerCrud', … })`**; gdy widok **`my…`** nie bierze parametrów ścieżki, często **`identifiers: {}`** + **`allowReadWithoutIdentifiers: true`**. Szczegóły: `live-change-stack/docs/docs/frontend/09-api-frontend-auto-form.md`.
|
|
62
|
+
|
|
39
63
|
## Arity relacji (bardzo ważne)
|
|
40
64
|
|
|
41
65
|
Rozróżniaj dwa poziomy:
|
|
@@ -61,7 +85,7 @@ Przykłady:
|
|
|
61
85
|
|
|
62
86
|
## `userItem` – zasób należący do użytkownika
|
|
63
87
|
|
|
64
|
-
Używaj, gdy model należy do zalogowanego użytkownika.
|
|
88
|
+
Używaj, gdy model należy do zalogowanego użytkownika (preferuj **userItem** zamiast **`entity`** + ręcznego **`owner`** dla typowych list „moich” obiektów — szczegóły: `docs/docs/server/09-07-owner-selection-useritem.md`).
|
|
65
89
|
|
|
66
90
|
```js
|
|
67
91
|
definition.model({
|
|
@@ -66,6 +66,10 @@ Schemat decyzji:
|
|
|
66
66
|
2. Edytuje rekord modelu (create/update)? → **Tak**: użyj `editorData`. **Nie**: użyj `actionData`.
|
|
67
67
|
3. `<command-form>` tylko do najprostszych jednorazowych przypadków.
|
|
68
68
|
|
|
69
|
+
**`editorData` — `crud` vs `ownerCrud`:** domyślnie używane jest `model.crud`. Modele z relacji user-service mają często też **`model.ownerCrud`** (`describe`): wtedy `editorData({ crudSource: 'ownerCrud', … })`. Gdy widok read typu **`my…`** nie ma parametrów w ścieżce, a przekazujesz `identifiers: {}`, ustaw **`allowReadWithoutIdentifiers: true`** (inaczej `editorData` nie podłączy `live` zapisanej wartości). Szczegóły: `live-change-stack/docs/docs/frontend/09-api-frontend-auto-form.md`.
|
|
70
|
+
|
|
71
|
+
**`AutoField` i `definition.if`:** pola z warunkiem `if` w definicji modelu oceniają widoczność względem obiektu przekazanego jako **`rootValue`**. Bez **`:root-value="editor.data.value"`** (lub równoważnego całego rekordu) domyślne `{}` ukrywa te pola mimo poprawnego `v-model`.
|
|
72
|
+
|
|
69
73
|
## Autosave helpery – `synchronized` i `synchronizedList`
|
|
70
74
|
|
|
71
75
|
Używaj helperów dla danych z `live(...)`:
|
|
@@ -35,6 +35,12 @@ Use this skill to design **actions, views, and triggers** in LiveChange services
|
|
|
35
35
|
- session keys,
|
|
36
36
|
- any data needed for the next step.
|
|
37
37
|
|
|
38
|
+
### Validation and `if`
|
|
39
|
+
|
|
40
|
+
- Define conditional fields with property-level `if` directly on the schema field.
|
|
41
|
+
- Runtime validation skips fields where `if` evaluates to `false` (frontend `validateData` and backend auto-validation).
|
|
42
|
+
- Keep the validator list (`validation`, `softValidation`) on the field itself, and avoid duplicating the same condition in ad-hoc `execute` guards unless business logic requires additional checks.
|
|
43
|
+
|
|
38
44
|
Example:
|
|
39
45
|
|
|
40
46
|
```js
|
package/Dockerfile
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
FROM --platform=amd64 debian:
|
|
1
|
+
FROM --platform=amd64 debian:bookworm
|
|
2
2
|
|
|
3
3
|
# SYSTEM
|
|
4
4
|
RUN echo no cache 3
|
|
@@ -21,6 +21,8 @@ RUN apt-get install -y nodejs
|
|
|
21
21
|
#NPM, PM2
|
|
22
22
|
RUN npm install cross-env yarn typescript -g
|
|
23
23
|
|
|
24
|
+
RUN echo no cache 1
|
|
25
|
+
|
|
24
26
|
# APP
|
|
25
27
|
RUN mkdir -p /app
|
|
26
28
|
WORKDIR /app
|
|
@@ -49,4 +51,4 @@ COPY docker/start-service.sh /start-service.sh
|
|
|
49
51
|
COPY docker/app.initd.sh /etc/init.d/app
|
|
50
52
|
|
|
51
53
|
#CMD /start-service.sh
|
|
52
|
-
CMD node --inspect=0.0.0.0:9229 --
|
|
54
|
+
CMD node --inspect=0.0.0.0:9229 --expose_gc dist/server/start.js ssrServer --withApi --withServices --updateServices --enableSessions --createDb
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import test from 'node:test'
|
|
2
1
|
import assert from 'node:assert'
|
|
2
|
+
import { e2eSuite, test } from '@live-change/e2e-test'
|
|
3
3
|
import { withBrowser } from './withBrowser.js'
|
|
4
|
-
import { e2eSuite } from './e2eSuite.js'
|
|
5
4
|
|
|
6
5
|
e2eSuite('client-session', () => {
|
|
7
6
|
test('frontend initializes api client session', async () => {
|
package/e2e/e2eSuite.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
})
|
|
10
|
-
define()
|
|
11
|
-
})
|
|
12
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
e2eSuite,
|
|
3
|
+
getE2ERegistry,
|
|
4
|
+
resetE2ERegistry,
|
|
5
|
+
setCurrentE2EFile,
|
|
6
|
+
test,
|
|
7
|
+
type E2ETestDefinition
|
|
8
|
+
} from '@live-change/e2e-test'
|
package/e2e/env.ts
CHANGED
|
@@ -1,26 +1,10 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
2
|
import { fileURLToPath } from 'url'
|
|
3
3
|
import { TestServer } from '@live-change/server'
|
|
4
|
+
import { createTestEnvHelpers, waitForServerReady } from '@live-change/e2e-test'
|
|
4
5
|
import appConfig from '../server/app.config.js'
|
|
5
6
|
import * as services from '../server/services.list.js'
|
|
6
7
|
|
|
7
|
-
const READY_TIMEOUT_MS = 60000
|
|
8
|
-
const READY_POLL_MS = 2000
|
|
9
|
-
|
|
10
|
-
async function waitForServerReady(url: string): Promise<void> {
|
|
11
|
-
const deadline = Date.now() + READY_TIMEOUT_MS
|
|
12
|
-
while (Date.now() < deadline) {
|
|
13
|
-
try {
|
|
14
|
-
const res = await fetch(url)
|
|
15
|
-
if (res.ok) return
|
|
16
|
-
} catch {
|
|
17
|
-
// not ready yet
|
|
18
|
-
}
|
|
19
|
-
await new Promise((r) => setTimeout(r, READY_POLL_MS))
|
|
20
|
-
}
|
|
21
|
-
throw new Error(`Server at ${url} did not become ready within ${READY_TIMEOUT_MS}ms`)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
25
9
|
const serverDir = path.join(__dirname, '..', 'server')
|
|
26
10
|
const frontDir = path.join(__dirname, '..', 'front')
|
|
@@ -49,22 +33,10 @@ let testServer: TestServerInstance | null = null
|
|
|
49
33
|
|
|
50
34
|
export async function disposeTestEnv(): Promise<void> {
|
|
51
35
|
const s = testServer
|
|
36
|
+
if (!s) return
|
|
52
37
|
testServer = null
|
|
53
38
|
envPromise = null
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function haveService(server: TestServerInstance, name: string) {
|
|
58
|
-
const service = server.apiServer.services.services.find((s: { name: string }) => s.name === name)
|
|
59
|
-
if (!service) throw new Error('service ' + name + ' not found')
|
|
60
|
-
return service
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function haveModel(server: TestServerInstance, serviceName: string, modelName: string) {
|
|
64
|
-
const service = haveService(server, serviceName)
|
|
65
|
-
const model = service.models[modelName]
|
|
66
|
-
if (!model) throw new Error('model ' + modelName + ' not found')
|
|
67
|
-
return model
|
|
39
|
+
await s.dispose()
|
|
68
40
|
}
|
|
69
41
|
|
|
70
42
|
export async function getTestEnv(): Promise<TestEnv> {
|
|
@@ -97,33 +69,16 @@ export async function getTestEnv(): Promise<TestEnv> {
|
|
|
97
69
|
})
|
|
98
70
|
|
|
99
71
|
const url = server.url!
|
|
72
|
+
const helpers = createTestEnvHelpers(server)
|
|
100
73
|
return {
|
|
101
74
|
server,
|
|
102
75
|
url,
|
|
103
|
-
haveService:
|
|
104
|
-
haveModel:
|
|
105
|
-
haveView:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return view
|
|
110
|
-
},
|
|
111
|
-
haveAction: (serviceName: string, actionName: string) => {
|
|
112
|
-
const service = haveService(server, serviceName)
|
|
113
|
-
const action = service.actions[actionName]
|
|
114
|
-
if (!action) throw new Error('action ' + actionName + ' not found')
|
|
115
|
-
return action
|
|
116
|
-
},
|
|
117
|
-
haveTrigger: (serviceName: string, triggerName: string) => {
|
|
118
|
-
const service = haveService(server, serviceName)
|
|
119
|
-
const trigger = service.triggers[triggerName]
|
|
120
|
-
if (!trigger) throw new Error('trigger ' + triggerName + ' not found')
|
|
121
|
-
return trigger
|
|
122
|
-
},
|
|
123
|
-
grabObject: async (serviceName: string, modelName: string, id: string) => {
|
|
124
|
-
const model = haveModel(server, serviceName, modelName)
|
|
125
|
-
return await model.get(id)
|
|
126
|
-
}
|
|
76
|
+
haveService: helpers.haveService,
|
|
77
|
+
haveModel: helpers.haveModel,
|
|
78
|
+
haveView: helpers.haveView,
|
|
79
|
+
haveAction: helpers.haveAction,
|
|
80
|
+
haveTrigger: helpers.haveTrigger,
|
|
81
|
+
grabObject: helpers.grabObject
|
|
127
82
|
}
|
|
128
83
|
})()
|
|
129
84
|
return envPromise
|
package/e2e/homepage.test.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import test from 'node:test'
|
|
2
1
|
import assert from 'node:assert'
|
|
2
|
+
import { e2eSuite, test } from '@live-change/e2e-test'
|
|
3
3
|
import { withBrowser } from './withBrowser.js'
|
|
4
|
-
import { e2eSuite } from './e2eSuite.js'
|
|
5
4
|
|
|
6
5
|
e2eSuite('homepage', () => {
|
|
7
6
|
test('homepage responds and renders html', async () => {
|
package/e2e/runner.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createRunner } from '@live-change/e2e-test'
|
|
2
|
+
import { disposeTestEnv, getTestEnv } from './env.js'
|
|
3
|
+
|
|
4
|
+
const runner = createRunner({
|
|
5
|
+
setupEnv: getTestEnv,
|
|
6
|
+
teardownEnv: disposeTestEnv
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export const runE2E = runner.runE2E
|
|
10
|
+
await runner.runCli(import.meta.url, process.argv.slice(2))
|
package/e2e/withBrowser.ts
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import { createWithBrowser } from '@live-change/e2e-test'
|
|
2
|
+
import { getTestEnv } from './env.js'
|
|
3
|
+
import type { TestEnv } from './env.js'
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
fn: (page: Page, env: TestEnv) => Promise<void>
|
|
7
|
-
): Promise<void> {
|
|
8
|
-
const env = await getTestEnv()
|
|
9
|
-
const browser = await chromium.launch({ headless: process.env.SHOW_BROWSER ? false : true })
|
|
10
|
-
const context = await browser.newContext()
|
|
11
|
-
const page = await context.newPage()
|
|
12
|
-
try {
|
|
13
|
-
await fn(page, env)
|
|
14
|
-
} finally {
|
|
15
|
-
await context.close()
|
|
16
|
-
await browser.close()
|
|
17
|
-
}
|
|
18
|
-
}
|
|
5
|
+
export const withBrowser = createWithBrowser<TestEnv>(getTestEnv)
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
|
|
66
66
|
<NotificationsIcon v-if="client.user" />
|
|
67
67
|
|
|
68
|
-
<UserIcon v-if="client.user" />
|
|
68
|
+
<UserIcon v-if="client.user" :menuStyle="{ right: '5px' }" />
|
|
69
69
|
|
|
70
70
|
<a v-ripple class="cursor-pointer flex items-center justify-content-center no-underline lg:hidden text-surface-700 dark:text-surface-100 p-ripple
|
|
71
71
|
ml-2 hover:bg-surface-100 dark:hover:bg-surface-700 p-2"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@live-change/frontend-template",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.207",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"memDev": "tsx --inspect --expose-gc server/start.js memDev --enableSessions --initScript ./init.js --dbAccess",
|
|
6
6
|
"localDevInit": "tsx server/start.js localDev --enableSessions --initScript ./init.js --dbAccess",
|
|
@@ -39,64 +39,66 @@
|
|
|
39
39
|
"prerenderTsx": "cross-env NODE_ENV=production tsx server/start.js prerender --enableSessions",
|
|
40
40
|
"prerenderMemTsx": "cross-env NODE_ENV=production tsx server/start.js prerender --enableSessions --withDb --dbBackend mem --createDb --withApi --withServices --updateServices",
|
|
41
41
|
"prerenderLocalTsx": "cross-env NODE_ENV=production tsx server/start.js prerender --enableSessions --withDb --createDb --withApi --withServices --updateServices",
|
|
42
|
-
"e2e": "
|
|
43
|
-
"e2e:headed": "SHOW_BROWSER=1
|
|
42
|
+
"e2e": "fnm exec -- node --import tsx e2e/runner.ts",
|
|
43
|
+
"e2e:headed": "SHOW_BROWSER=1 fnm exec -- node --import tsx e2e/runner.ts",
|
|
44
|
+
"e2e:file": "fnm exec -- node --import tsx e2e/runner.ts"
|
|
44
45
|
},
|
|
45
46
|
"type": "module",
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@codemirror/language": "6.12.3",
|
|
48
49
|
"@dotenvx/dotenvx": "0.27.0",
|
|
49
50
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
|
50
|
-
"@live-change/access-control-frontend": "^0.9.
|
|
51
|
-
"@live-change/access-control-service": "^0.9.
|
|
52
|
-
"@live-change/agreement-service": "^0.9.
|
|
53
|
-
"@live-change/backup-service": "^0.9.
|
|
54
|
-
"@live-change/blog-frontend": "^0.9.
|
|
55
|
-
"@live-change/blog-service": "^0.9.
|
|
56
|
-
"@live-change/cli": "^0.9.
|
|
57
|
-
"@live-change/content-frontend": "^0.9.
|
|
58
|
-
"@live-change/content-service": "^0.9.
|
|
59
|
-
"@live-change/cron-service": "^0.9.
|
|
60
|
-
"@live-change/dao": "^0.9.
|
|
61
|
-
"@live-change/dao-vue3": "^0.9.
|
|
62
|
-
"@live-change/dao-websocket": "^0.9.
|
|
63
|
-
"@live-change/db-client": "^0.9.
|
|
64
|
-
"@live-change/draft-service": "^0.9.
|
|
65
|
-
"@live-change/
|
|
66
|
-
"@live-change/
|
|
67
|
-
"@live-change/
|
|
68
|
-
"@live-change/frontend-
|
|
69
|
-
"@live-change/
|
|
70
|
-
"@live-change/
|
|
71
|
-
"@live-change/
|
|
72
|
-
"@live-change/
|
|
73
|
-
"@live-change/
|
|
74
|
-
"@live-change/
|
|
75
|
-
"@live-change/
|
|
76
|
-
"@live-change/
|
|
77
|
-
"@live-change/peer-connection-
|
|
78
|
-
"@live-change/
|
|
79
|
-
"@live-change/
|
|
80
|
-
"@live-change/
|
|
81
|
-
"@live-change/secret-
|
|
82
|
-
"@live-change/
|
|
83
|
-
"@live-change/
|
|
84
|
-
"@live-change/
|
|
85
|
-
"@live-change/task-
|
|
86
|
-
"@live-change/
|
|
87
|
-
"@live-change/
|
|
88
|
-
"@live-change/upload-
|
|
89
|
-
"@live-change/
|
|
90
|
-
"@live-change/url-
|
|
91
|
-
"@live-change/
|
|
92
|
-
"@live-change/user-
|
|
93
|
-
"@live-change/user-service": "^0.9.
|
|
94
|
-
"@live-change/
|
|
95
|
-
"@live-change/video-call-
|
|
96
|
-
"@live-change/
|
|
97
|
-
"@live-change/
|
|
98
|
-
"@live-change/vue3-
|
|
99
|
-
"@live-change/
|
|
51
|
+
"@live-change/access-control-frontend": "^0.9.207",
|
|
52
|
+
"@live-change/access-control-service": "^0.9.207",
|
|
53
|
+
"@live-change/agreement-service": "^0.9.207",
|
|
54
|
+
"@live-change/backup-service": "^0.9.207",
|
|
55
|
+
"@live-change/blog-frontend": "^0.9.207",
|
|
56
|
+
"@live-change/blog-service": "^0.9.207",
|
|
57
|
+
"@live-change/cli": "^0.9.207",
|
|
58
|
+
"@live-change/content-frontend": "^0.9.207",
|
|
59
|
+
"@live-change/content-service": "^0.9.207",
|
|
60
|
+
"@live-change/cron-service": "^0.9.207",
|
|
61
|
+
"@live-change/dao": "^0.9.207",
|
|
62
|
+
"@live-change/dao-vue3": "^0.9.207",
|
|
63
|
+
"@live-change/dao-websocket": "^0.9.207",
|
|
64
|
+
"@live-change/db-client": "^0.9.207",
|
|
65
|
+
"@live-change/draft-service": "^0.9.207",
|
|
66
|
+
"@live-change/e2e-test": "^0.9.207",
|
|
67
|
+
"@live-change/email-service": "^0.9.207",
|
|
68
|
+
"@live-change/framework": "^0.9.207",
|
|
69
|
+
"@live-change/frontend-auto-form": "^0.9.207",
|
|
70
|
+
"@live-change/frontend-base": "^0.9.207",
|
|
71
|
+
"@live-change/geoip-service": "^0.9.207",
|
|
72
|
+
"@live-change/google-authentication-service": "^0.9.207",
|
|
73
|
+
"@live-change/image-frontend": "^0.9.207",
|
|
74
|
+
"@live-change/linkedin-authentication-service": "^0.9.207",
|
|
75
|
+
"@live-change/locale-settings-service": "^0.9.207",
|
|
76
|
+
"@live-change/notification-service": "^0.9.207",
|
|
77
|
+
"@live-change/password-authentication-service": "^0.9.207",
|
|
78
|
+
"@live-change/peer-connection-frontend": "^0.9.207",
|
|
79
|
+
"@live-change/peer-connection-service": "^0.9.207",
|
|
80
|
+
"@live-change/prosemirror-service": "^0.9.207",
|
|
81
|
+
"@live-change/scope-service": "^0.9.207",
|
|
82
|
+
"@live-change/secret-code-service": "^0.9.207",
|
|
83
|
+
"@live-change/secret-link-service": "^0.9.207",
|
|
84
|
+
"@live-change/security-service": "^0.9.207",
|
|
85
|
+
"@live-change/session-service": "^0.9.207",
|
|
86
|
+
"@live-change/task-frontend": "^0.9.207",
|
|
87
|
+
"@live-change/task-service": "^0.9.207",
|
|
88
|
+
"@live-change/timer-service": "^0.9.207",
|
|
89
|
+
"@live-change/upload-frontend": "^0.9.207",
|
|
90
|
+
"@live-change/upload-service": "^0.9.207",
|
|
91
|
+
"@live-change/url-frontend": "^0.9.207",
|
|
92
|
+
"@live-change/url-service": "^0.9.207",
|
|
93
|
+
"@live-change/user-frontend": "^0.9.207",
|
|
94
|
+
"@live-change/user-identification-service": "^0.9.207",
|
|
95
|
+
"@live-change/user-service": "^0.9.207",
|
|
96
|
+
"@live-change/video-call-frontend": "^0.9.207",
|
|
97
|
+
"@live-change/video-call-service": "^0.9.207",
|
|
98
|
+
"@live-change/vote-service": "^0.9.207",
|
|
99
|
+
"@live-change/vue3-components": "^0.9.207",
|
|
100
|
+
"@live-change/vue3-ssr": "^0.9.207",
|
|
101
|
+
"@live-change/wysiwyg-frontend": "^0.9.207",
|
|
100
102
|
"@vueuse/core": "^12.3.0",
|
|
101
103
|
"codeceptjs-assert": "^0.0.5",
|
|
102
104
|
"compression": "^1.7.5",
|
|
@@ -120,7 +122,7 @@
|
|
|
120
122
|
"devDependencies": {
|
|
121
123
|
"copyfiles": "^2.4.1",
|
|
122
124
|
"generate-password": "^1.7.1",
|
|
123
|
-
"playwright": "
|
|
125
|
+
"playwright": "=1.50.1",
|
|
124
126
|
"random-profile-generator": "^2.3.0",
|
|
125
127
|
"tsx": "^4.21.0",
|
|
126
128
|
"txtgen": "^3.0.7",
|
|
@@ -129,5 +131,5 @@
|
|
|
129
131
|
"author": "Michał Łaszczewski <michal@laszczewski.pl>",
|
|
130
132
|
"license": "ISC",
|
|
131
133
|
"description": "",
|
|
132
|
-
"gitHead": "
|
|
134
|
+
"gitHead": "1937f8f9798d5df011b38341119a25afe0def6d1"
|
|
133
135
|
}
|