@aerokit/sdk 14.66.0 → 14.67.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/package.json +1 -1
- package/test/api.js +2 -0
- package/test/flows/crud.js +65 -46
- package/test/flows/list.js +9 -2
- package/test/flows/multilingual.js +2 -1
- package/test/form.js +15 -5
package/package.json
CHANGED
package/test/api.js
CHANGED
|
@@ -19,6 +19,8 @@ export function makeApi(request, manifest) {
|
|
|
19
19
|
count: (entity) => request.get(url(entity, '/count')).then(asJson).then((body) => (typeof body === 'number' ? body : body.count)),
|
|
20
20
|
get: (entity, id) => request.get(url(entity, '/' + id)).then(asJson),
|
|
21
21
|
getResponse: (entity, id) => request.get(url(entity, '/' + id)),
|
|
22
|
+
// the generated controller's filtered read (what the list's filters POST): [{ propertyName, operator, value }]
|
|
23
|
+
search: (entity, conditions) => request.post(url(entity, '/search'), { data: { conditions } }).then(asJson),
|
|
22
24
|
create: (entity, data) => request.post(url(entity), { data }).then(asJson),
|
|
23
25
|
update: (entity, id, data) => request.put(url(entity, '/' + id), { data }).then(asJson),
|
|
24
26
|
// Answers whether the row is gone. A record whose process declares `whenDeleted: refuse` is
|
package/test/flows/crud.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expect, test } from '../fixtures.js';
|
|
2
|
+
import { makeApi } from '../api.js';
|
|
2
3
|
import { fillField, fillForm, resolveRelationSamples } from '../form.js';
|
|
3
4
|
import { handleField, sampleRecord } from '../sample-values.js';
|
|
4
5
|
|
|
@@ -14,6 +15,23 @@ function dataRow(page, text) {
|
|
|
14
15
|
return page.locator('tbody tr', { hasText: text });
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
// Edit and Delete of a row live in its "Row actions" menu - the one place every list layout
|
|
19
|
+
// (manage, document, master-detail) offers both; the manage list's record sheet puts Delete behind
|
|
20
|
+
// an overflow menu and its primary button may be a process task rather than Edit (#7491).
|
|
21
|
+
async function rowAction(page, text, action) {
|
|
22
|
+
await dataRow(page, text).getByRole('button', { name: 'Row actions' }).click();
|
|
23
|
+
await page.getByRole('menuitem', { name: action, exact: true }).click();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A walk that fails halfway leaves its APPTEST- row behind, and the next run's REST flow then hits
|
|
27
|
+
// a unique: column with it (409). Remove whatever the walk created - the edited name starts with
|
|
28
|
+
// the created one - unless the UI delete already did.
|
|
29
|
+
async function removeLeftovers(client, manifest, entity, handle, created) {
|
|
30
|
+
const idProperty = manifest.idProperty ?? 'Id';
|
|
31
|
+
const rows = await client.search(entity, [{ propertyName: handle.name, operator: 'LIKE', value: created + '%' }]);
|
|
32
|
+
for (const row of rows ?? []) await client.remove(entity, row[idProperty]);
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
export function crudFlow(manifest, entity, opts = {}) {
|
|
18
36
|
const cfg = opts.extend?.entities?.[entity.name] ?? {};
|
|
19
37
|
const skip = new Set(cfg.skip ?? []);
|
|
@@ -30,55 +48,56 @@ export function crudFlow(manifest, entity, opts = {}) {
|
|
|
30
48
|
const handle = handleField(entity);
|
|
31
49
|
const relationSamples = await resolveRelationSamples(api, manifest, entity);
|
|
32
50
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
await expect(page).toHaveURL(/\/create$/);
|
|
38
|
-
await cfg.beforeCreate?.(page, record);
|
|
39
|
-
await fillForm(page, manifest, entity, record, relationSamples, opts);
|
|
40
|
-
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
|
41
|
-
if (entity.layout === 'document') {
|
|
42
|
-
// a document create lands on the NEW record's page (line-item editing continues there);
|
|
43
|
-
// saving an edit is what returns to the list
|
|
44
|
-
await expect(page).toHaveURL(/\/edit$/);
|
|
51
|
+
const created = record[handle.name];
|
|
52
|
+
const listRoute = entity.route.replace(/[#/]/g, '\\$&');
|
|
53
|
+
try {
|
|
54
|
+
// create
|
|
45
55
|
await page.goto(manifest.standaloneShell + entity.route);
|
|
46
|
-
|
|
47
|
-
await
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
// exact: the empty state adds a second "New <Entity>" button that a substring match also hits
|
|
57
|
+
await page.getByRole('button', { name: 'New', exact: true }).click();
|
|
58
|
+
await expect(page).toHaveURL(/\/create$/);
|
|
59
|
+
await cfg.beforeCreate?.(page, record);
|
|
60
|
+
await fillForm(page, manifest, entity, record, relationSamples, opts);
|
|
61
|
+
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
|
62
|
+
// A create lands on the NEW record's page when there is more to do there - a document's
|
|
63
|
+
// line items, a manage record's detail collections - and on the list otherwise. Saving an
|
|
64
|
+
// edit is what returns to the list.
|
|
65
|
+
const recordPage = entity.layout === 'document' ? '/[^/]+/edit' : '(/[^/]+/edit)?';
|
|
66
|
+
await expect(page).toHaveURL(new RegExp(listRoute + recordPage + '$'));
|
|
67
|
+
if (page.url().endsWith('/edit')) await page.goto(manifest.standaloneShell + entity.route);
|
|
68
|
+
await filterBy(page, created);
|
|
69
|
+
await expect(dataRow(page, created)).toHaveCount(1);
|
|
70
|
+
await cfg.afterCreate?.(page, record);
|
|
52
71
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
record[handle.name] = updated;
|
|
69
|
-
}
|
|
72
|
+
// edit
|
|
73
|
+
if (!skip.has('edit')) {
|
|
74
|
+
const updated = created + '-UPD';
|
|
75
|
+
await rowAction(page, created, 'Edit');
|
|
76
|
+
await expect(page).toHaveURL(/\/edit$/);
|
|
77
|
+
// the record loads async after the form renders - filling before the fetch completes
|
|
78
|
+
// gets overwritten by the load and Save persists the OLD value
|
|
79
|
+
await expect(page.locator('#f_' + handle.name)).toHaveValue(created);
|
|
80
|
+
await fillField(page, handle, updated, opts);
|
|
81
|
+
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
|
82
|
+
await expect(page).toHaveURL(new RegExp(listRoute + '$'));
|
|
83
|
+
await filterBy(page, updated);
|
|
84
|
+
await expect(dataRow(page, updated)).toHaveCount(1);
|
|
85
|
+
record[handle.name] = updated;
|
|
86
|
+
}
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
// delete (row menu, then the confirm dialog). An entity whose process declares
|
|
89
|
+
// `whenDeleted: refuse` answers the delete with an error toast while its instance runs - the
|
|
90
|
+
// create above started it - so the walk stops here and the REST flow asserts that refusal.
|
|
91
|
+
if (!skip.has('delete') && !entity.deleteGuardedByProcess?.length) {
|
|
92
|
+
await rowAction(page, record[handle.name], 'Delete');
|
|
93
|
+
const dialog = page.locator('[x-h-dialog-overlay][data-open]');
|
|
94
|
+
await dialog.getByRole('button', { name: 'Delete', exact: true }).click();
|
|
95
|
+
await expect(dialog).toHaveCount(0);
|
|
96
|
+
await filterBy(page, record[handle.name]);
|
|
97
|
+
await expect(dataRow(page, record[handle.name])).toHaveCount(0);
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
await removeLeftovers(makeApi(api, manifest), manifest, entity, handle, created);
|
|
82
101
|
}
|
|
83
102
|
});
|
|
84
103
|
}
|
package/test/flows/list.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { expect, test } from '../fixtures.js';
|
|
2
2
|
import { labelOf } from '../sample-values.js';
|
|
3
3
|
|
|
4
|
-
// The
|
|
4
|
+
// The shell breadcrumb's current crumb - the one page element every list layout shares that names
|
|
5
|
+
// the entity (the sidebar label, which the manifest's labelPlural mirrors).
|
|
6
|
+
export function listCrumb(page, entity) {
|
|
7
|
+
return page.locator('[x-h-breadcrumb-page]', { hasText: entity.labelPlural }).first();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// The list page renders under the shell breadcrumb naming it by its plural label (the #7491
|
|
11
|
+
// manage list carries no title of its own), then one of three bodies -
|
|
5
12
|
// a tree (hierarchy entities render role=treeitem nodes, no table), the table with one
|
|
6
13
|
// column header per major field, or (when the entity has no rows yet) the empty state.
|
|
7
14
|
// When seed data is expected, rows/nodes must actually be there.
|
|
8
15
|
export function listFlow(manifest, entity) {
|
|
9
16
|
test(`${entity.name}: list page renders the declared columns`, async ({ page }) => {
|
|
10
17
|
await page.goto(manifest.standaloneShell + entity.route);
|
|
11
|
-
await expect(page
|
|
18
|
+
await expect(listCrumb(page, entity)).toBeVisible();
|
|
12
19
|
if (entity.hierarchy) {
|
|
13
20
|
if (entity.expectSeedData) {
|
|
14
21
|
await expect(page.getByRole('treeitem').first()).toBeVisible();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expect, test } from '../fixtures.js';
|
|
2
|
+
import { listCrumb } from './list.js';
|
|
2
3
|
|
|
3
4
|
// The read-time translation overlay: switch the shared language key (what the Region &
|
|
4
5
|
// Language setting writes), reload, and a known seed row shows its translated name.
|
|
@@ -9,7 +10,7 @@ export function multilingualFlow(manifest, entity) {
|
|
|
9
10
|
|
|
10
11
|
test(`${entity.name}: ${sample.language} translation overlays on read`, async ({ page }) => {
|
|
11
12
|
await page.goto(manifest.standaloneShell + entity.route);
|
|
12
|
-
await expect(page
|
|
13
|
+
await expect(listCrumb(page, entity)).toBeVisible();
|
|
13
14
|
await page.evaluate((lang) => localStorage.setItem('codbex.harmonia.language', lang), sample.language);
|
|
14
15
|
await page.reload();
|
|
15
16
|
await expect(page.locator('tbody tr', { hasText: sample.translated }).first()).toBeVisible();
|
package/test/form.js
CHANGED
|
@@ -16,21 +16,31 @@ export async function fillField(page, field, value, opts = {}) {
|
|
|
16
16
|
await input.fill(String(value));
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
// The x-h-select directive hides its input and
|
|
20
|
-
//
|
|
21
|
-
|
|
19
|
+
// The x-h-select directive hides its input and appends a button[role=combobox] trigger beside it;
|
|
20
|
+
// options carry role=option.
|
|
21
|
+
// The generated manage/document forms give that input id="f_<Name>", so the trigger is reached
|
|
22
|
+
// through its sibling - independent of how the field label reads. The accessible name is the
|
|
23
|
+
// fallback for a form whose select input carries no id (the my/partner surfaces).
|
|
24
|
+
async function relationTrigger(page, relation) {
|
|
25
|
+
const input = page.locator('#f_' + relation.name);
|
|
26
|
+
if (await input.count()) return input.locator('xpath=../button[@role="combobox"]');
|
|
22
27
|
// Anchored prefix match: the combobox accessible name is the label plus the placeholder or
|
|
23
28
|
// selected value ("Country Select a Country..."), so exact matching finds nothing - while a
|
|
24
29
|
// bare substring match collides with longer sibling labels ("Type" also hits "Chart Type").
|
|
25
30
|
const label = relation.label ?? relation.name;
|
|
26
31
|
const anchored = new RegExp('^' + label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
|
|
27
|
-
|
|
32
|
+
return page.getByRole('combobox', { name: anchored }).first();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function pickDropdown(page, relation, optionText) {
|
|
36
|
+
const trigger = await relationTrigger(page, relation);
|
|
37
|
+
await trigger.click();
|
|
28
38
|
const option = page.getByRole('option', { name: optionText }).first();
|
|
29
39
|
try {
|
|
30
40
|
await option.click({ timeout: 10_000 });
|
|
31
41
|
} catch {
|
|
32
42
|
// the option list re-rendered mid-click (async option load reflow) - reopen and retry
|
|
33
|
-
await
|
|
43
|
+
await trigger.click();
|
|
34
44
|
await option.click({ force: true });
|
|
35
45
|
}
|
|
36
46
|
}
|