@jskit-ai/users-web 0.1.173 → 0.1.175

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 (113) hide show
  1. package/package.json +18 -149
  2. package/src/client/composables/account-settings/accountSettingsAvatarUploadRuntime.js +2 -2
  3. package/src/client/composables/useAccountSettingsRuntime.js +3 -3
  4. package/src/client/index.js +0 -11
  5. package/src/client/providers/UsersWebClientProvider.js +0 -2
  6. package/test/exportsContract.test.js +3 -29
  7. package/test/settingsPlacementContract.test.js +10 -0
  8. package/fixtures/crud-list-date-filters/App.vue +0 -107
  9. package/fixtures/crud-list-date-filters/index.html +0 -12
  10. package/fixtures/crud-list-date-filters/main.js +0 -24
  11. package/fixtures/crud-list-date-filters/vite.config.mjs +0 -18
  12. package/src/client/bulkActions.js +0 -47
  13. package/src/client/components/CrudAddEditScreen.vue +0 -186
  14. package/src/client/components/CrudDeleteAction.vue +0 -93
  15. package/src/client/components/CrudListBulkActionSurface.vue +0 -126
  16. package/src/client/components/CrudListDateFilterControl.vue +0 -188
  17. package/src/client/components/CrudListFilterSurface.vue +0 -388
  18. package/src/client/components/CrudListRecordActionMenu.vue +0 -120
  19. package/src/client/components/CrudListScreen.vue +0 -501
  20. package/src/client/components/CrudViewScreen.vue +0 -197
  21. package/src/client/composables/crud/crudBindingSupport.js +0 -75
  22. package/src/client/composables/crud/crudHttpClientSupport.js +0 -57
  23. package/src/client/composables/crud/crudJsonApiTransportSupport.js +0 -145
  24. package/src/client/composables/crud/crudLookupFieldLabelSupport.js +0 -151
  25. package/src/client/composables/crud/crudLookupFieldRuntime.js +0 -251
  26. package/src/client/composables/crud/crudSchemaFormHelpers.js +0 -464
  27. package/src/client/composables/internal/crudListFilterLookupSupport.js +0 -161
  28. package/src/client/composables/internal/crudListParentTitleSupport.js +0 -171
  29. package/src/client/composables/internal/useOperationScope.js +0 -144
  30. package/src/client/composables/records/useAddEdit.js +0 -224
  31. package/src/client/composables/records/useCrudAddEdit.js +0 -227
  32. package/src/client/composables/records/useCrudList.js +0 -101
  33. package/src/client/composables/records/useCrudView.js +0 -47
  34. package/src/client/composables/records/useList.js +0 -500
  35. package/src/client/composables/records/useView.js +0 -164
  36. package/src/client/composables/runtime/addEditUiRuntime.js +0 -130
  37. package/src/client/composables/runtime/listUiRuntime.js +0 -123
  38. package/src/client/composables/runtime/modelStateHelpers.js +0 -90
  39. package/src/client/composables/runtime/operationAdapters.js +0 -34
  40. package/src/client/composables/runtime/operationUiHelpers.js +0 -125
  41. package/src/client/composables/runtime/operationValidationHelpers.js +0 -51
  42. package/src/client/composables/runtime/useAddEditCore.js +0 -112
  43. package/src/client/composables/runtime/useCommandCore.js +0 -123
  44. package/src/client/composables/runtime/useEndpointResource.js +0 -199
  45. package/src/client/composables/runtime/useFieldErrorBag.js +0 -61
  46. package/src/client/composables/runtime/useListCore.js +0 -112
  47. package/src/client/composables/runtime/useUiFeedback.js +0 -98
  48. package/src/client/composables/runtime/useViewCore.js +0 -93
  49. package/src/client/composables/runtime/viewUiRuntime.js +0 -96
  50. package/src/client/composables/support/errorMessageHelpers.js +0 -66
  51. package/src/client/composables/support/listQueryParamSupport.js +0 -473
  52. package/src/client/composables/support/listSearchSupport.js +0 -70
  53. package/src/client/composables/support/refValueHelpers.js +0 -19
  54. package/src/client/composables/support/requestQueryRuntimeSupport.js +0 -141
  55. package/src/client/composables/support/resourceLoadStateHelpers.js +0 -208
  56. package/src/client/composables/support/routeTemplateHelpers.js +0 -285
  57. package/src/client/composables/support/scopeHelpers.js +0 -145
  58. package/src/client/composables/useAccess.js +0 -109
  59. package/src/client/composables/useCommand.js +0 -118
  60. package/src/client/composables/useCrudAddEditScreen.js +0 -88
  61. package/src/client/composables/useCrudDeleteAction.js +0 -160
  62. package/src/client/composables/useCrudListBulkActions.js +0 -147
  63. package/src/client/composables/useCrudListFilterLookups.js +0 -146
  64. package/src/client/composables/useCrudListFilters.js +0 -339
  65. package/src/client/composables/useCrudListParentTitle.js +0 -151
  66. package/src/client/composables/useCrudListRowActions.js +0 -144
  67. package/src/client/composables/useCrudListScreen.js +0 -237
  68. package/src/client/composables/useCrudViewScreen.js +0 -77
  69. package/src/client/composables/usePagedCollection.js +0 -181
  70. package/src/client/composables/useRealtimeQueryInvalidation.js +0 -156
  71. package/src/client/composables/useScopeRuntime.js +0 -135
  72. package/src/client/filters.js +0 -15
  73. package/src/client/lib/httpClient.js +0 -47
  74. package/src/client/lib/permissions.js +0 -34
  75. package/src/client/rowActions.js +0 -47
  76. package/src/client/support/contractGuards.js +0 -34
  77. package/src/client/support/crudListDateFilterSupport.js +0 -91
  78. package/test/addEditUiRuntime.test.js +0 -110
  79. package/test/crudBindingSupport.test.js +0 -110
  80. package/test/crudHttpClientSupport.test.js +0 -77
  81. package/test/crudJsonApiTransportSupport.test.js +0 -179
  82. package/test/crudListBulkActionSurface.test.js +0 -27
  83. package/test/crudListDateFilterSupport.test.js +0 -92
  84. package/test/crudListDateFilterSurface.browser.test.js +0 -182
  85. package/test/crudListFilterSurface.test.js +0 -90
  86. package/test/crudLookupFieldRuntime.test.js +0 -264
  87. package/test/crudScreenComponents.test.js +0 -117
  88. package/test/errorIntentContract.test.js +0 -31
  89. package/test/errorMessageHelpers.test.js +0 -28
  90. package/test/listQueryParamSupport.test.js +0 -223
  91. package/test/listUiRuntime.test.js +0 -102
  92. package/test/operationValidationHelpers.test.js +0 -64
  93. package/test/permissions.test.js +0 -35
  94. package/test/refValueHelpers.test.js +0 -14
  95. package/test/requestTransportOptions.test.js +0 -457
  96. package/test/resourceLoadStateHelpers.test.js +0 -189
  97. package/test/routeTemplateHelpers.test.js +0 -85
  98. package/test/scopeHelpers.test.js +0 -57
  99. package/test/useAddEditCore.test.js +0 -124
  100. package/test/useAddEditRequestQueryParams.test.js +0 -190
  101. package/test/useCrudAddEdit.test.js +0 -415
  102. package/test/useCrudDeleteAction.test.js +0 -139
  103. package/test/useCrudListBulkActions.test.js +0 -65
  104. package/test/useCrudListFilterLookups.test.js +0 -114
  105. package/test/useCrudListFilters.test.js +0 -293
  106. package/test/useCrudListParentTitle.test.js +0 -262
  107. package/test/useCrudListRowActions.test.js +0 -77
  108. package/test/useListRouteInitialization.test.js +0 -337
  109. package/test/useListSearchSupport.test.js +0 -61
  110. package/test/usePagedCollection.test.js +0 -53
  111. package/test/useViewRequestQueryParams.test.js +0 -20
  112. package/test/viewCoreLoading.test.js +0 -44
  113. package/test/viewUiRuntime.test.js +0 -95
