@jskit-ai/users-web 0.1.178-oldline.2 → 0.1.178

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 (124) hide show
  1. package/package.json +197 -45
  2. package/patterns/account-settings/PATTERN.md +61 -0
  3. package/src/client/bootstrap/user-bootstrap-handler.js +2 -12
  4. package/src/client/composables/account-settings/accountSettingsAvatarUploadRuntime.js +2 -2
  5. package/src/client/composables/useAccountSettingsRuntime.js +3 -3
  6. package/src/client/index.js +0 -16
  7. package/src/client/providers/UsersWebClientProvider.js +13 -14
  8. package/test/bootstrap.test.js +6 -41
  9. package/test/exportsContract.test.js +3 -29
  10. package/test/packageMetadataOwnership.test.js +19 -0
  11. package/test/settingsPlacementContract.test.js +93 -405
  12. package/fixtures/crud-list-date-filters/App.vue +0 -107
  13. package/fixtures/crud-list-date-filters/index.html +0 -12
  14. package/fixtures/crud-list-date-filters/main.js +0 -24
  15. package/fixtures/crud-list-date-filters/vite.config.mjs +0 -18
  16. package/package.descriptor.mjs +0 -468
  17. package/src/client/bulkActions.js +0 -47
  18. package/src/client/components/CrudAddEditScreen.vue +0 -186
  19. package/src/client/components/CrudListBulkActionSurface.vue +0 -126
  20. package/src/client/components/CrudListDateFilterControl.vue +0 -188
  21. package/src/client/components/CrudListFilterSurface.vue +0 -388
  22. package/src/client/components/CrudListRecordActionMenu.vue +0 -120
  23. package/src/client/components/CrudListScreen.vue +0 -501
  24. package/src/client/components/CrudViewScreen.vue +0 -197
  25. package/src/client/composables/crud/crudBindingSupport.js +0 -75
  26. package/src/client/composables/crud/crudHttpClientSupport.js +0 -57
  27. package/src/client/composables/crud/crudJsonApiTransportSupport.js +0 -145
  28. package/src/client/composables/crud/crudLookupFieldLabelSupport.js +0 -151
  29. package/src/client/composables/crud/crudLookupFieldRuntime.js +0 -251
  30. package/src/client/composables/crud/crudSchemaFormHelpers.js +0 -464
  31. package/src/client/composables/internal/crudListFilterLookupSupport.js +0 -161
  32. package/src/client/composables/internal/crudListParentTitleSupport.js +0 -171
  33. package/src/client/composables/internal/useOperationScope.js +0 -144
  34. package/src/client/composables/records/useAddEdit.js +0 -224
  35. package/src/client/composables/records/useCrudAddEdit.js +0 -227
  36. package/src/client/composables/records/useCrudList.js +0 -101
  37. package/src/client/composables/records/useCrudView.js +0 -47
  38. package/src/client/composables/records/useList.js +0 -500
  39. package/src/client/composables/records/useView.js +0 -164
  40. package/src/client/composables/runtime/addEditUiRuntime.js +0 -130
  41. package/src/client/composables/runtime/listUiRuntime.js +0 -123
  42. package/src/client/composables/runtime/modelStateHelpers.js +0 -90
  43. package/src/client/composables/runtime/operationAdapters.js +0 -34
  44. package/src/client/composables/runtime/operationUiHelpers.js +0 -125
  45. package/src/client/composables/runtime/operationValidationHelpers.js +0 -51
  46. package/src/client/composables/runtime/useAddEditCore.js +0 -112
  47. package/src/client/composables/runtime/useCommandCore.js +0 -123
  48. package/src/client/composables/runtime/useEndpointResource.js +0 -199
  49. package/src/client/composables/runtime/useFieldErrorBag.js +0 -61
  50. package/src/client/composables/runtime/useListCore.js +0 -112
  51. package/src/client/composables/runtime/useUiFeedback.js +0 -98
  52. package/src/client/composables/runtime/useViewCore.js +0 -93
  53. package/src/client/composables/runtime/viewUiRuntime.js +0 -96
  54. package/src/client/composables/support/errorMessageHelpers.js +0 -66
  55. package/src/client/composables/support/listQueryParamSupport.js +0 -473
  56. package/src/client/composables/support/listSearchSupport.js +0 -70
  57. package/src/client/composables/support/refValueHelpers.js +0 -19
  58. package/src/client/composables/support/requestQueryRuntimeSupport.js +0 -141
  59. package/src/client/composables/support/resourceLoadStateHelpers.js +0 -229
  60. package/src/client/composables/support/routeTemplateHelpers.js +0 -285
  61. package/src/client/composables/support/scopeHelpers.js +0 -145
  62. package/src/client/composables/useAccess.js +0 -109
  63. package/src/client/composables/useCommand.js +0 -118
  64. package/src/client/composables/useCrudAddEditScreen.js +0 -88
  65. package/src/client/composables/useCrudDeleteAction.js +0 -160
  66. package/src/client/composables/useCrudListBulkActions.js +0 -147
  67. package/src/client/composables/useCrudListFilterLookups.js +0 -146
  68. package/src/client/composables/useCrudListFilters.js +0 -339
  69. package/src/client/composables/useCrudListParentTitle.js +0 -151
  70. package/src/client/composables/useCrudListRowActions.js +0 -144
  71. package/src/client/composables/useCrudListScreen.js +0 -237
  72. package/src/client/composables/useCrudViewScreen.js +0 -77
  73. package/src/client/composables/usePagedCollection.js +0 -181
  74. package/src/client/composables/usePaths.js +0 -120
  75. package/src/client/composables/useRealtimeQueryInvalidation.js +0 -156
  76. package/src/client/composables/useScopeRuntime.js +0 -135
  77. package/src/client/composables/useSurfaceRouteContext.js +0 -25
  78. package/src/client/filters.js +0 -15
  79. package/src/client/lib/httpClient.js +0 -47
  80. package/src/client/lib/permissions.js +0 -34
  81. package/src/client/rowActions.js +0 -47
  82. package/src/client/support/contractGuards.js +0 -34
  83. package/src/client/support/crudListDateFilterSupport.js +0 -91
  84. package/test/addEditUiRuntime.test.js +0 -110
  85. package/test/crudBindingSupport.test.js +0 -110
  86. package/test/crudHttpClientSupport.test.js +0 -77
  87. package/test/crudJsonApiTransportSupport.test.js +0 -179
  88. package/test/crudListBulkActionSurface.test.js +0 -27
  89. package/test/crudListDateFilterSupport.test.js +0 -92
  90. package/test/crudListDateFilterSurface.browser.test.js +0 -273
  91. package/test/crudListFilterSurface.test.js +0 -90
  92. package/test/crudLookupFieldRuntime.test.js +0 -264
  93. package/test/crudScreenComponents.test.js +0 -108
  94. package/test/errorIntentContract.test.js +0 -31
  95. package/test/errorMessageHelpers.test.js +0 -28
  96. package/test/listQueryParamSupport.test.js +0 -223
  97. package/test/listUiRuntime.test.js +0 -102
  98. package/test/operationValidationHelpers.test.js +0 -64
  99. package/test/packageDescriptorOwnership.test.js +0 -18
  100. package/test/permissions.test.js +0 -35
  101. package/test/refValueHelpers.test.js +0 -14
  102. package/test/requestTransportOptions.test.js +0 -457
  103. package/test/resourceLoadStateHelpers.test.js +0 -189
  104. package/test/routeTemplateHelpers.test.js +0 -85
  105. package/test/scopeHelpers.test.js +0 -57
  106. package/test/useAddEditCore.test.js +0 -124
  107. package/test/useAddEditRequestQueryParams.test.js +0 -190
  108. package/test/useCrudAddEdit.test.js +0 -435
  109. package/test/useCrudDeleteAction.test.js +0 -139
  110. package/test/useCrudListBulkActions.test.js +0 -65
  111. package/test/useCrudListFilterLookups.test.js +0 -114
  112. package/test/useCrudListFilters.test.js +0 -293
  113. package/test/useCrudListParentTitle.test.js +0 -262
  114. package/test/useCrudListRowActions.test.js +0 -77
  115. package/test/useListRouteInitialization.test.js +0 -337
  116. package/test/useListSearchSupport.test.js +0 -61
  117. package/test/usePagedCollection.test.js +0 -53
  118. package/test/useViewRequestQueryParams.test.js +0 -20
  119. package/test/viewCoreLoading.test.js +0 -44
  120. package/test/viewUiRuntime.test.js +0 -95
  121. /package/{templates → patterns/account-settings/example}/src/components/account/settings/AccountSettingsNotificationsSection.vue +0 -0
  122. /package/{templates → patterns/account-settings/example}/src/components/account/settings/AccountSettingsPreferencesSection.vue +0 -0
  123. /package/{templates → patterns/account-settings/example}/src/components/account/settings/AccountSettingsProfileSection.vue +0 -0
  124. /package/{templates → patterns/account-settings/example}/src/pages/account/index.vue +0 -0
