@rebasepro/plugin-ai 0.17.3 → 0.18.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/LICENSE +0 -1
- package/README.md +4 -0
- package/dist/api.d.ts +8 -0
- package/dist/index.es.js.map +1 -1
- package/package.json +35 -21
- package/src/api.ts +0 -320
- package/src/components/AutofillReviewDialog.tsx +0 -209
- package/src/components/DataEnhancementControllerProvider.tsx +0 -320
- package/src/components/FormEnhanceAction.tsx +0 -307
- package/src/editor/useEditorAIController.tsx +0 -24
- package/src/index.ts +0 -9
- package/src/tests/AutofillReviewDialog.test.tsx +0 -340
- package/src/tests/api.test.ts +0 -382
- package/src/tests/properties.test.ts +0 -420
- package/src/tests/request_agreement.test.ts +0 -240
- package/src/tests/review.test.tsx +0 -596
- package/src/tests/useDataEnhancementPlugin.test.tsx +0 -72
- package/src/tests/useEditorAIController.test.ts +0 -98
- package/src/tests/values.test.ts +0 -87
- package/src/types/data_enhancement_controller.tsx +0 -142
- package/src/useDataEnhancementPlugin.tsx +0 -68
- package/src/utils/properties.ts +0 -168
- package/src/utils/values.ts +0 -72
- package/src/vite-env.d.ts +0 -1
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
import { TextEncoder, TextDecoder } from "util";
|
|
2
|
-
Object.assign(global, { TextEncoder,
|
|
3
|
-
TextDecoder });
|
|
4
|
-
|
|
5
|
-
if (typeof window !== "undefined") {
|
|
6
|
-
Object.defineProperty(window, "matchMedia", {
|
|
7
|
-
writable: true,
|
|
8
|
-
value: jest.fn().mockImplementation(query => ({
|
|
9
|
-
matches: false,
|
|
10
|
-
media: query,
|
|
11
|
-
onchange: null,
|
|
12
|
-
addEventListener: jest.fn(),
|
|
13
|
-
removeEventListener: jest.fn(),
|
|
14
|
-
dispatchEvent: jest.fn()
|
|
15
|
-
}))
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
import React from "react";
|
|
20
|
-
import { act, render, screen, within } from "@testing-library/react";
|
|
21
|
-
import userEvent from "@testing-library/user-event";
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
DataEnhancementControllerProvider,
|
|
25
|
-
useDataEnhancementController
|
|
26
|
-
} from "../components/DataEnhancementControllerProvider";
|
|
27
|
-
import { AutofillReviewDialog } from "../components/AutofillReviewDialog";
|
|
28
|
-
import { DataEnhancementController } from "../types/data_enhancement_controller";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* The review surface.
|
|
32
|
-
*
|
|
33
|
-
* The controller's behaviour is covered in `review.test.tsx`; this is about
|
|
34
|
-
* what the operator can actually see and press. The two things worth proving
|
|
35
|
-
* here are that a proposal which would overwrite existing content *says so* —
|
|
36
|
-
* a review that hides what it is about to destroy is worse than no review — and
|
|
37
|
-
* that the Apply button's count matches what Apply will really write.
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
jest.mock("@rebasepro/cms", () => ({
|
|
41
|
-
getFieldId: (property: { type?: string }) => (property?.type === "string" ? "text_field" : "number_field")
|
|
42
|
-
}));
|
|
43
|
-
|
|
44
|
-
const COLLECTION = {
|
|
45
|
-
name: "Products",
|
|
46
|
-
singularName: "Product",
|
|
47
|
-
properties: {
|
|
48
|
-
title: { type: "string",
|
|
49
|
-
name: "Title" },
|
|
50
|
-
stock: { type: "number",
|
|
51
|
-
name: "Stock" }
|
|
52
|
-
}
|
|
53
|
-
} as any;
|
|
54
|
-
|
|
55
|
-
function streamingResponse(body: string): any {
|
|
56
|
-
const encoder = new TextEncoder();
|
|
57
|
-
let sent = false;
|
|
58
|
-
return {
|
|
59
|
-
ok: true,
|
|
60
|
-
status: 200,
|
|
61
|
-
body: {
|
|
62
|
-
getReader: () => ({
|
|
63
|
-
read: async () => {
|
|
64
|
-
if (sent) return { done: true,
|
|
65
|
-
value: undefined };
|
|
66
|
-
sent = true;
|
|
67
|
-
return { done: false,
|
|
68
|
-
value: encoder.encode(body) };
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const FULL_RUN = [
|
|
76
|
-
'event: suggestion\ndata: {"key":"title","value":"Blue widget"}',
|
|
77
|
-
'event: suggestion\ndata: {"key":"stock","value":42}',
|
|
78
|
-
'event: done\ndata: {"suggestions":{"title":"Blue widget","stock":42}}',
|
|
79
|
-
""
|
|
80
|
-
].join("\n\n");
|
|
81
|
-
|
|
82
|
-
function mockService(autofillBody: string | (() => any)) {
|
|
83
|
-
(global as any).fetch = jest.fn((url: string) => {
|
|
84
|
-
if (String(url).endsWith("/status")) {
|
|
85
|
-
return Promise.resolve({ ok: true,
|
|
86
|
-
status: 200,
|
|
87
|
-
json: async () => ({ available: true }) });
|
|
88
|
-
}
|
|
89
|
-
if (String(url).endsWith("/autofill")) {
|
|
90
|
-
return Promise.resolve(typeof autofillBody === "string" ? streamingResponse(autofillBody) : autofillBody());
|
|
91
|
-
}
|
|
92
|
-
return Promise.resolve({ ok: true,
|
|
93
|
-
status: 200,
|
|
94
|
-
json: async () => ({ prompts: [] }) });
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
let controller: DataEnhancementController;
|
|
99
|
-
|
|
100
|
-
function Capture() {
|
|
101
|
-
controller = useDataEnhancementController();
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function mount(formValues: Record<string, unknown> = {}) {
|
|
106
|
-
const setFieldValue = jest.fn();
|
|
107
|
-
const formContext = { values: formValues,
|
|
108
|
-
setFieldValue } as any;
|
|
109
|
-
|
|
110
|
-
render(
|
|
111
|
-
<DataEnhancementControllerProvider
|
|
112
|
-
path={"products"}
|
|
113
|
-
collection={COLLECTION}
|
|
114
|
-
formContext={formContext}
|
|
115
|
-
{...({} as any)}>
|
|
116
|
-
<Capture/>
|
|
117
|
-
<AutofillReviewDialog/>
|
|
118
|
-
</DataEnhancementControllerProvider>
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
// Let the /status probe resolve so the controller reports enabled.
|
|
122
|
-
await act(async () => {
|
|
123
|
-
await Promise.resolve();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
return { setFieldValue };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function runAutofill(values: Record<string, unknown> = {}) {
|
|
130
|
-
await act(async () => {
|
|
131
|
-
await controller.generate({ values });
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
afterEach(() => {
|
|
136
|
-
jest.restoreAllMocks();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe("AutofillReviewDialog", () => {
|
|
140
|
-
|
|
141
|
-
it("renders nothing until a run has produced something to review", async () => {
|
|
142
|
-
mockService(FULL_RUN);
|
|
143
|
-
await mount();
|
|
144
|
-
expect(screen.queryByText(/Review autofill/i)).toBeNull();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("lists each proposed field with its label and value", async () => {
|
|
148
|
-
mockService(FULL_RUN);
|
|
149
|
-
await mount();
|
|
150
|
-
await runAutofill();
|
|
151
|
-
|
|
152
|
-
expect(screen.getByText("Review autofill")).toBeTruthy();
|
|
153
|
-
expect(screen.getByText("Title")).toBeTruthy();
|
|
154
|
-
expect(screen.getByText("Blue widget")).toBeTruthy();
|
|
155
|
-
expect(screen.getByText("Stock")).toBeTruthy();
|
|
156
|
-
expect(screen.getByText("42")).toBeTruthy();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("says so when a proposal would overwrite something already written", async () => {
|
|
160
|
-
// The property that makes this a review rather than a preview. Hiding
|
|
161
|
-
// what is about to be destroyed is worse than not offering a review.
|
|
162
|
-
mockService(FULL_RUN);
|
|
163
|
-
await mount({ title: "Hand grinder" });
|
|
164
|
-
await runAutofill({ title: "Hand grinder" });
|
|
165
|
-
|
|
166
|
-
expect(screen.getAllByText(/replaces the current value/i).length).toBe(1);
|
|
167
|
-
expect(screen.getByText("Hand grinder")).toBeTruthy();
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("does not claim a replacement for a field that was empty", async () => {
|
|
171
|
-
mockService(FULL_RUN);
|
|
172
|
-
await mount({ title: "" });
|
|
173
|
-
await runAutofill({ title: "" });
|
|
174
|
-
|
|
175
|
-
expect(screen.queryByText(/replaces the current value/i)).toBeNull();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("counts only the fields Apply will actually write", async () => {
|
|
179
|
-
mockService(FULL_RUN);
|
|
180
|
-
await mount();
|
|
181
|
-
await runAutofill();
|
|
182
|
-
|
|
183
|
-
expect(screen.getByRole("button", { name: /Apply 2 fields/i })).toBeTruthy();
|
|
184
|
-
|
|
185
|
-
const user = userEvent.setup();
|
|
186
|
-
const rows = screen.getAllByRole("checkbox");
|
|
187
|
-
// The first checkbox is "select all"; the next two are the fields.
|
|
188
|
-
await act(async () => {
|
|
189
|
-
await user.click(rows[rows.length - 1]);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
expect(screen.getByRole("button", { name: /Apply 1 field/i })).toBeTruthy();
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("writes only the ticked fields when Apply is pressed", async () => {
|
|
196
|
-
mockService(FULL_RUN);
|
|
197
|
-
const { setFieldValue } = await mount();
|
|
198
|
-
await runAutofill();
|
|
199
|
-
|
|
200
|
-
const user = userEvent.setup();
|
|
201
|
-
const boxes = screen.getAllByRole("checkbox");
|
|
202
|
-
await act(async () => {
|
|
203
|
-
await user.click(boxes[boxes.length - 1]);
|
|
204
|
-
});
|
|
205
|
-
await act(async () => {
|
|
206
|
-
await user.click(screen.getByRole("button", { name: /Apply 1 field/i }));
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
expect(setFieldValue).toHaveBeenCalledTimes(1);
|
|
210
|
-
expect(setFieldValue).toHaveBeenCalledWith("title", "Blue widget");
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("writes nothing when Discard is pressed, and closes", async () => {
|
|
214
|
-
mockService(FULL_RUN);
|
|
215
|
-
const { setFieldValue } = await mount({ title: "Original" });
|
|
216
|
-
await runAutofill({ title: "Original" });
|
|
217
|
-
|
|
218
|
-
const user = userEvent.setup();
|
|
219
|
-
await act(async () => {
|
|
220
|
-
await user.click(screen.getByRole("button", { name: /Discard/i }));
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
expect(setFieldValue).not.toHaveBeenCalled();
|
|
224
|
-
expect(screen.queryByText("Review autofill")).toBeNull();
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("disables Apply when nothing is ticked", async () => {
|
|
228
|
-
mockService(FULL_RUN);
|
|
229
|
-
await mount();
|
|
230
|
-
await runAutofill();
|
|
231
|
-
|
|
232
|
-
const user = userEvent.setup();
|
|
233
|
-
await act(async () => {
|
|
234
|
-
await user.click(screen.getAllByRole("checkbox")[0]);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
const apply = screen.getByRole("button", { name: /Apply 0 fields/i });
|
|
238
|
-
expect(apply.hasAttribute("disabled")).toBe(true);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("shows the instruction back to the operator while they review", async () => {
|
|
242
|
-
mockService(FULL_RUN);
|
|
243
|
-
await mount();
|
|
244
|
-
await act(async () => {
|
|
245
|
-
await controller.generate({ values: {},
|
|
246
|
-
instructions: "A burr grinder for travel" });
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
expect(screen.getByText(/A burr grinder for travel/)).toBeTruthy();
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it("keeps what arrived when a run fails part-way, and explains", async () => {
|
|
253
|
-
// A run that produced one good field and then broke should still let
|
|
254
|
-
// the operator take the one.
|
|
255
|
-
mockService([
|
|
256
|
-
'event: suggestion\ndata: {"key":"title","value":"Blue widget"}',
|
|
257
|
-
'event: error\ndata: {"message":"quota exhausted"}',
|
|
258
|
-
""
|
|
259
|
-
].join("\n\n"));
|
|
260
|
-
await mount();
|
|
261
|
-
await runAutofill();
|
|
262
|
-
|
|
263
|
-
expect(screen.getByText(/quota exhausted/)).toBeTruthy();
|
|
264
|
-
expect(screen.getByText("Blue widget")).toBeTruthy();
|
|
265
|
-
expect(screen.getByRole("button", { name: /Apply 1 field/i })).toBeTruthy();
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it("explains an empty result rather than showing a blank dialog", async () => {
|
|
269
|
-
mockService('event: done\ndata: {"suggestions":{}}\n\n');
|
|
270
|
-
await mount();
|
|
271
|
-
await runAutofill();
|
|
272
|
-
|
|
273
|
-
expect(screen.getByText(/Nothing to fill in/i)).toBeTruthy();
|
|
274
|
-
expect(screen.getByRole("button", { name: /Apply 0 fields/i })).toBeTruthy();
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it("offers select-all only when there is more than one field", async () => {
|
|
278
|
-
mockService('event: suggestion\ndata: {"key":"title","value":"Only one"}\n\nevent: done\ndata: {"suggestions":{"title":"Only one"}}\n\n');
|
|
279
|
-
await mount();
|
|
280
|
-
await runAutofill();
|
|
281
|
-
|
|
282
|
-
expect(screen.queryByText(/Select all|Deselect all/i)).toBeNull();
|
|
283
|
-
expect(screen.getAllByRole("checkbox").length).toBe(1);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it("toggles every row at once", async () => {
|
|
287
|
-
mockService(FULL_RUN);
|
|
288
|
-
await mount();
|
|
289
|
-
await runAutofill();
|
|
290
|
-
|
|
291
|
-
const user = userEvent.setup();
|
|
292
|
-
await act(async () => {
|
|
293
|
-
await user.click(screen.getByText(/Deselect all/i));
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
expect(screen.getByRole("button", { name: /Apply 0 fields/i })).toBeTruthy();
|
|
297
|
-
expect(screen.getByText(/Select all/i)).toBeTruthy();
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it("renders a date proposal readably rather than as an ISO string", async () => {
|
|
301
|
-
const collection = {
|
|
302
|
-
name: "Posts",
|
|
303
|
-
singularName: "Post",
|
|
304
|
-
properties: { publishedAt: { type: "date",
|
|
305
|
-
name: "Published at" } }
|
|
306
|
-
} as any;
|
|
307
|
-
|
|
308
|
-
(global as any).fetch = jest.fn((url: string) => {
|
|
309
|
-
if (String(url).endsWith("/status")) return Promise.resolve({ ok: true,
|
|
310
|
-
status: 200,
|
|
311
|
-
json: async () => ({ available: true }) });
|
|
312
|
-
return Promise.resolve(streamingResponse(
|
|
313
|
-
'event: suggestion\ndata: {"key":"publishedAt","value":"2026-08-03T10:00:00.000Z"}\n\nevent: done\ndata: {"suggestions":{}}\n\n'
|
|
314
|
-
));
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
const setFieldValue = jest.fn();
|
|
318
|
-
render(
|
|
319
|
-
<DataEnhancementControllerProvider
|
|
320
|
-
path={"posts"}
|
|
321
|
-
collection={collection}
|
|
322
|
-
formContext={{ values: {},
|
|
323
|
-
setFieldValue } as any}
|
|
324
|
-
{...({} as any)}>
|
|
325
|
-
<Capture/>
|
|
326
|
-
<AutofillReviewDialog/>
|
|
327
|
-
</DataEnhancementControllerProvider>
|
|
328
|
-
);
|
|
329
|
-
await act(async () => {
|
|
330
|
-
await Promise.resolve();
|
|
331
|
-
});
|
|
332
|
-
await runAutofill();
|
|
333
|
-
|
|
334
|
-
// Not the raw ISO string — the row shows a locale-formatted date, which
|
|
335
|
-
// is only possible because the controller coerced it to a Date first.
|
|
336
|
-
expect(screen.queryByText("2026-08-03T10:00:00.000Z")).toBeNull();
|
|
337
|
-
const row = screen.getByText("Published at").closest("label[class*=\"flex\"]");
|
|
338
|
-
expect(within(row as HTMLElement).getByText(/2026/)).toBeTruthy();
|
|
339
|
-
});
|
|
340
|
-
});
|