@supersoniks/concorde 4.9.92 → 4.9.94

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 (111) hide show
  1. package/ai/cursor/rules/concorde.mdc +7 -0
  2. package/ai/jetbrains/rules/concorde.md +6 -0
  3. package/ai/skills/concorde/SKILL.md +48 -0
  4. package/build-infos.json +1 -1
  5. package/concorde-core.bundle.js +299 -292
  6. package/concorde-core.es.js +10139 -6035
  7. package/dist/concorde-core.bundle.js +299 -292
  8. package/dist/concorde-core.es.js +10139 -6035
  9. package/dist/docs-mock-api-sw.js +110 -0
  10. package/dist/docs-mock-api-sw.js.map +3 -3
  11. package/dist/js/core/components/functional/functional.js +1 -0
  12. package/dist/js/core/components/functional/functional.js.map +1 -1
  13. package/dist/js/core/components/functional/jsonata/jsonata.js +119 -0
  14. package/dist/js/core/components/functional/jsonata/jsonata.js.map +1 -0
  15. package/dist/js/core/components/ui/button/button.js +1 -1
  16. package/dist/js/core/components/ui/button/button.js.map +1 -1
  17. package/dist/js/core/decorators/Subscriber.js +1 -0
  18. package/dist/js/core/decorators/Subscriber.js.map +1 -1
  19. package/dist/js/core/decorators/api.js +108 -16
  20. package/dist/js/core/decorators/api.js.map +1 -1
  21. package/dist/js/core/decorators/subscriber/derive.js +60 -0
  22. package/dist/js/core/decorators/subscriber/derive.js.map +1 -0
  23. package/dist/js/core/utils/api.js +29 -8
  24. package/dist/js/core/utils/api.js.map +1 -1
  25. package/dist/js/core/utils/derive.js +246 -0
  26. package/dist/js/core/utils/derive.js.map +1 -0
  27. package/dist/js/core/utils/jsonataHelpers.js +69 -0
  28. package/dist/js/core/utils/jsonataHelpers.js.map +1 -0
  29. package/dist/js/core/utils/jsonataRuntime.js +75 -0
  30. package/dist/js/core/utils/jsonataRuntime.js.map +1 -0
  31. package/dist/js/utils.js +2 -0
  32. package/dist/js/utils.js.map +1 -1
  33. package/dist/types/core/components/functional/functional.d.ts +1 -0
  34. package/dist/types/core/components/functional/functional.d.ts.map +1 -1
  35. package/dist/types/core/components/functional/jsonata/jsonata.d.ts +29 -0
  36. package/dist/types/core/components/functional/jsonata/jsonata.d.ts.map +1 -0
  37. package/dist/types/core/components/ui/button/button.d.ts +1 -1
  38. package/dist/types/core/components/ui/button/button.d.ts.map +1 -1
  39. package/dist/types/core/components/ui/form/checkbox/checkbox.d.ts +3 -3
  40. package/dist/types/core/components/ui/form/input/input.d.ts +3 -3
  41. package/dist/types/core/components/ui/form/input-autocomplete/input-autocomplete.d.ts +3 -3
  42. package/dist/types/core/components/ui/form/textarea/textarea.d.ts +3 -3
  43. package/dist/types/core/decorators/Subscriber.d.ts +2 -0
  44. package/dist/types/core/decorators/Subscriber.d.ts.map +1 -1
  45. package/dist/types/core/decorators/api.d.ts +21 -12
  46. package/dist/types/core/decorators/api.d.ts.map +1 -1
  47. package/dist/types/core/decorators/subscriber/derive.d.ts +30 -0
  48. package/dist/types/core/decorators/subscriber/derive.d.ts.map +1 -0
  49. package/dist/types/core/utils/api.d.ts +1 -1
  50. package/dist/types/core/utils/api.d.ts.map +1 -1
  51. package/dist/types/core/utils/derive.d.ts +40 -0
  52. package/dist/types/core/utils/derive.d.ts.map +1 -0
  53. package/dist/types/core/utils/jsonataHelpers.d.ts +15 -0
  54. package/dist/types/core/utils/jsonataHelpers.d.ts.map +1 -0
  55. package/dist/types/core/utils/jsonataRuntime.d.ts +13 -0
  56. package/dist/types/core/utils/jsonataRuntime.d.ts.map +1 -0
  57. package/dist/types/mixins.d.ts +3 -3
  58. package/dist/types/utils.d.ts +2 -0
  59. package/dist/types/utils.d.ts.map +1 -1
  60. package/docs/assets/{index-DURr6O71.js → index-DpIkksht.js} +652 -479
  61. package/docs/crawl/docs/_decorators/get.html +27 -6
  62. package/docs/crawl/docs/_decorators/patch.html +6 -0
  63. package/docs/crawl/docs/_decorators/post.html +8 -0
  64. package/docs/crawl/docs/_decorators/put.html +6 -0
  65. package/docs/crawl/hubs/decorators.html +36 -31
  66. package/docs/crawl/hubs/remote-data.html +36 -31
  67. package/docs/index.html +1 -1
  68. package/docs/llms-full.txt +47 -3
  69. package/docs/src/docs/_decorators/get.md +31 -0
  70. package/docs/src/docs/_decorators/patch.md +9 -0
  71. package/docs/src/docs/_decorators/post.md +11 -0
  72. package/docs/src/docs/_decorators/put.md +9 -0
  73. package/docs/src/docs/search/docs-search.json +120 -5
  74. package/docs/src/tsconfig.json +268 -268
  75. package/package.json +28 -3
  76. package/public/docs-mock-api-sw.js +110 -0
  77. package/public/docs-mock-api-sw.js.map +3 -3
  78. package/src/core/components/functional/functional.ts +1 -0
  79. package/src/core/components/functional/jsonata/jsonata.md +56 -0
  80. package/src/core/components/functional/jsonata/jsonata.spec.ts +202 -0
  81. package/src/core/components/functional/jsonata/jsonata.ts +116 -0
  82. package/src/core/components/ui/button/button.ts +2 -2
  83. package/src/core/decorators/Subscriber.ts +2 -0
  84. package/src/core/decorators/api.post.spec.ts +99 -4
  85. package/src/core/decorators/api.spec.ts +90 -0
  86. package/src/core/decorators/api.ts +212 -92
  87. package/src/core/decorators/subscriber/derive.ts +95 -0
  88. package/src/core/utils/api.spec.ts +73 -0
  89. package/src/core/utils/api.ts +44 -11
  90. package/src/core/utils/derive.spec.ts +190 -0
  91. package/src/core/utils/derive.ts +357 -0
  92. package/src/core/utils/jsonataHelpers.ts +100 -0
  93. package/src/core/utils/jsonataRuntime.ts +88 -0
  94. package/src/docs/_core-concept/dataFlow.md +9 -0
  95. package/src/docs/_decorators/get.md +31 -0
  96. package/src/docs/_decorators/patch.md +9 -0
  97. package/src/docs/_decorators/post.md +11 -0
  98. package/src/docs/_decorators/put.md +9 -0
  99. package/src/docs/_misc/docs-mock-api.md +3 -0
  100. package/src/docs/components/docs-demo-sources.ts +40 -0
  101. package/src/docs/docs.ts +1 -0
  102. package/src/docs/example/decorators-demo-post.ts +70 -0
  103. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +59 -0
  104. package/src/docs/example/docs-jsonata-demos.ts +128 -0
  105. package/src/docs/mock-api/fixtures-jsonata.ts +131 -0
  106. package/src/docs/mock-api/router.ts +26 -0
  107. package/src/docs/navigation/navigation.ts +4 -0
  108. package/src/docs/search/docs-search.json +300 -5
  109. package/src/tsconfig.json +18 -0
  110. package/src/tsconfig.tsbuildinfo +1 -1
  111. package/src/utils.ts +13 -0
