@aerokit/sdk 14.2.0 → 14.3.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/flows/my.js +75 -6
package/package.json
CHANGED
package/test/flows/my.js
CHANGED
|
@@ -3,6 +3,65 @@ import { expect, test } from '../fixtures.js';
|
|
|
3
3
|
import { resolveRelationSamples } from '../form.js';
|
|
4
4
|
import { sampleRecord } from '../sample-values.js';
|
|
5
5
|
|
|
6
|
+
// Wave 2 (UI parity): the personal PAGE renders what the wire serves. Driven by the same
|
|
7
|
+
// own-row lifecycle as the wire test (created through the scoped controller, removed in the
|
|
8
|
+
// finally); skips without an identity mapping, exactly like the wire flow.
|
|
9
|
+
// - layout 'list': every relation column of the own row shows its referenced LABEL - the
|
|
10
|
+
// raw-FK-id regression class the personal templates fixed;
|
|
11
|
+
// - layout 'calendar'/'slots': the personal route renders the calendar / slot picker, never a
|
|
12
|
+
// plain form+list downgrade;
|
|
13
|
+
// - layout 'document-chat': the composer accepts a message and the bubble renders (the my
|
|
14
|
+
// document chat parity class). A plain 'document' asserts the page serves.
|
|
15
|
+
function myUiTest(manifest, entity, mine, buildContext, idProperty) {
|
|
16
|
+
test(`${entity.name}: the My page renders the own record with resolved labels (UI parity)`, async ({ api, page }) => {
|
|
17
|
+
const { payload, samples } = await buildContext(api);
|
|
18
|
+
const createdResponse = await api.post(manifest.restBase + mine.api, { data: payload });
|
|
19
|
+
test.skip(
|
|
20
|
+
!createdResponse.ok(),
|
|
21
|
+
`personal create returned ${createdResponse.status()} - the test user has no identity mapping; ` +
|
|
22
|
+
`seed the dev identity row (an owner-target record whose identity field equals the login user)`
|
|
23
|
+
);
|
|
24
|
+
const created = JSON.parse(await createdResponse.text());
|
|
25
|
+
const id = created?.[idProperty];
|
|
26
|
+
const client = makeApi(api, manifest);
|
|
27
|
+
try {
|
|
28
|
+
const layout = entity.personal.layout ?? 'list';
|
|
29
|
+
if (layout === 'calendar' || layout === 'slots') {
|
|
30
|
+
await page.goto(manifest.standaloneShell + entity.personal.route);
|
|
31
|
+
await expect(page.locator('[x-h-calendar], [x-h-slot-picker]').first()).toBeVisible();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (layout === 'document-chat' || layout === 'document') {
|
|
35
|
+
await page.goto(manifest.standaloneShell + entity.personal.route + '/' + id + '/edit');
|
|
36
|
+
if (layout === 'document-chat') {
|
|
37
|
+
const message = `UI parity probe ${id}`;
|
|
38
|
+
const composer = page.getByRole('textbox', { name: /write a message/i }).first();
|
|
39
|
+
await composer.fill(message);
|
|
40
|
+
await page.getByRole('button', { name: /send/i }).first().click();
|
|
41
|
+
await expect(page.getByText(message).first(), 'the sent message must render as a bubble').toBeVisible();
|
|
42
|
+
} else {
|
|
43
|
+
await expect(page.locator('[x-h-toolbar-title]').first()).toBeVisible();
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// list: the own rows are the only rows - every asserted relation column of the created
|
|
48
|
+
// row must surface its referenced label somewhere in the table body
|
|
49
|
+
await page.goto(manifest.standaloneShell + entity.personal.route);
|
|
50
|
+
await expect(page.locator('tbody tr:visible').first()).toBeVisible();
|
|
51
|
+
const fkColumns = new Set(entity.personal.fkColumns ?? []);
|
|
52
|
+
for (const sample of samples) {
|
|
53
|
+
if (!fkColumns.has(sample.relation.name) || sample.label == null) continue;
|
|
54
|
+
await expect(
|
|
55
|
+
page.locator('tbody').getByText(String(sample.label)).first(),
|
|
56
|
+
`${sample.relation.name} must render its referenced label ("${sample.label}"), not the raw id ${sample.id}`
|
|
57
|
+
).toBeVisible();
|
|
58
|
+
}
|
|
59
|
+
} finally {
|
|
60
|
+
await client.remove(mine, id);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
6
65
|
// The personal (my) surface WIRE contract, driven when the manifest marks an entity `personal`:
|
|
7
66
|
// the scoped <Entity>MyController filters reads to the identity-mapped user, forces the owner FK
|
|
8
67
|
// server-side on create (whatever the client sends is ignored), strips every `sensitive` field
|
|
@@ -12,9 +71,8 @@ import { sampleRecord } from '../sample-values.js';
|
|
|
12
71
|
// Prerequisite: the DEV IDENTITY mapping - a row of the owner relation's target whose identity
|
|
13
72
|
// field equals the test user (e.g. an Employee with email 'admin'). Without it the personal
|
|
14
73
|
// surface is EMPTY by design (never an error), so the flow SKIPS with a pointer instead of
|
|
15
|
-
// failing.
|
|
16
|
-
//
|
|
17
|
-
// parity fixes; this flow pins down the wire contract those pages consume.
|
|
74
|
+
// failing. Wave 2 (myUiTest below) walks the personal PAGE over the same own-row lifecycle:
|
|
75
|
+
// resolved relation labels on the list, calendar/slots rendering, the chat composer round-trip.
|
|
18
76
|
export function myFlow(manifest, entity, opts = {}) {
|
|
19
77
|
const cfg = opts.extend?.entities?.[entity.name] ?? {};
|
|
20
78
|
if (!entity.personal || new Set(cfg.skip ?? []).has('my')) return;
|
|
@@ -22,14 +80,19 @@ export function myFlow(manifest, entity, opts = {}) {
|
|
|
22
80
|
// the same entity through the scoped controller
|
|
23
81
|
const mine = { ...entity, api: entity.personal.api };
|
|
24
82
|
|
|
25
|
-
async function
|
|
83
|
+
async function buildContext(api) {
|
|
26
84
|
const payload = sampleRecord(entity);
|
|
27
|
-
|
|
85
|
+
const samples = await resolveRelationSamples(api, manifest, entity);
|
|
86
|
+
for (const sample of samples) {
|
|
28
87
|
payload[sample.relation.name] = sample.id;
|
|
29
88
|
}
|
|
30
89
|
// the owner FK is server-forced on the personal surface; sending a value must be pointless
|
|
31
90
|
delete payload[entity.personal.owner];
|
|
32
|
-
return payload;
|
|
91
|
+
return { payload, samples };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function buildPayload(api) {
|
|
95
|
+
return (await buildContext(api)).payload;
|
|
33
96
|
}
|
|
34
97
|
|
|
35
98
|
test(`${entity.name}: personal (my) surface scopes rows, forces the owner and strips sensitive fields`, async ({ api }) => {
|
|
@@ -76,4 +139,10 @@ export function myFlow(manifest, entity, opts = {}) {
|
|
|
76
139
|
const gone = await api.get(manifest.restBase + mine.api + '/' + id);
|
|
77
140
|
expect(gone.status()).toBe(404);
|
|
78
141
|
});
|
|
142
|
+
|
|
143
|
+
// wave 2: the UI-parity walk over the same personal surface (older manifests without the
|
|
144
|
+
// route/layout metadata regenerate to get it - skip quietly until then)
|
|
145
|
+
if (entity.personal.route) {
|
|
146
|
+
myUiTest(manifest, entity, mine, buildContext, idProperty);
|
|
147
|
+
}
|
|
79
148
|
}
|