@@ -1,182 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import path from "node:path";
3
- import process from "node:process";
4
- import test from "node:test";
5
- import { fileURLToPath } from "node:url";
6
- import { chromium } from "@playwright/test";
7
- import {
8
- createChromiumLaunchOptions,
9
- startViteFixture,
10
- stopProcess
11
- } from "../../../tooling/testUtils/browserFixture.mjs";
12
-
13
- const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
14
- const PACKAGE_ROOT = path.resolve(TEST_DIRECTORY, "..");
15
- const FIXTURE_ROOT = path.join(PACKAGE_ROOT, "fixtures", "crud-list-date-filters");
16
- const RUN_BROWSER_TEST = process.env.JSKIT_USERS_WEB_DATE_FILTER_VISUAL_INTEGRATION === "1";
17
- const VIEWPORT_WIDTHS = Object.freeze([375, 768, 1280, 1440]);
18
- const QUERY = "?status=active&submittedOn=2026-04-18&arrivalDate=2026-05-04..2026-05-10";
19
-
20
- async function readContract(page) {
21
- return JSON.parse(await page.getByTestId("date-filter-contract").textContent());
22
- }
23
-
24
- async function openCompactFilters(page) {
25
- const openButton = page.getByRole("button", { name: /^Filters/u });
26
- if (await openButton.isVisible()) {
27
- await openButton.click();
28
- await page.getByRole("dialog").waitFor({ state: "visible" });
29
- }
30
- }
31
-
32
- async function assertResponsiveDateControls(page, width) {
33
- await page.setViewportSize({ width, height: 900 });
34
- await page.goto(`/${QUERY}`, {
35
- waitUntil: "networkidle"
36
- });
37
- await openCompactFilters(page);
38
-
39
- for (const name of ["Submitted date", "Arrival from", "Arrival to"]) {
40
- const field = page.getByLabel(name, { exact: true });
41
- await field.waitFor({ state: "visible" });
42
- const box = await field.locator("xpath=ancestor::*[contains(@class, 'v-input')][1]").boundingBox();
43
- assert.ok(box, `${name} is missing a visible field at ${width}px.`);
44
- assert.ok(box.width >= 220, `${name} is only ${box.width}px wide at ${width}px.`);
45
- assert.ok(box.x >= 0 && box.x + box.width <= width, `${name} overflows at ${width}px.`);
46
- }
47
-
48
- const layout = await page.evaluate(() => ({
49
- clientWidth: document.documentElement.clientWidth,
50
- scrollWidth: document.documentElement.scrollWidth,
51
- clippedFields: Array.from(document.querySelectorAll(".crud-list-date-filter-control .v-field"))
52
- .filter((field) => field.scrollWidth > field.clientWidth + 1)
53
- .length,
54
- visibleCalendarIcons: Array.from(document.querySelectorAll(
55
- ".crud-list-date-filter-control .v-field__append-inner"
56
- )).filter((icon) => {
57
- const rect = icon.getBoundingClientRect();
58
- return rect.width > 0 && rect.right <= window.innerWidth;
59
- }).length
60
- }));
61
- assert.equal(layout.scrollWidth, layout.clientWidth, `Horizontal overflow at ${width}px.`);
62
- assert.equal(layout.clippedFields, 0, `A date field is clipped at ${width}px.`);
63
- assert.equal(layout.visibleCalendarIcons, 3, `Calendar affordance missing at ${width}px.`);
64
-
65
- const screenshot = await page.screenshot({ fullPage: true });
66
- assert.ok(screenshot.length > 5_000, `Visual evidence was empty at ${width}px.`);
67
- }
68
-
69
- async function selectPickerDayWithKeyboard(picker, day) {
70
- const dayButton = picker
71
- .locator(".v-date-picker-month__day-btn")
72
- .filter({ hasText: new RegExp(`^${day}$`, "u") })
73
- .first();
74
- await dayButton.focus();
75
- await dayButton.press("Space");
76
- }
77
-
78
- test("CRUD date filters render and operate without clipping across responsive layouts", {
79
- skip: RUN_BROWSER_TEST
80
- ? false
81
- : "set JSKIT_USERS_WEB_DATE_FILTER_VISUAL_INTEGRATION=1 to run the browser visual regression",
82
- timeout: 180_000
83
- }, async () => {
84
- const viteRuntime = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
85
- let browser = null;
86
-
87
- try {
88
- browser = await chromium.launch(createChromiumLaunchOptions());
89
- const context = await browser.newContext({
90
- baseURL: viteRuntime.baseURL,
91
- locale: "en-US"
92
- });
93
- const page = await context.newPage();
94
-
95
- for (const width of VIEWPORT_WIDTHS) {
96
- await assertResponsiveDateControls(page, width);
97
- }
98
-
99
- await page.setViewportSize({ width: 1280, height: 900 });
100
- await page.goto(`/${QUERY}`, { waitUntil: "networkidle" });
101
- assert.deepEqual(await readContract(page), {
102
- values: {
103
- submittedOn: "2026-04-18",
104
- arrivalDate: { from: "2026-05-04", to: "2026-05-10" }
105
- },
106
- query: {
107
- submittedOn: "2026-04-18",
108
- arrivalDate: "2026-05-04..2026-05-10"
109
- }
110
- });
111
- await page.getByText("Submitted date: Apr 18, 2026", { exact: true }).waitFor();
112
- await page.getByText("Arrival: May 4, 2026 to May 10, 2026", { exact: true }).waitFor();
113
-
114
- const submittedField = page.getByLabel("Submitted date", { exact: true });
115
- await submittedField.focus();
116
- await submittedField.press("Enter");
117
- const submittedPicker = page.getByRole("dialog", { name: "Submitted date calendar" });
118
- await submittedPicker.waitFor({ state: "visible" });
119
- await selectPickerDayWithKeyboard(submittedPicker, 21);
120
- assert.equal((await readContract(page)).values.submittedOn, "2026-04-21");
121
- assert.equal((await readContract(page)).query.submittedOn, "2026-04-21");
122
- assert.equal(await submittedField.evaluate((element) => element === document.activeElement), true);
123
-
124
- const arrivalToField = page.getByLabel("Arrival to", { exact: true });
125
- await arrivalToField.focus();
126
- await arrivalToField.press("ArrowDown");
127
- const arrivalToPicker = page.getByRole("dialog", { name: "Arrival to calendar" });
128
- await arrivalToPicker.waitFor({ state: "visible" });
129
- await selectPickerDayWithKeyboard(arrivalToPicker, 12);
130
- assert.deepEqual((await readContract(page)).values.arrivalDate, {
131
- from: "2026-05-04",
132
- to: "2026-05-12"
133
- });
134
- assert.equal((await readContract(page)).query.arrivalDate, "2026-05-04..2026-05-12");
135
-
136
- await arrivalToField.focus();
137
- await arrivalToField.press("Enter");
138
- const clearButton = page
139
- .getByRole("dialog", { name: "Arrival to calendar" })
140
- .getByRole("button", { name: "Clear", exact: true });
141
- await clearButton.focus();
142
- await clearButton.press("Space");
143
- assert.deepEqual((await readContract(page)).values.arrivalDate, {
144
- from: "2026-05-04",
145
- to: ""
146
- });
147
- assert.equal((await readContract(page)).query.arrivalDate, "2026-05-04..");
148
- assert.equal(await arrivalToField.evaluate((element) => element === document.activeElement), true);
149
-
150
- const arrivalFromField = page.getByLabel("Arrival from", { exact: true });
151
- await arrivalFromField.focus();
152
- await arrivalFromField.press(" ");
153
- await page.getByRole("dialog", { name: "Arrival from calendar" }).waitFor();
154
- await page.keyboard.press("Escape");
155
- assert.equal(await arrivalFromField.evaluate((element) => element === document.activeElement), true);
156
-
157
- const clearAll = page.getByRole("button", { name: "Clear all", exact: true });
158
- await clearAll.focus();
159
- await clearAll.press("Space");
160
- assert.deepEqual((await readContract(page)).values, {
161
- submittedOn: "",
162
- arrivalDate: { from: "", to: "" }
163
- });
164
-
165
- await page.setViewportSize({ width: 375, height: 812 });
166
- await page.reload({ waitUntil: "networkidle" });
167
- const openFilters = page.getByRole("button", { name: /^Filters/u });
168
- const openButtonBox = await openFilters.boundingBox();
169
- assert.ok(openButtonBox && openButtonBox.height >= 48);
170
- await openFilters.click();
171
- const mobileSheet = page.getByRole("dialog");
172
- await mobileSheet.waitFor({ state: "visible" });
173
- const mobileSheetBox = await mobileSheet.boundingBox();
174
- assert.ok(mobileSheetBox && mobileSheetBox.width <= 375);
175
- await page.getByLabel("Arrival from", { exact: true }).waitFor({ state: "visible" });
176
- } finally {
177
- if (browser) {
178
- await browser.close();
179
- }
180
- await stopProcess(viteRuntime);
181
- }
182
- });
@@ -1,90 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { readFile } from "node:fs/promises";
3
- import path from "node:path";
4
- import test from "node:test";
5
- import { fileURLToPath } from "node:url";
6
- import { compileScript, compileTemplate, parse } from "@vue/compiler-sfc";
7
-
8
- const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
9
- const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
10
-
11
- test("users-web exposes client-side CRUD list filter definition helpers", async () => {
12
- const { defineCrudListFilters } = await import("@jskit-ai/users-web/client/filters");
13
-
14
- const filters = defineCrudListFilters({
15
- status: {
16
- type: "enum",
17
- label: "Status",
18
- options: [
19
- { value: "active", label: "Active" }
20
- ]
21
- }
22
- });
23
-
24
- assert.equal(filters.status.queryKey, "status");
25
- assert.equal(filters.status.options[0].label, "Active");
26
- });
27
-
28
- test("CrudListFilterSurface provides adaptive controls without owning server semantics", async () => {
29
- const source = await readFile(
30
- path.join(PACKAGE_DIR, "src", "client", "components", "CrudListFilterSurface.vue"),
31
- "utf8"
32
- );
33
-
34
- assert.match(source, /defineProps/);
35
- assert.match(source, /useDisplay/);
36
- assert.match(source, /v-if="shouldRender"/);
37
- assert.match(source, /filterEntries/);
38
- assert.match(source, /runtimeValues\[filter\.key\]/);
39
- assert.match(source, /activeChips/);
40
- assert.match(source, /clearChip/);
41
- assert.match(source, /clearFilters/);
42
- assert.match(source, /v-dialog/);
43
- assert.match(source, /min-height:\s*48px/);
44
- assert.match(source, /CrudListDateFilterControl/);
45
- assert.match(source, /crud-list-filter-surface__range--date/);
46
- assert.match(source, /grid-column:\s*span 2/);
47
- assert.doesNotMatch(source, /type="date"/);
48
- assert.doesNotMatch(source, /useCrudList\(/);
49
- assert.doesNotMatch(source, /apiSuffix|server|repository/);
50
- });
51
-
52
- test("CrudListDateFilterControl composes stable Vuetify picker components accessibly", async () => {
53
- const componentPath = path.join(
54
- PACKAGE_DIR,
55
- "src",
56
- "client",
57
- "components",
58
- "CrudListDateFilterControl.vue"
59
- );
60
- const source = await readFile(componentPath, "utf8");
61
-
62
- assert.match(source, /<v-menu/);
63
- assert.match(source, /<v-text-field/);
64
- assert.match(source, /<v-date-picker/);
65
- assert.match(source, /append-inner-icon="\$calendar"/);
66
- assert.match(source, /:aria-label="label"/);
67
- assert.match(source, /aria-haspopup="dialog"/);
68
- assert.match(source, /v-bind="\{ \.\.\.attrs, \.\.\.activatorProps \}"/);
69
- assert.match(source, /\["Enter", " ", "ArrowDown"\]/);
70
- assert.match(source, /role="dialog"/);
71
- assert.match(source, />\s*Clear\s*</u);
72
- assert.match(source, />\s*Close\s*</u);
73
- assert.doesNotMatch(source, /VDateInput|vuetify\/labs|type="date"/);
74
-
75
- const { descriptor, errors: parseErrors } = parse(source, { filename: componentPath });
76
- assert.deepEqual(parseErrors, []);
77
- const script = compileScript(descriptor, {
78
- id: "crud-list-date-filter-control"
79
- });
80
- const compiled = compileTemplate({
81
- id: "crud-list-date-filter-control",
82
- filename: componentPath,
83
- source: descriptor.template.content,
84
- scoped: descriptor.styles.some((style) => style.scoped),
85
- compilerOptions: {
86
- bindingMetadata: script.bindings
87
- }
88
- });
89
- assert.deepEqual(compiled.errors, []);
90
- });
@@ -1,264 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import {
4
- resolveLookupItemLabel,
5
- resolveLookupFieldDisplayValue,
6
- resolveRecordTitle
7
- } from "../src/client/composables/crud/crudLookupFieldLabelSupport.js";
8
-
9
- test("resolveLookupItemLabel composes name + surname", () => {
10
- assert.equal(
11
- resolveLookupItemLabel(
12
- {
13
- name: "South",
14
- surname: "Clinic"
15
- },
16
- "name"
17
- ),
18
- "South Clinic"
19
- );
20
- });
21
-
22
- test("resolveLookupItemLabel composes firstName + surname", () => {
23
- assert.equal(
24
- resolveLookupItemLabel(
25
- {
26
- firstName: "Ana",
27
- surname: "Marin"
28
- },
29
- "name"
30
- ),
31
- "Ana Marin"
32
- );
33
- });
34
-
35
- test("resolveLookupItemLabel composes firstName + lastName", () => {
36
- assert.equal(
37
- resolveLookupItemLabel(
38
- {
39
- firstName: "Ana",
40
- lastName: "Marin"
41
- },
42
- "name"
43
- ),
44
- "Ana Marin"
45
- );
46
- });
47
-
48
- test("resolveLookupItemLabel falls back to explicit labelKey", () => {
49
- assert.equal(
50
- resolveLookupItemLabel(
51
- {
52
- clinicName: "Harbor Vet"
53
- },
54
- "clinicName"
55
- ),
56
- "Harbor Vet"
57
- );
58
- });
59
-
60
- test("resolveLookupItemLabel resolves name when surname is missing", () => {
61
- assert.equal(
62
- resolveLookupItemLabel(
63
- {
64
- name: "Harbor Vet"
65
- },
66
- ""
67
- ),
68
- "Harbor Vet"
69
- );
70
- });
71
-
72
- test("resolveLookupItemLabel returns empty when no label fields match", () => {
73
- assert.equal(resolveLookupItemLabel({ id: 42 }, "name"), "");
74
- });
75
-
76
- test("resolveRecordTitle composes name-like fields before fallback key", () => {
77
- assert.equal(
78
- resolveRecordTitle(
79
- {
80
- firstName: "Ana",
81
- lastName: "Marin",
82
- title: "Ignored"
83
- },
84
- {
85
- fallbackKey: "title",
86
- defaultValue: "Record"
87
- }
88
- ),
89
- "Ana Marin"
90
- );
91
- });
92
-
93
- test("resolveRecordTitle falls back to provided field key", () => {
94
- assert.equal(
95
- resolveRecordTitle(
96
- {
97
- title: "Harbor Visit"
98
- },
99
- {
100
- fallbackKey: "title",
101
- defaultValue: "Record"
102
- }
103
- ),
104
- "Harbor Visit"
105
- );
106
- });
107
-
108
- test('resolveRecordTitle falls back to "-" when no title data exists', () => {
109
- assert.equal(resolveRecordTitle({}, { fallbackKey: "title", defaultValue: "" }), "-");
110
- });
111
-
112
- test("resolveLookupFieldDisplayValue returns hydrated label for lookup fields", () => {
113
- assert.equal(
114
- resolveLookupFieldDisplayValue(
115
- {
116
- vetId: 17,
117
- lookups: {
118
- vetId: {
119
- id: 17,
120
- name: "Harbor Vet"
121
- }
122
- }
123
- },
124
- {
125
- key: "vetId",
126
- relation: {
127
- kind: "lookup",
128
- valueKey: "id",
129
- labelKey: "name"
130
- }
131
- }
132
- ),
133
- "Harbor Vet"
134
- );
135
- });
136
-
137
- test("resolveLookupFieldDisplayValue falls back to hydrated valueKey", () => {
138
- assert.equal(
139
- resolveLookupFieldDisplayValue(
140
- {
141
- vetId: 17,
142
- lookups: {
143
- vetId: {
144
- id: 99
145
- }
146
- }
147
- },
148
- {
149
- key: "vetId",
150
- relation: {
151
- kind: "lookup",
152
- valueKey: "id",
153
- labelKey: "name"
154
- }
155
- }
156
- ),
157
- 99
158
- );
159
- });
160
-
161
- test("resolveLookupFieldDisplayValue falls back to raw id when lookup is not hydrated", () => {
162
- assert.equal(
163
- resolveLookupFieldDisplayValue(
164
- {
165
- vetId: 17
166
- },
167
- {
168
- key: "vetId",
169
- relation: {
170
- kind: "lookup",
171
- valueKey: "id",
172
- labelKey: "name"
173
- }
174
- }
175
- ),
176
- 17
177
- );
178
- });
179
-
180
- test("resolveLookupFieldDisplayValue supports custom lookup container key", () => {
181
- assert.equal(
182
- resolveLookupFieldDisplayValue(
183
- {
184
- vetId: 17,
185
- lookupData: {
186
- vetId: {
187
- id: 17,
188
- name: "Harbor Vet"
189
- }
190
- }
191
- },
192
- {
193
- key: "vetId",
194
- relation: {
195
- kind: "lookup",
196
- containerKey: "lookupData",
197
- valueKey: "id",
198
- labelKey: "name"
199
- }
200
- }
201
- ),
202
- "Harbor Vet"
203
- );
204
- });
205
-
206
- test("resolveLookupFieldDisplayValue falls back to alias-keyed lookups for relationship-backed fields", () => {
207
- assert.equal(
208
- resolveLookupFieldDisplayValue(
209
- {
210
- vetId: "17",
211
- lookups: {
212
- vet: {
213
- id: "17",
214
- name: "Harbor Vet"
215
- }
216
- }
217
- },
218
- {
219
- key: "vetId",
220
- relation: {
221
- kind: "lookup",
222
- valueKey: "id",
223
- labelKey: "name"
224
- }
225
- }
226
- ),
227
- "Harbor Vet"
228
- );
229
- });
230
-
231
- test("resolveLookupFieldDisplayValue returns raw value for non-lookup fields", () => {
232
- assert.equal(
233
- resolveLookupFieldDisplayValue(
234
- {
235
- firstName: "Ana"
236
- },
237
- {
238
- key: "firstName"
239
- }
240
- ),
241
- "Ana"
242
- );
243
- });
244
-
245
- test("resolveLookupFieldDisplayValue supports scalar lookup descriptor arguments", () => {
246
- assert.equal(
247
- resolveLookupFieldDisplayValue(
248
- {
249
- vetId: 17,
250
- lookups: {
251
- vetId: {
252
- id: 17,
253
- name: "Harbor Vet"
254
- }
255
- }
256
- },
257
- "vetId",
258
- "lookup",
259
- "id",
260
- "name"
261
- ),
262
- "Harbor Vet"
263
- );
264
- });
@@ -1,117 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import path from "node:path";
3
- import test from "node:test";
4
- import { readFile } from "node:fs/promises";
5
- import { fileURLToPath } from "node:url";
6
-
7
- const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
8
- const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
9
-
10
- async function readComponent(name) {
11
- return readFile(path.join(PACKAGE_DIR, "src", "client", "components", name), "utf8");
12
- }
13
-
14
- async function readClientFile(...parts) {
15
- return readFile(path.join(PACKAGE_DIR, "src", "client", ...parts), "utf8");
16
- }
17
-
18
- test("CRUD screen components own generated list/view/form chrome centrally", async () => {
19
- const listSource = await readComponent("CrudListScreen.vue");
20
- const viewSource = await readComponent("CrudViewScreen.vue");
21
- const addEditSource = await readComponent("CrudAddEditScreen.vue");
22
- const deleteActionSource = await readComponent("CrudDeleteAction.vue");
23
-
24
- assert.match(listSource, /CrudListBulkActionSurface/);
25
- assert.match(listSource, /CrudListFilterSurface/);
26
- assert.match(listSource, /CrudListRecordActionMenu/);
27
- assert.match(listSource, /class="ui-generator-list-cards"/);
28
- assert.match(listSource, /class="ui-generator-list-table"/);
29
- assert.match(listSource, /class="ui-generator-list-fab"/);
30
- assert.doesNotMatch(listSource, /ui-generator-list-cards d-md-none/);
31
- assert.doesNotMatch(listSource, /ui-generator-list-table d-none d-md-block/);
32
- assert.doesNotMatch(listSource, /class="ui-generator-list-fab d-md-none"/);
33
- assert.match(listSource, /\.ui-generator-list-table\s*\{\s*display:\s*none;/);
34
- assert.match(
35
- listSource,
36
- /@media \(min-width: 960px\)[\s\S]*\.ui-generator-list-cards\s*\{\s*display:\s*none;[\s\S]*\.ui-generator-list-table\s*\{\s*display:\s*block;[\s\S]*\.ui-generator-list-fab\s*\{\s*display:\s*none;/
37
- );
38
- assert.match(listSource, /button-label="More"/);
39
- assert.match(listSource, /selectableRows/);
40
- assert.match(
41
- listSource,
42
- /\.ui-generator-list-element :deep\(\.v-btn\)\s*\{\s*min-height:\s*48px;/
43
- );
44
- assert.match(listSource, /<slot[\s\S]*name="card-fields"/);
45
- assert.match(listSource, /<slot name="table-header"/);
46
- assert.match(listSource, /<slot[\s\S]*name="table-row"/);
47
- assert.match(listSource, /:row="row"/);
48
-
49
- assert.match(viewSource, /generated-ui-screen generated-ui-screen--operator ui-generator-view-element/);
50
- assert.match(viewSource, /ui-generator-view-panel/);
51
- assert.match(viewSource, /@click="view\.refresh"/);
52
- assert.match(viewSource, /<slot name="actions" :screen="screen" :view="view" \/>/);
53
- assert.match(viewSource, /<slot name="before-fields"/);
54
- assert.match(viewSource, /<slot name="fields"/);
55
- assert.match(viewSource, /<slot name="after-fields"/);
56
- assert.match(viewSource, /supporting-content/);
57
-
58
- assert.match(addEditSource, /generated-ui-screen generated-ui-screen--operator ui-generator-add-edit-form/);
59
- assert.match(addEditSource, /addEdit\.canRetryLoad/);
60
- assert.match(addEditSource, /@click="addEdit\.refresh"/);
61
- assert.match(addEditSource, /<slot[\s\S]*name="fields"/);
62
-
63
- assert.match(deleteActionSource, /@click="action\.request"/);
64
- assert.match(deleteActionSource, /@click="action\.confirm"/);
65
- assert.match(deleteActionSource, /@click="closeDialog"/);
66
- assert.doesNotMatch(deleteActionSource, /\bactivator=/);
67
- });
68
-
69
- test("CRUD screen composables expose generated page extension inputs", async () => {
70
- const listSource = await readClientFile("composables", "useCrudListScreen.js");
71
- const viewSource = await readClientFile("composables", "useCrudViewScreen.js");
72
-
73
- assert.match(listSource, /listRowActions = \[\]/);
74
- assert.match(listSource, /syntheticRows = null/);
75
- assert.match(listSource, /readEnabled = true/);
76
- assert.match(listSource, /useCrudListRowActions/);
77
- assert.match(listSource, /selectableRows/);
78
- assert.match(listSource, /readEnabled,\n\s+requestRecoveryLabel/);
79
-
80
- assert.match(viewSource, /requestQueryParams = null/);
81
- assert.match(viewSource, /readEnabled = true/);
82
- assert.match(viewSource, /queryKeyFactory = null/);
83
- assert.match(viewSource, /requestQueryParams/);
84
- assert.match(viewSource, /readEnabled/);
85
- });
86
-
87
- test("account settings load state exposes a local retry action", async () => {
88
- const source = await readComponent("AccountSettingsClientElement.vue");
89
-
90
- assert.match(source, /settingsLoadError/);
91
- assert.match(source, /@click="runtime\.refreshSettings"/);
92
- });
93
-
94
- test("CRUD screen composables are importable package APIs", async () => {
95
- const [
96
- listModule,
97
- viewModule,
98
- addEditModule,
99
- rowActionsModule,
100
- rowActionsRuntimeModule,
101
- deleteActionModule
102
- ] = await Promise.all([
103
- import("@jskit-ai/users-web/client/composables/useCrudListScreen"),
104
- import("@jskit-ai/users-web/client/composables/useCrudViewScreen"),
105
- import("@jskit-ai/users-web/client/composables/useCrudAddEditScreen"),
106
- import("@jskit-ai/users-web/client/rowActions"),
107
- import("@jskit-ai/users-web/client/composables/useCrudListRowActions"),
108
- import("@jskit-ai/users-web/client/composables/useCrudDeleteAction")
109
- ]);
110
-
111
- assert.equal(typeof listModule.useCrudListScreen, "function");
112
- assert.equal(typeof viewModule.useCrudViewScreen, "function");
113
- assert.equal(typeof addEditModule.useCrudAddEditScreen, "function");
114
- assert.equal(typeof rowActionsModule.defineCrudListRowActions, "function");
115
- assert.equal(typeof rowActionsRuntimeModule.useCrudListRowActions, "function");
116
- assert.equal(typeof deleteActionModule.useCrudDeleteAction, "function");
117
- });
@@ -1,31 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import path from "node:path";
3
- import test from "node:test";
4
- import { readFile } from "node:fs/promises";
5
- import { fileURLToPath } from "node:url";
6
-
7
- const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
8
- const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
9
-
10
- async function readClientSource(relativePath) {
11
- return readFile(path.join(PACKAGE_DIR, "src", "client", relativePath), "utf8");
12
- }
13
-
14
- test("users-web reports load errors as local resource-load intent by default", async () => {
15
- const source = await readClientSource("composables/runtime/operationUiHelpers.js");
16
-
17
- assert.match(source, /loadChannel = ""/);
18
- assert.match(source, /notFoundChannel = ""/);
19
- assert.match(source, /intent = "resource-load"/);
20
- assert.match(source, /intent: "resource-load"/);
21
- assert.doesNotMatch(source, /loadChannel = "banner"/);
22
- });
23
-
24
- test("users-web action feedback lets shell policy choose snackbar presentation", async () => {
25
- const source = await readClientSource("composables/runtime/useUiFeedback.js");
26
-
27
- assert.match(source, /successChannel = ""/);
28
- assert.match(source, /errorChannel = ""/);
29
- assert.match(source, /intent: "action-feedback"/);
30
- assert.doesNotMatch(source, /errorChannel = "banner"/);
31
- });