@@ -0,0 +1,202 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import TestUtils from "@supersoniks/concorde/test-utils/TestUtils";
3
+ import { DataProviderKey } from "@supersoniks/concorde/core/utils/dataProviderKey";
4
+ import { deriveStateKey } from "@supersoniks/concorde/core/utils/derive";
5
+ import { clearJsonataCache } from "@supersoniks/concorde/core/utils/jsonataRuntime";
6
+ import { dp, set } from "@supersoniks/concorde/utils";
7
+ import "./jsonata";
8
+ import "../states/states";
9
+ import {
10
+ DOCS_COFFEE_BAGS,
11
+ DOCS_SPACE_CAT_MISSIONS,
12
+ } from "../../../../docs/mock-api/fixtures-jsonata";
13
+
14
+ const missionsKey = new DataProviderKey<typeof DOCS_SPACE_CAT_MISSIONS>(
15
+ "jsonata-test-missions",
16
+ );
17
+ const formKey = new DataProviderKey<{ top: number | string }>(
18
+ "jsonata-test-form",
19
+ );
20
+ const boardKey = new DataProviderKey<
21
+ Array<{ name: string; points: number }>
22
+ >("jsonata-test-board");
23
+
24
+ function wait(ms: number) {
25
+ return new Promise((r) => setTimeout(r, ms));
26
+ }
27
+
28
+ afterEach(() => {
29
+ clearJsonataCache();
30
+ });
31
+
32
+ describe("sonic-jsonata", () => {
33
+ it("calcule un leaderboard (groupby + top N) en live", async () => {
34
+ set(missionsKey, DOCS_SPACE_CAT_MISSIONS);
35
+ set(formKey, { top: 2 });
36
+
37
+ const [elt] = TestUtils.bootstrap(`
38
+ <sonic-jsonata
39
+ inputProvider="jsonata-test-missions"
40
+ outputProvider="jsonata-test-board"
41
+ formDataProvider="jsonata-test-form"
42
+ expression='$input{name: $sum(points)} ~> $each(function($v, $k) {{"name": $k, "points": $v}}) ^(>points)[[0..$number($top)-1]]'
43
+ ></sonic-jsonata>
44
+ `);
45
+
46
+ await wait(80);
47
+ const result = dp(boardKey).get();
48
+ expect(result).toHaveLength(2);
49
+ expect(result[0].name).toBe("Nebula Paws");
50
+ expect(result[0].points).toBe(75);
51
+ expect(result[1].name).toBe("Captain Whiskers");
52
+ expect(dp(deriveStateKey(boardKey)).get().status).toBe("ready");
53
+
54
+ // Comme sonic-input / sonic-select : mutation profonde sur le form DP
55
+ dp(formKey).top.set(1);
56
+ await wait(80);
57
+ const top1 = dp(boardKey).get();
58
+ const rows = Array.isArray(top1) ? top1 : [top1];
59
+ expect(rows).toHaveLength(1);
60
+ expect(rows[0].name).toBe("Nebula Paws");
61
+
62
+ (elt as HTMLElement).remove();
63
+ });
64
+
65
+ it("agrège le stock café par origine avec filtre roast", async () => {
66
+ const bagsKey = new DataProviderKey<typeof DOCS_COFFEE_BAGS>(
67
+ "jsonata-test-bags",
68
+ );
69
+ const coffeeForm = new DataProviderKey<{ roast: string }>(
70
+ "jsonata-test-coffee-form",
71
+ );
72
+ const stockKey = new DataProviderKey<Array<{ origin: string; kg: number }>>(
73
+ "jsonata-test-stock",
74
+ );
75
+ set(bagsKey, DOCS_COFFEE_BAGS);
76
+ set(coffeeForm, { roast: "light" });
77
+
78
+ const [elt] = TestUtils.bootstrap(`
79
+ <sonic-jsonata
80
+ inputProvider="jsonata-test-bags"
81
+ outputProvider="jsonata-test-stock"
82
+ formDataProvider="jsonata-test-coffee-form"
83
+ expression='($roast = "all" ? $input : $input[roast=$roast]){origin: $sum(kg)} ~> $each(function($v, $k) {{"origin": $k, "kg": $v}}) ^(>kg)'
84
+ ></sonic-jsonata>
85
+ `);
86
+
87
+ await wait(80);
88
+ const stock = dp(stockKey).get();
89
+ expect(stock.length).toBeGreaterThan(0);
90
+ expect(stock[0].origin).toBe("Ethiopia");
91
+ expect(stock[0].kg).toBe(16);
92
+ expect(stock.map((r) => r.origin)).not.toContain("Brazil");
93
+
94
+ dp(coffeeForm).roast.set("all");
95
+ await wait(80);
96
+ const all = dp(stockKey).get();
97
+ expect(all.map((r) => r.origin)).toContain("Brazil");
98
+
99
+ (elt as HTMLElement).remove();
100
+ });
101
+
102
+ it("expose status error sur expression invalide", async () => {
103
+ const err = vi.spyOn(console, "error").mockImplementation(() => {});
104
+ set(missionsKey, DOCS_SPACE_CAT_MISSIONS);
105
+
106
+ const [elt] = TestUtils.bootstrap(`
107
+ <sonic-jsonata
108
+ inputProvider="jsonata-test-missions"
109
+ outputProvider="jsonata-test-board"
110
+ expression='$input ~> $$$$invalid'
111
+ ></sonic-jsonata>
112
+ `);
113
+
114
+ await wait(80);
115
+ const state = dp(deriveStateKey(boardKey)).get();
116
+ expect(state.status).toBe("error");
117
+ expect(state.error).toBeTruthy();
118
+ err.mockRestore();
119
+ (elt as HTMLElement).remove();
120
+ });
121
+
122
+ it("branche sonic-states sur le DP /_state_", async () => {
123
+ set(missionsKey, DOCS_SPACE_CAT_MISSIONS);
124
+ set(formKey, { top: 3 });
125
+
126
+ const [wrap] = TestUtils.bootstrap(`
127
+ <div>
128
+ <sonic-jsonata
129
+ inputProvider="jsonata-test-missions"
130
+ outputProvider="jsonata-test-board"
131
+ formDataProvider="jsonata-test-form"
132
+ expression='$input[0].name'
133
+ ></sonic-jsonata>
134
+ <sonic-states dataProvider="jsonata-test-board/_state_" data-path="status">
135
+ <template data-value="ready"><span id="ok">prêt</span></template>
136
+ <template data-value="error"><span id="ko">erreur</span></template>
137
+ </sonic-states>
138
+ </div>
139
+ `);
140
+
141
+ await wait(80);
142
+ expect(wrap.querySelector("#ok")?.textContent).toBe("prêt");
143
+ (wrap as HTMLElement).remove();
144
+ });
145
+
146
+ it("calcule total + moyenne par ligne d’un tableau", async () => {
147
+ const linesKey = new DataProviderKey<
148
+ Array<{ qty: number; unitPrice: number }>
149
+ >("jsonata-test-lines");
150
+ const taxForm = new DataProviderKey<{ taxPercent: number | string }>(
151
+ "jsonata-test-tax-form",
152
+ );
153
+ const totalsKey = new DataProviderKey<{
154
+ count: number;
155
+ subtotal: number;
156
+ average: number;
157
+ tax: number;
158
+ grandTotal: number;
159
+ }>("jsonata-test-totals");
160
+
161
+ set(linesKey, [
162
+ { qty: 4, unitPrice: 3.5 },
163
+ { qty: 2, unitPrice: 6 },
164
+ { qty: 1, unitPrice: 8.5 },
165
+ ]);
166
+ set(taxForm, { taxPercent: 10 });
167
+
168
+ const [elt] = TestUtils.bootstrap(`
169
+ <sonic-jsonata
170
+ inputProvider="jsonata-test-lines"
171
+ outputProvider="jsonata-test-totals"
172
+ formDataProvider="jsonata-test-tax-form"
173
+ expression='(
174
+ $lines := $input.(qty * unitPrice);
175
+ {
176
+ "count": $count($input),
177
+ "subtotal": $round($sum($lines), 2),
178
+ "average": $round($average($lines), 2),
179
+ "tax": $round($sum($lines) * $number($taxPercent) / 100, 2),
180
+ "grandTotal": $round($sum($lines) * (1 + $number($taxPercent) / 100), 2)
181
+ }
182
+ )'
183
+ ></sonic-jsonata>
184
+ `);
185
+
186
+ await wait(80);
187
+ expect(dp(totalsKey).get()).toMatchObject({
188
+ count: 3,
189
+ subtotal: 34.5,
190
+ average: 11.5,
191
+ tax: 3.45,
192
+ grandTotal: 37.95,
193
+ });
194
+
195
+ dp(taxForm).taxPercent.set(20);
196
+ await wait(80);
197
+ expect(dp(totalsKey).get().tax).toBe(6.9);
198
+ expect(dp(totalsKey).get().grandTotal).toBe(41.4);
199
+
200
+ (elt as HTMLElement).remove();
201
+ });
202
+ });
@@ -0,0 +1,116 @@
1
+ import { LitElement, html, css } from "lit";
2
+ import { customElement, property } from "lit/decorators.js";
3
+ import {
4
+ derive,
5
+ type DeriveHandle,
6
+ } from "@supersoniks/concorde/core/utils/derive";
7
+ import {
8
+ compileJsonata,
9
+ validateOutputSchema,
10
+ } from "@supersoniks/concorde/core/utils/jsonataRuntime";
11
+ import { DataProviderKey } from "@supersoniks/concorde/core/utils/dataProviderKey";
12
+
13
+ /**
14
+ * Adaptateur JSONata de `derive` : 1 entrée → expression → 1 sortie (readonly).
15
+ *
16
+ * Contexte d’évaluation :
17
+ * - `$input` = contenu de `inputProvider`
18
+ * - champs du `formDataProvider` optionnel = `$top`, `$roast`, …
19
+ */
20
+ @customElement("sonic-jsonata")
21
+ export default class SonicJsonata extends LitElement {
22
+ static styles = [
23
+ css`
24
+ :host {
25
+ display: contents;
26
+ }
27
+ `,
28
+ ];
29
+
30
+ /** DataProvider d’entrée (source). */
31
+ @property({ type: String })
32
+ inputProvider = "";
33
+
34
+ /** DataProvider de sortie (agrégat, readonly). */
35
+ @property({ type: String })
36
+ outputProvider = "";
37
+
38
+ /**
39
+ * Form / variables optionnelles (`$top`, `$roast`, …).
40
+ * Même id que le `formDataProvider` du formulaire — recalcule sans refetch.
41
+ */
42
+ @property({ type: String })
43
+ formDataProvider = "";
44
+
45
+ @property({ type: String })
46
+ expression = "";
47
+
48
+ @property({ type: Object })
49
+ outputSchema: Record<string, unknown> | null = null;
50
+
51
+ private handle: DeriveHandle | null = null;
52
+
53
+ connectedCallback(): void {
54
+ super.connectedCallback();
55
+ this.restart();
56
+ }
57
+
58
+ disconnectedCallback(): void {
59
+ this.handle?.stop();
60
+ this.handle = null;
61
+ super.disconnectedCallback();
62
+ }
63
+
64
+ protected updated(
65
+ changed: Map<string | number | symbol, unknown>,
66
+ ): void {
67
+ if (
68
+ changed.has("expression") ||
69
+ changed.has("inputProvider") ||
70
+ changed.has("outputProvider") ||
71
+ changed.has("formDataProvider") ||
72
+ changed.has("outputSchema")
73
+ ) {
74
+ this.restart();
75
+ }
76
+ }
77
+
78
+ private restart(): void {
79
+ this.handle?.stop();
80
+ this.handle = null;
81
+ if (!this.isConnected) return;
82
+ if (!this.inputProvider || !this.outputProvider) return;
83
+ if (!this.expression?.trim()) return;
84
+
85
+ const expressionSource = this.expression;
86
+ const schema = this.outputSchema;
87
+
88
+ this.handle = derive({
89
+ inputs: { input: new DataProviderKey(this.inputProvider) },
90
+ output: new DataProviderKey(this.outputProvider),
91
+ variables: this.formDataProvider
92
+ ? new DataProviderKey(this.formDataProvider)
93
+ : undefined,
94
+ compute: async ({ input }, vars) => {
95
+ const expr = compileJsonata(expressionSource);
96
+ const bindings: Record<string, unknown> = {
97
+ ...((vars ?? {}) as Record<string, unknown>),
98
+ input,
99
+ };
100
+ let result: unknown;
101
+ try {
102
+ result = await expr.evaluate({}, bindings);
103
+ } catch (err) {
104
+ const message = err instanceof Error ? err.message : String(err);
105
+ throw new Error(`[sonic-jsonata] Évaluation : ${message}`);
106
+ }
107
+ validateOutputSchema(result, schema);
108
+ return result;
109
+ },
110
+ });
111
+ }
112
+
113
+ render() {
114
+ return html`<slot></slot>`;
115
+ }
116
+ }
@@ -506,8 +506,8 @@ export class Button extends FormCheckable(FormElement(Subscriber(LitElement))) {
506
506
  * L'url
507
507
  */
508
508
  private _href: Route | string = "";
509
- @property({ type: String }) set href(value: Route | string) {
510
- this._href = value;
509
+ @property({ type: String }) set href(value: Route | string | null) {
510
+ this._href = value ?? "";
511
511
  const str = this._href.toString();
512
512
  if (str && str.indexOf("http") != 0) {
513
513
  LocationHandler.onChange(this);
@@ -4,6 +4,8 @@ export { subscribe } from "./subscriber/subscribe";
4
4
  export { onAssign } from "./subscriber/onAssign";
5
5
  export { handle, Skip } from "./subscriber/handle";
6
6
  export type { HandleOptions } from "./subscriber/handle";
7
+ export { derive } from "./subscriber/derive";
8
+ export type { DeriveDecoratorOptions } from "./subscriber/derive";
7
9
  export { autoSubscribe } from "./subscriber/autoSubscribe";
8
10
  export { autoFill } from "./subscriber/autoFill";
9
11
  export { ancestorAttribute } from "./subscriber/ancestorAttribute";
@@ -303,6 +303,91 @@ describe("post", () => {
303
303
  );
304
304
  expect(el.payload?.result).toEqual({ ok: true });
305
305
  });
306
+
307
+ it("transmet le payload complet et conserve le retrigger du POST", async () => {
308
+ const bodyPubId = "postSpecHandlerBody";
309
+ const triggerPubId = "postSpecHandlerTrigger";
310
+ PublisherManager.get(bodyPubId).set({ tick: 1 });
311
+ const bodyKey = new DataProviderKey<{ tick: number }>(bodyPubId);
312
+ const triggerKey = new DataProviderKey<number>(triggerPubId);
313
+ const ep = new Endpoint<{ ok: boolean }>("items/handler");
314
+ const localPost = post(ep, bodyKey, {
315
+ autoPostOnBodyMutation: false,
316
+ triggerKey,
317
+ });
318
+ const received: Array<ApiResult<{ ok: boolean }> | undefined> = [];
319
+ const errorPayload = mockPayload({ ok: false });
320
+ errorPayload.response = new Response(JSON.stringify({ error: true }), {
321
+ status: 409,
322
+ headers: { "Content-Type": "application/json" },
323
+ });
324
+ vi.mocked(API.prototype.postDetailed).mockResolvedValueOnce(
325
+ errorPayload as never,
326
+ );
327
+ const tag = "test-api-post-handler";
328
+
329
+ class C extends HTMLElement {
330
+ @localPost
331
+ onPayload(payload: ApiResult<{ ok: boolean }> | undefined) {
332
+ received.push(payload);
333
+ }
334
+ }
335
+ if (!customElements.get(tag)) {
336
+ customElements.define(tag, C);
337
+ }
338
+
339
+ const wrap = document.createElement("div");
340
+ wrap.setAttribute("serviceURL", "https://api.example.test");
341
+ const el = document.createElement(tag) as InstanceType<typeof C>;
342
+ wrap.appendChild(el);
343
+ document.body.appendChild(wrap);
344
+ await flushSendFrames();
345
+
346
+ expect(API.prototype.postDetailed).not.toHaveBeenCalled();
347
+ await localPost.send(el);
348
+ expect(received).toHaveLength(1);
349
+ expect(received[0]?.response?.status).toBe(409);
350
+
351
+ PublisherManager.get(triggerPubId).invalidate();
352
+ await flushSendFrames();
353
+ expect(API.prototype.postDetailed).toHaveBeenCalledTimes(2);
354
+ expect(received).toHaveLength(2);
355
+ });
356
+
357
+ it("transmet undefined à une méthode @post sur une erreur de transport", async () => {
358
+ const bodyPubId = "postSpecHandlerTransportBody";
359
+ PublisherManager.get(bodyPubId).set({ tick: 1 });
360
+ const bodyKey = new DataProviderKey<{ tick: number }>(bodyPubId);
361
+ const ep = new Endpoint<{ ok: boolean }>("items/handler-transport");
362
+ const localPost = post(ep, bodyKey, {
363
+ autoPostOnBodyMutation: false,
364
+ });
365
+ const received: Array<ApiResult<{ ok: boolean }> | undefined> = [];
366
+ vi.mocked(API.prototype.postDetailed).mockRejectedValueOnce(
367
+ new Error("network failure"),
368
+ );
369
+ const tag = "test-api-post-handler-transport";
370
+
371
+ class C extends HTMLElement {
372
+ @localPost
373
+ onPayload(payload: ApiResult<{ ok: boolean }> | undefined) {
374
+ received.push(payload);
375
+ }
376
+ }
377
+ if (!customElements.get(tag)) {
378
+ customElements.define(tag, C);
379
+ }
380
+
381
+ const wrap = document.createElement("div");
382
+ wrap.setAttribute("serviceURL", "https://api.example.test");
383
+ const el = document.createElement(tag) as InstanceType<typeof C>;
384
+ wrap.appendChild(el);
385
+ document.body.appendChild(wrap);
386
+ await flushSendFrames();
387
+
388
+ await expect(localPost.send(el)).rejects.toThrow("network failure");
389
+ expect(received).toEqual([undefined]);
390
+ });
306
391
  });
307
392
 
308
393
  describe("put / patch", () => {
@@ -319,11 +404,15 @@ describe("put / patch", () => {
319
404
  PublisherManager.get(bodyPubId).set({ name: "x" });
320
405
  const bodyKey = new DataProviderKey<{ name: string }>(bodyPubId);
321
406
  const ep = new Endpoint<{ ok: boolean }>("items/1");
407
+ const localPut = put(ep, bodyKey);
408
+ const received: Array<ApiResult<{ ok: boolean }> | undefined> = [];
322
409
  const tag = "test-api-put";
323
410
 
324
411
  class C extends HTMLElement {
325
- @put(ep, bodyKey)
326
- payload: ApiResult<{ ok: boolean }> | null = null;
412
+ @localPut
413
+ onPayload(payload: ApiResult<{ ok: boolean }> | undefined) {
414
+ received.push(payload);
415
+ }
327
416
  }
328
417
  if (!customElements.get(tag)) {
329
418
  customElements.define(tag, C);
@@ -338,6 +427,7 @@ describe("put / patch", () => {
338
427
  expect(API.prototype.putDetailed).toHaveBeenCalledWith("items/1", {
339
428
  name: "x",
340
429
  });
430
+ expect(received).toHaveLength(1);
341
431
  });
342
432
 
343
433
  it("@patch appelle patchDetailed", async () => {
@@ -348,11 +438,15 @@ describe("put / patch", () => {
348
438
  PublisherManager.get(bodyPubId).set({ name: "y" });
349
439
  const bodyKey = new DataProviderKey<{ name: string }>(bodyPubId);
350
440
  const ep = new Endpoint<{ ok: boolean }>("items/1");
441
+ const localPatch = patch(ep, bodyKey);
442
+ const received: Array<ApiResult<{ ok: boolean }> | undefined> = [];
351
443
  const tag = "test-api-patch";
352
444
 
353
445
  class C extends HTMLElement {
354
- @patch(ep, bodyKey)
355
- payload: ApiResult<{ ok: boolean }> | null = null;
446
+ @localPatch
447
+ onPayload(payload: ApiResult<{ ok: boolean }> | undefined) {
448
+ received.push(payload);
449
+ }
356
450
  }
357
451
  if (!customElements.get(tag)) {
358
452
  customElements.define(tag, C);
@@ -367,5 +461,6 @@ describe("put / patch", () => {
367
461
  expect(API.prototype.patchDetailed).toHaveBeenCalledWith("items/1", {
368
462
  name: "y",
369
463
  });
464
+ expect(received).toHaveLength(1);
370
465
  });
371
466
  });
@@ -190,6 +190,96 @@ describe("get", () => {
190
190
  expect(el.payload?.result).toEqual({ id: "loaded" });
191
191
  });
192
192
 
193
+ it("transmet le payload complet à une méthode @get", async () => {
194
+ const itemEp = new Endpoint<{ id: string }>("items/handler");
195
+ const received: Array<ApiResult<{ id: string }> | undefined> = [];
196
+ const tag = "test-api-get-handler";
197
+ const errorPayload = mockPayload({ id: "error" });
198
+ errorPayload.response = new Response(JSON.stringify({ error: true }), {
199
+ status: 422,
200
+ headers: { "Content-Type": "application/json" },
201
+ });
202
+ vi.mocked(API.prototype.getDetailed).mockResolvedValueOnce(
203
+ errorPayload as never,
204
+ );
205
+
206
+ class C extends HTMLElement {
207
+ @get(itemEp)
208
+ onPayload(payload: ApiResult<{ id: string }> | undefined) {
209
+ received.push(payload);
210
+ }
211
+ }
212
+ if (!customElements.get(tag)) {
213
+ customElements.define(tag, C);
214
+ }
215
+
216
+ const wrap = document.createElement("div");
217
+ wrap.setAttribute("serviceURL", "https://api.example.test");
218
+ wrap.appendChild(document.createElement(tag));
219
+ document.body.appendChild(wrap);
220
+ await flushGetFrames();
221
+
222
+ expect(received).toHaveLength(1);
223
+ expect(received[0]?.result).toEqual({ id: "error" });
224
+ expect(received[0]?.response?.status).toBe(422);
225
+ expect(received[0]?.request.url).toContain("api.example.test");
226
+ });
227
+
228
+ it("transmet undefined à une méthode @get quand aucun payload n'est produit", async () => {
229
+ vi.mocked(API.prototype.getDetailed).mockResolvedValueOnce(undefined);
230
+ const itemEp = new Endpoint<{ id: string }>("items/handler-error");
231
+ const received: Array<ApiResult<{ id: string }> | undefined> = [];
232
+ const tag = "test-api-get-handler-undefined";
233
+
234
+ class C extends HTMLElement {
235
+ @get(itemEp)
236
+ onPayload(payload: ApiResult<{ id: string }> | undefined) {
237
+ received.push(payload);
238
+ }
239
+ }
240
+ if (!customElements.get(tag)) {
241
+ customElements.define(tag, C);
242
+ }
243
+
244
+ const wrap = document.createElement("div");
245
+ wrap.setAttribute("serviceURL", "https://api.example.test");
246
+ wrap.appendChild(document.createElement(tag));
247
+ document.body.appendChild(wrap);
248
+ await flushGetFrames();
249
+
250
+ expect(received).toEqual([undefined]);
251
+ });
252
+
253
+ it("permet d'appliquer un handle get directement à une méthode", async () => {
254
+ const itemEp = new Endpoint<{ id: string }>("items/method");
255
+ const localGet = get(itemEp);
256
+ const received: Array<ApiResult<{ id: string }> | undefined> = [];
257
+ const tag = "test-api-get-method-handler";
258
+
259
+ class C extends HTMLElement {
260
+ @localGet
261
+ onPayload(payload: ApiResult<{ id: string }> | undefined) {
262
+ received.push(payload);
263
+ }
264
+ }
265
+ if (!customElements.get(tag)) {
266
+ customElements.define(tag, C);
267
+ }
268
+
269
+ const wrap = document.createElement("div");
270
+ wrap.setAttribute("serviceURL", "https://api.example.test");
271
+ const el = document.createElement(tag) as InstanceType<typeof C>;
272
+ wrap.appendChild(el);
273
+ document.body.appendChild(wrap);
274
+ await flushGetFrames();
275
+
276
+ expect(received).toHaveLength(1);
277
+ expect(received[0]?.result).toEqual({ id: "loaded" });
278
+
279
+ await localGet.refetch(el);
280
+ expect(received).toHaveLength(2);
281
+ });
282
+
193
283
  it("expose un refetch local avant et après le premier payload", async () => {
194
284
  const pubId = "getSpecLocalRefetch";
195
285
  PublisherManager.get(pubId).set(staticConfig);