@@ -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,108 +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
-
23
- assert.match(listSource, /CrudListBulkActionSurface/);
24
- assert.match(listSource, /CrudListFilterSurface/);
25
- assert.match(listSource, /CrudListRecordActionMenu/);
26
- assert.match(listSource, /class="ui-generator-list-cards"/);
27
- assert.match(listSource, /class="ui-generator-list-table"/);
28
- assert.match(listSource, /class="ui-generator-list-fab"/);
29
- assert.doesNotMatch(listSource, /ui-generator-list-cards d-md-none/);
30
- assert.doesNotMatch(listSource, /ui-generator-list-table d-none d-md-block/);
31
- assert.doesNotMatch(listSource, /class="ui-generator-list-fab d-md-none"/);
32
- assert.match(listSource, /\.ui-generator-list-table\s*\{\s*display:\s*none;/);
33
- assert.match(
34
- listSource,
35
- /@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;/
36
- );
37
- assert.match(listSource, /button-label="More"/);
38
- assert.match(listSource, /selectableRows/);
39
- assert.match(listSource, /min-height:\s*48px/);
40
- assert.match(listSource, /<slot[\s\S]*name="card-fields"/);
41
- assert.match(listSource, /<slot name="table-header"/);
42
- assert.match(listSource, /<slot[\s\S]*name="table-row"/);
43
- assert.match(listSource, /:row="row"/);
44
-
45
- assert.match(viewSource, /generated-ui-screen generated-ui-screen--operator ui-generator-view-element/);
46
- assert.match(viewSource, /ui-generator-view-panel/);
47
- assert.match(viewSource, /@click="view\.refresh"/);
48
- assert.match(viewSource, /<slot name="actions" :screen="screen" :view="view" \/>/);
49
- assert.match(viewSource, /<slot name="before-fields"/);
50
- assert.match(viewSource, /<slot name="fields"/);
51
- assert.match(viewSource, /<slot name="after-fields"/);
52
- assert.match(viewSource, /supporting-content/);
53
-
54
- assert.match(addEditSource, /generated-ui-screen generated-ui-screen--operator ui-generator-add-edit-form/);
55
- assert.match(addEditSource, /addEdit\.canRetryLoad/);
56
- assert.match(addEditSource, /@click="addEdit\.refresh"/);
57
- assert.match(addEditSource, /<slot[\s\S]*name="fields"/);
58
- });
59
-
60
- test("CRUD screen composables expose generated page extension inputs", async () => {
61
- const listSource = await readClientFile("composables", "useCrudListScreen.js");
62
- const viewSource = await readClientFile("composables", "useCrudViewScreen.js");
63
-
64
- assert.match(listSource, /listRowActions = \[\]/);
65
- assert.match(listSource, /syntheticRows = null/);
66
- assert.match(listSource, /readEnabled = true/);
67
- assert.match(listSource, /useCrudListRowActions/);
68
- assert.match(listSource, /selectableRows/);
69
- assert.match(listSource, /readEnabled,\n\s+requestRecoveryLabel/);
70
-
71
- assert.match(viewSource, /requestQueryParams = null/);
72
- assert.match(viewSource, /readEnabled = true/);
73
- assert.match(viewSource, /queryKeyFactory = null/);
74
- assert.match(viewSource, /requestQueryParams/);
75
- assert.match(viewSource, /readEnabled/);
76
- });
77
-
78
- test("account settings load state exposes a local retry action", async () => {
79
- const source = await readComponent("AccountSettingsClientElement.vue");
80
-
81
- assert.match(source, /settingsLoadError/);
82
- assert.match(source, /@click="runtime\.refreshSettings"/);
83
- });
84
-
85
- test("CRUD screen composables are importable package APIs", async () => {
86
- const [
87
- listModule,
88
- viewModule,
89
- addEditModule,
90
- rowActionsModule,
91
- rowActionsRuntimeModule,
92
- deleteActionModule
93
- ] = await Promise.all([
94
- import("@jskit-ai/users-web/client/composables/useCrudListScreen"),
95
- import("@jskit-ai/users-web/client/composables/useCrudViewScreen"),
96
- import("@jskit-ai/users-web/client/composables/useCrudAddEditScreen"),
97
- import("@jskit-ai/users-web/client/rowActions"),
98
- import("@jskit-ai/users-web/client/composables/useCrudListRowActions"),
99
- import("@jskit-ai/users-web/client/composables/useCrudDeleteAction")
100
- ]);
101
-
102
- assert.equal(typeof listModule.useCrudListScreen, "function");
103
- assert.equal(typeof viewModule.useCrudViewScreen, "function");
104
- assert.equal(typeof addEditModule.useCrudAddEditScreen, "function");
105
- assert.equal(typeof rowActionsModule.defineCrudListRowActions, "function");
106
- assert.equal(typeof rowActionsRuntimeModule.useCrudListRowActions, "function");
107
- assert.equal(typeof deleteActionModule.useCrudDeleteAction, "function");
108
- });
@@ -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
- });
@@ -1,28 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { toQueryErrorMessage, toUiErrorMessage } from "../src/client/composables/support/errorMessageHelpers.js";
4
-
5
- test("toQueryErrorMessage returns empty when query has no error", () => {
6
- assert.equal(toQueryErrorMessage(null, "Unable to load list."), "");
7
- });
8
-
9
- test("toQueryErrorMessage resolves specific runtime error messages before fallback", () => {
10
- assert.equal(toQueryErrorMessage({ message: "Network timeout" }, "Unable to load list."), "Network timeout");
11
- assert.equal(toQueryErrorMessage({}, "Unable to load list."), "Unable to load list.");
12
- });
13
-
14
- test("toQueryErrorMessage prefers fallback for generic transport messages", () => {
15
- assert.equal(
16
- toQueryErrorMessage({ status: 500, message: "Request failed with status 500." }, "Unable to load list."),
17
- "Unable to load list."
18
- );
19
- assert.equal(
20
- toQueryErrorMessage({ status: 0, message: "Network request failed." }, "Unable to load list."),
21
- "Unable to load list."
22
- );
23
- });
24
-
25
- test("toUiErrorMessage prefers fallback copy before runtime error text", () => {
26
- assert.equal(toUiErrorMessage({ message: "Server exploded" }, "Unable to save."), "Unable to save.");
27
- assert.equal(toUiErrorMessage({ message: "Server exploded" }, ""), "Server exploded");
28
- });
@@ -1,223 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { ref } from "vue";
4
- import {
5
- normalizeListSyncToRouteConfig,
6
- resolveQueryParamDescriptors,
7
- resolveActiveQueryParamEntries,
8
- resolveWritableQueryParamBindings,
9
- buildQueryParamEntriesToken,
10
- parseRouteBindingValue,
11
- areQueryParamBindingValuesEqual,
12
- buildRouteQueryCompareToken,
13
- mergeManagedQueryParamKeyHistory,
14
- resolveRouteSyncManagedKeys
15
- } from "../src/client/composables/support/listQueryParamSupport.js";
16
-
17
- test("normalizeListSyncToRouteConfig defaults", () => {
18
- assert.deepEqual(
19
- normalizeListSyncToRouteConfig(false, {
20
- defaultSearchParam: "search"
21
- }),
22
- {
23
- enabled: false,
24
- mode: "replace",
25
- syncSearch: false,
26
- syncQueryParams: false,
27
- hydrateFromRoute: false,
28
- searchParam: "search",
29
- queryParamBlacklist: []
30
- }
31
- );
32
-
33
- assert.deepEqual(
34
- normalizeListSyncToRouteConfig(true),
35
- {
36
- enabled: true,
37
- mode: "replace",
38
- syncSearch: true,
39
- syncQueryParams: true,
40
- hydrateFromRoute: true,
41
- searchParam: "q",
42
- queryParamBlacklist: []
43
- }
44
- );
45
-
46
- assert.deepEqual(
47
- normalizeListSyncToRouteConfig({
48
- enabled: true,
49
- queryParamBlacklist: [" include ", "cursor", "include", ""]
50
- }),
51
- {
52
- enabled: true,
53
- mode: "replace",
54
- syncSearch: true,
55
- syncQueryParams: true,
56
- hydrateFromRoute: true,
57
- searchParam: "q",
58
- queryParamBlacklist: ["include", "cursor"]
59
- }
60
- );
61
- });
62
-
63
- test("resolveQueryParamDescriptors keeps declared keys and supports writable bindings", () => {
64
- const source = {
65
- " status ": ref("open"),
66
- " count ": 2,
67
- includeArchived: ref(false),
68
- empty: ""
69
- };
70
-
71
- const descriptors = resolveQueryParamDescriptors(source);
72
- assert.deepEqual(
73
- descriptors.map((descriptor) => descriptor.key),
74
- ["count", "empty", "includeArchived", "status"]
75
- );
76
-
77
- const activeEntries = resolveActiveQueryParamEntries(descriptors);
78
- assert.deepEqual(
79
- activeEntries,
80
- [
81
- {
82
- key: "count",
83
- values: ["2"]
84
- },
85
- {
86
- key: "status",
87
- values: ["open"]
88
- }
89
- ]
90
- );
91
-
92
- const bindings = resolveWritableQueryParamBindings(descriptors);
93
- const countBinding = bindings.find((binding) => binding.key === "count");
94
- assert.ok(countBinding);
95
- countBinding.set(9);
96
- assert.equal(source[" count "], 9);
97
- assert.equal(source.count, undefined);
98
- });
99
-
100
- test("buildQueryParamEntriesToken and compare token stay deterministic", () => {
101
- assert.equal(
102
- buildQueryParamEntriesToken([
103
- {
104
- key: "status",
105
- values: ["open"]
106
- },
107
- {
108
- key: "tags",
109
- values: ["a", "b"]
110
- }
111
- ]),
112
- "status=open&tags=a,b"
113
- );
114
-
115
- assert.equal(
116
- buildRouteQueryCompareToken({
117
- b: "2",
118
- a: ["1"]
119
- }),
120
- buildRouteQueryCompareToken({
121
- a: "1",
122
- b: ["2"]
123
- })
124
- );
125
- });
126
-
127
- test("parseRouteBindingValue handles boolean, number and array bindings", () => {
128
- const booleanBinding = {
129
- valueType: "boolean",
130
- get: () => false
131
- };
132
- assert.equal(parseRouteBindingValue(booleanBinding, "1"), true);
133
- assert.equal(parseRouteBindingValue(booleanBinding, undefined), false);
134
-
135
- const numberBinding = {
136
- valueType: "number",
137
- get: () => 7
138
- };
139
- assert.equal(parseRouteBindingValue(numberBinding, "12"), 12);
140
- assert.equal(parseRouteBindingValue(numberBinding, "bad"), 7);
141
- assert.equal(parseRouteBindingValue(numberBinding, undefined), null);
142
-
143
- const arrayBinding = {
144
- valueType: "array",
145
- arrayItemType: "number",
146
- get: () => []
147
- };
148
- assert.deepEqual(
149
- parseRouteBindingValue(arrayBinding, ["1", "bad", "3"]),
150
- [1, 3]
151
- );
152
- });
153
-
154
- test("route-aware query parameter bindings can distinguish initial hydration from later clearing", () => {
155
- const currentness = ref("active");
156
- const [binding] = resolveWritableQueryParamBindings(
157
- resolveQueryParamDescriptors(
158
- { currentness },
159
- {},
160
- {
161
- routeValueResolvers: {
162
- currentness(routeValue, { initial }) {
163
- if (routeValue === undefined) {
164
- return initial ? "active" : "";
165
- }
166
- return String(routeValue || "").trim() || "active";
167
- }
168
- }
169
- }
170
- )
171
- );
172
-
173
- assert.equal(
174
- parseRouteBindingValue(binding, undefined, { initial: true }),
175
- "active"
176
- );
177
- assert.equal(
178
- parseRouteBindingValue(binding, "archived", { initial: true }),
179
- "archived"
180
- );
181
- assert.equal(
182
- parseRouteBindingValue(binding, undefined, { initial: false }),
183
- ""
184
- );
185
- });
186
-
187
- test("route sync key helpers preserve declared key history for cleanup", () => {
188
- const history = mergeManagedQueryParamKeyHistory(
189
- ["status"],
190
- ["assignee", "status"]
191
- );
192
- assert.deepEqual(history, ["assignee", "status"]);
193
-
194
- assert.deepEqual(
195
- resolveRouteSyncManagedKeys({
196
- searchEnabled: true,
197
- searchParam: "q",
198
- syncSearch: true,
199
- syncQueryParams: true,
200
- declaredKeys: ["status"],
201
- keyHistory: ["assignee"]
202
- }),
203
- ["assignee", "q", "status"]
204
- );
205
- });
206
-
207
- test("areQueryParamBindingValuesEqual handles arrays and dates", () => {
208
- assert.equal(
209
- areQueryParamBindingValuesEqual([1, 2], [1, 2]),
210
- true
211
- );
212
- assert.equal(
213
- areQueryParamBindingValuesEqual([1, 2], [2, 1]),
214
- false
215
- );
216
- assert.equal(
217
- areQueryParamBindingValuesEqual(
218
- new Date("2026-01-01T00:00:00.000Z"),
219
- new Date("2026-01-01T00:00:00.000Z")
220
- ),
221
- true
222
- );
223
- });