@happyvertical/smrt-chat 0.53.0 → 0.53.2
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/dist/index.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/smrt-knowledge.json +3 -3
- package/package.json +13 -13
- package/dist/__tests__/chat-test-db.d.ts +0 -8
- package/dist/__tests__/chat-test-db.d.ts.map +0 -1
- package/dist/__tests__/raw-collections.d.ts +0 -35
- package/dist/__tests__/raw-collections.d.ts.map +0 -1
- package/dist/svelte/components/agent/__tests__/ToolCallDisplay.test.js +0 -135
- package/dist/svelte/components/agent/message-blocks.test.js +0 -31
- package/dist/svelte/components/assistant/__tests__/AssistantComposer.test.js +0 -75
- package/dist/svelte/components/assistant/__tests__/AssistantDock.test.js +0 -515
- package/dist/svelte/components/assistant/__tests__/AssistantThreadList.test.js +0 -45
- package/dist/svelte/components/assistant/__tests__/action-status.test.js +0 -25
- package/dist/svelte/components/assistant/__tests__/assistant-composer-dictation.test.js +0 -197
- package/dist/svelte/components/assistant/__tests__/assistant-dock-action-outcome.test.js +0 -320
- package/dist/svelte/components/assistant/__tests__/assistant-dock-choices-pending.test.js +0 -224
- package/dist/svelte/components/assistant/__tests__/assistant-dock-choices-preview.test.js +0 -312
- package/dist/svelte/components/assistant/__tests__/assistant-dock-choices.test.js +0 -338
- package/dist/svelte/components/assistant/__tests__/assistant-dock-client-tools.test.js +0 -442
- package/dist/svelte/components/assistant/__tests__/assistant-dock-draft-settled.test.js +0 -448
- package/dist/svelte/components/assistant/__tests__/assistant-dock-host-actions.test.js +0 -185
- package/dist/svelte/components/assistant/__tests__/assistant-dock-run.test.js +0 -303
- package/dist/svelte/components/assistant/__tests__/assistant-dock-settle.test.js +0 -146
- package/dist/svelte/components/assistant/__tests__/assistant-transport.test.js +0 -253
- package/dist/svelte/components/assistant/__tests__/attachment-href.test.js +0 -31
- package/dist/svelte/components/assistant/__tests__/create-assistant-dock-controller.test.js +0 -2491
- package/dist/svelte/components/layout/__tests__/ChatLayout.test.js +0 -67
- package/dist/svelte/components/messages/__tests__/MessageInput.test.js +0 -38
- package/dist/svelte/components/messages/__tests__/MessageItem.test.js +0 -108
- package/dist/svelte/components/shared/__tests__/Avatar.test.js +0 -20
- package/dist/svelte/components/shared/__tests__/LinkPreview.test.js +0 -29
- package/dist/svelte/components/shared/__tests__/MentionAutocomplete.test.js +0 -66
- package/dist/svelte/components/shared/__tests__/ModelPicker.test.js +0 -36
- package/dist/svelte/components/shared/__tests__/ReactionPicker.test.js +0 -35
- package/dist/svelte/components/shared/__tests__/ReadReceipts.test.js +0 -28
- package/dist/svelte/components/shared/__tests__/UserPresence.test.js +0 -23
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
/**
|
|
3
|
-
* Choices whose options take a while (generated pictures): the offer shows
|
|
4
|
-
* "Making…" placeholders, options are added as they finish, the person can
|
|
5
|
-
* pick any that arrived, and a failure is said plainly.
|
|
6
|
-
*/
|
|
7
|
-
import { createDataSurfaceRegistry } from '@happyvertical/smrt-ui/data-surface';
|
|
8
|
-
import { render, screen, userEvent } from '@happyvertical/smrt-vitest/svelte';
|
|
9
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
-
import AssistantDock from '../AssistantDock.svelte';
|
|
11
|
-
import { choiceToolName, createAssistantChoiceSourceRegistry, } from '../assistant-choices.svelte.js';
|
|
12
|
-
import { createAssistantDockController } from '../create-assistant-dock-controller.svelte.js';
|
|
13
|
-
function message(id, role, content) {
|
|
14
|
-
return {
|
|
15
|
-
id,
|
|
16
|
-
threadId: 't1',
|
|
17
|
-
role,
|
|
18
|
-
content,
|
|
19
|
-
createdAt: new Date(0).toISOString(),
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
/** Every send asks for one choice tool call; resuming answers in text. */
|
|
23
|
-
function transportCalling(call) {
|
|
24
|
-
const resumed = [];
|
|
25
|
-
let sends = 0;
|
|
26
|
-
const transport = {
|
|
27
|
-
resumed,
|
|
28
|
-
async listThreads() {
|
|
29
|
-
return [{ id: 't1', title: 'T', isResolved: false, messageCount: 0 }];
|
|
30
|
-
},
|
|
31
|
-
async createThread() {
|
|
32
|
-
return { id: 't1', title: 'T', isResolved: false, messageCount: 0 };
|
|
33
|
-
},
|
|
34
|
-
async loadMessages() {
|
|
35
|
-
return [];
|
|
36
|
-
},
|
|
37
|
-
async uploadAttachment() {
|
|
38
|
-
throw new Error('no');
|
|
39
|
-
},
|
|
40
|
-
async sendMessage(input) {
|
|
41
|
-
sends += 1;
|
|
42
|
-
return {
|
|
43
|
-
inProgress: false,
|
|
44
|
-
userMessage: message(`u${sends}`, 'user', input.content),
|
|
45
|
-
clientToolCalls: {
|
|
46
|
-
continuationId: `cont-${sends}`,
|
|
47
|
-
calls: [{ ...call, id: sends > 1 ? `${call.id}-${sends}` : call.id }],
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
},
|
|
51
|
-
async resumeTurn(input) {
|
|
52
|
-
resumed.push(input);
|
|
53
|
-
return {
|
|
54
|
-
inProgress: false,
|
|
55
|
-
assistantMessage: message('a1', 'assistant', 'Making them now.'),
|
|
56
|
-
};
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
return transport;
|
|
60
|
-
}
|
|
61
|
-
/** A source whose versions arrive when the test says so. */
|
|
62
|
-
function slowSource() {
|
|
63
|
-
let update;
|
|
64
|
-
let signal;
|
|
65
|
-
let finish = () => { };
|
|
66
|
-
const apply = vi.fn(async () => 'Used the snowy one.');
|
|
67
|
-
const source = {
|
|
68
|
-
id: 'picture_edit',
|
|
69
|
-
description: 'Make new versions of the picture.',
|
|
70
|
-
offer: () => ({
|
|
71
|
-
title: 'Snowy versions',
|
|
72
|
-
options: [],
|
|
73
|
-
pending: {
|
|
74
|
-
message: 'Making versions… this takes a minute or two.',
|
|
75
|
-
expected: 3,
|
|
76
|
-
fill: (u, s) => new Promise((resolve, reject) => {
|
|
77
|
-
update = u;
|
|
78
|
-
signal = s;
|
|
79
|
-
finish = (err) => (err ? reject(err) : resolve());
|
|
80
|
-
}),
|
|
81
|
-
},
|
|
82
|
-
}),
|
|
83
|
-
apply,
|
|
84
|
-
};
|
|
85
|
-
return {
|
|
86
|
-
source,
|
|
87
|
-
apply,
|
|
88
|
-
add: (id) => update?.add([{ id, label: `Version ${id}`, imageUrl: `/v/${id}.jpg` }]),
|
|
89
|
-
finish: (err) => finish(err),
|
|
90
|
-
signal: () => signal,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
async function until(check) {
|
|
94
|
-
for (let i = 0; i < 50 && !check(); i += 1) {
|
|
95
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
async function offered(slow) {
|
|
99
|
-
const registry = createAssistantChoiceSourceRegistry();
|
|
100
|
-
registry.register(slow.source);
|
|
101
|
-
const transport = transportCalling({
|
|
102
|
-
id: 'e1',
|
|
103
|
-
name: choiceToolName('picture_edit'),
|
|
104
|
-
args: {},
|
|
105
|
-
effect: 'read',
|
|
106
|
-
});
|
|
107
|
-
const controller = createAssistantDockController({
|
|
108
|
-
transport,
|
|
109
|
-
registry: createDataSurfaceRegistry(),
|
|
110
|
-
choiceSources: registry,
|
|
111
|
-
});
|
|
112
|
-
await controller.loadThreads();
|
|
113
|
-
await controller.openThread('t1');
|
|
114
|
-
await controller.send('make it snowing');
|
|
115
|
-
await until(() => Boolean(slow.signal()));
|
|
116
|
-
return { controller, transport };
|
|
117
|
-
}
|
|
118
|
-
describe('pending choices', () => {
|
|
119
|
-
it('tells the model versions are coming, fills them in, and applies the pick', async () => {
|
|
120
|
-
const slow = slowSource();
|
|
121
|
-
const { controller, transport } = await offered(slow);
|
|
122
|
-
expect(JSON.parse(transport.resumed[0].results[0].result ?? '{}')).toEqual({
|
|
123
|
-
offered: true,
|
|
124
|
-
waitingForUser: true,
|
|
125
|
-
stillMaking: true,
|
|
126
|
-
progress: 'Making versions… this takes a minute or two.',
|
|
127
|
-
options: [],
|
|
128
|
-
});
|
|
129
|
-
expect(controller.choices[0]).toMatchObject({
|
|
130
|
-
status: 'waiting',
|
|
131
|
-
pending: { expected: 3 },
|
|
132
|
-
});
|
|
133
|
-
expect(controller.status.label).toBe('Making versions… this takes a minute or two.');
|
|
134
|
-
slow.add('a');
|
|
135
|
-
expect(controller.choices[0].options.map((o) => o.id)).toEqual(['a']);
|
|
136
|
-
expect(controller.choices[0].pending?.expected).toBe(2);
|
|
137
|
-
expect(controller.status.label).toBe('Pick one of the options');
|
|
138
|
-
// The person picks the first version while the others are still coming.
|
|
139
|
-
await controller.chooseOption('e1', 'a');
|
|
140
|
-
expect(slow.apply).toHaveBeenCalledTimes(1);
|
|
141
|
-
expect(controller.choices[0]).toMatchObject({ status: 'applied' });
|
|
142
|
-
expect(controller.choices[0].pending).toBeUndefined();
|
|
143
|
-
expect(slow.signal()?.aborted).toBe(true);
|
|
144
|
-
// Late versions are ignored.
|
|
145
|
-
slow.add('b');
|
|
146
|
-
expect(controller.choices[0].options).toHaveLength(1);
|
|
147
|
-
});
|
|
148
|
-
it('keeps the offer when the person sends another message; None of these stops it', async () => {
|
|
149
|
-
const slow = slowSource();
|
|
150
|
-
const { controller } = await offered(slow);
|
|
151
|
-
const firstSignal = slow.signal();
|
|
152
|
-
await controller.send('thanks');
|
|
153
|
-
expect(controller.choices[0]).toMatchObject({
|
|
154
|
-
id: 'e1',
|
|
155
|
-
status: 'waiting',
|
|
156
|
-
});
|
|
157
|
-
expect(firstSignal?.aborted).toBe(false);
|
|
158
|
-
controller.dismissChoices('e1');
|
|
159
|
-
expect(controller.choices[0].status).toBe('dismissed');
|
|
160
|
-
expect(firstSignal?.aborted).toBe(true);
|
|
161
|
-
});
|
|
162
|
-
it('ends ready with what arrived, and notes a partial failure', async () => {
|
|
163
|
-
const slow = slowSource();
|
|
164
|
-
const { controller } = await offered(slow);
|
|
165
|
-
slow.add('a');
|
|
166
|
-
slow.add('b');
|
|
167
|
-
slow.finish(new Error('One version could not be made.'));
|
|
168
|
-
await until(() => !controller.choices[0].pending);
|
|
169
|
-
expect(controller.choices[0]).toMatchObject({
|
|
170
|
-
status: 'waiting',
|
|
171
|
-
note: 'One version could not be made.',
|
|
172
|
-
});
|
|
173
|
-
expect(controller.choices[0].options).toHaveLength(2);
|
|
174
|
-
});
|
|
175
|
-
it('is unavailable, and holds nothing, when no version could be made', async () => {
|
|
176
|
-
const slow = slowSource();
|
|
177
|
-
const { controller } = await offered(slow);
|
|
178
|
-
slow.finish(new Error('The picture service is busy. Try again later.'));
|
|
179
|
-
await until(() => controller.choices[0].status !== 'waiting');
|
|
180
|
-
expect(controller.choices[0]).toMatchObject({
|
|
181
|
-
status: 'unavailable',
|
|
182
|
-
error: 'The picture service is busy. Try again later.',
|
|
183
|
-
});
|
|
184
|
-
expect(controller.status.label).not.toBe('Pick one of the options');
|
|
185
|
-
});
|
|
186
|
-
it('shows placeholders, then the versions as cards', async () => {
|
|
187
|
-
const slow = slowSource();
|
|
188
|
-
const registry = createAssistantChoiceSourceRegistry();
|
|
189
|
-
registry.register(slow.source);
|
|
190
|
-
const transport = transportCalling({
|
|
191
|
-
id: 'e1',
|
|
192
|
-
name: 'assistant_offer_picture_edit',
|
|
193
|
-
args: {},
|
|
194
|
-
effect: 'read',
|
|
195
|
-
});
|
|
196
|
-
let controller;
|
|
197
|
-
render(AssistantDock, {
|
|
198
|
-
props: {
|
|
199
|
-
transport,
|
|
200
|
-
registry: createDataSurfaceRegistry(),
|
|
201
|
-
choiceSources: registry,
|
|
202
|
-
oncontroller: (c) => {
|
|
203
|
-
controller = c;
|
|
204
|
-
},
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
await until(() => Boolean(controller));
|
|
208
|
-
await controller?.openThread('t1');
|
|
209
|
-
await controller?.send('make it snowing');
|
|
210
|
-
const group = await screen.findByRole('group', { name: 'Snowy versions' });
|
|
211
|
-
expect(group.getAttribute('aria-busy')).toBe('true');
|
|
212
|
-
expect(screen.getAllByText('Making…')).toHaveLength(3);
|
|
213
|
-
expect(screen.getAllByText('Making versions… this takes a minute or two.')
|
|
214
|
-
.length).toBeGreaterThan(0);
|
|
215
|
-
slow.add('a');
|
|
216
|
-
await screen.findByRole('button', { name: /Version a/ });
|
|
217
|
-
expect(screen.getAllByText('Making…')).toHaveLength(2);
|
|
218
|
-
slow.finish();
|
|
219
|
-
await until(() => screen.queryAllByText('Making…').length === 0);
|
|
220
|
-
expect(group.getAttribute('aria-busy')).toBeNull();
|
|
221
|
-
await userEvent.click(screen.getByRole('button', { name: /Version a/ }));
|
|
222
|
-
expect(await screen.findByText('Used the snowy one.')).toBeTruthy();
|
|
223
|
-
});
|
|
224
|
-
});
|
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
/**
|
|
3
|
-
* Previewable choices: a click previews a card in place (nothing applied),
|
|
4
|
-
* another click swaps the preview, "Original" shows the page as it was, and
|
|
5
|
-
* only the commit button applies. Cancel, Escape, dismissing and disposing
|
|
6
|
-
* restore the original.
|
|
7
|
-
*/
|
|
8
|
-
import { createDataSurfaceRegistry } from '@happyvertical/smrt-ui/data-surface';
|
|
9
|
-
import { render, screen, userEvent } from '@happyvertical/smrt-vitest/svelte';
|
|
10
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
11
|
-
import AssistantDock from '../AssistantDock.svelte';
|
|
12
|
-
import { createAssistantChoiceSourceRegistry, } from '../assistant-choices.svelte.js';
|
|
13
|
-
function message(id, role, content) {
|
|
14
|
-
return {
|
|
15
|
-
id,
|
|
16
|
-
threadId: 't1',
|
|
17
|
-
role,
|
|
18
|
-
content,
|
|
19
|
-
createdAt: new Date(0).toISOString(),
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
function suspendingTransport(calls) {
|
|
23
|
-
const sent = [];
|
|
24
|
-
const resumed = [];
|
|
25
|
-
const transport = {
|
|
26
|
-
sent,
|
|
27
|
-
resumed,
|
|
28
|
-
async listThreads() {
|
|
29
|
-
return [{ id: 't1', title: 'T', isResolved: false, messageCount: 0 }];
|
|
30
|
-
},
|
|
31
|
-
async createThread() {
|
|
32
|
-
return { id: 't1', title: 'T', isResolved: false, messageCount: 0 };
|
|
33
|
-
},
|
|
34
|
-
async loadMessages() {
|
|
35
|
-
return [];
|
|
36
|
-
},
|
|
37
|
-
async uploadAttachment() {
|
|
38
|
-
throw new Error('no');
|
|
39
|
-
},
|
|
40
|
-
async sendMessage(input) {
|
|
41
|
-
sent.push(input);
|
|
42
|
-
return {
|
|
43
|
-
inProgress: false,
|
|
44
|
-
userMessage: message('u1', 'user', input.content),
|
|
45
|
-
clientToolCalls: {
|
|
46
|
-
continuationId: `cont-${sent.length}`,
|
|
47
|
-
calls: calls.map((call) => ({
|
|
48
|
-
...call,
|
|
49
|
-
id: sent.length > 1 ? `${call.id}-${sent.length}` : call.id,
|
|
50
|
-
})),
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
},
|
|
54
|
-
async resumeTurn(input) {
|
|
55
|
-
resumed.push(input);
|
|
56
|
-
const reply = message('a1', 'assistant', 'Pick one of these.');
|
|
57
|
-
return { inProgress: false, assistantMessage: reply };
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
return transport;
|
|
61
|
-
}
|
|
62
|
-
async function until(check) {
|
|
63
|
-
for (let i = 0; i < 50 && !check(); i += 1) {
|
|
64
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const ORIGINAL = { id: 'original', label: 'Original', imageUrl: '/p/1?orig' };
|
|
68
|
-
function previewSource() {
|
|
69
|
-
const shown = [];
|
|
70
|
-
const apply = vi.fn(async () => 'Changed it.');
|
|
71
|
-
const source = {
|
|
72
|
-
id: 'picture_adjust',
|
|
73
|
-
description: 'Quick changes.',
|
|
74
|
-
offer: async () => ({
|
|
75
|
-
title: 'Pick a version',
|
|
76
|
-
options: [
|
|
77
|
-
{ id: 'a', label: 'Brighter', imageUrl: '/p/1?a' },
|
|
78
|
-
{ id: 'b', label: 'Much brighter', imageUrl: '/p/1?b' },
|
|
79
|
-
],
|
|
80
|
-
original: ORIGINAL,
|
|
81
|
-
commitLabel: 'Use this picture',
|
|
82
|
-
cancelLabel: 'Keep the original',
|
|
83
|
-
}),
|
|
84
|
-
apply,
|
|
85
|
-
preview: (option) => {
|
|
86
|
-
shown.push(option?.id === 'original' || !option ? null : (option.imageUrl ?? null));
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
return { source, shown, apply };
|
|
90
|
-
}
|
|
91
|
-
async function mount(source) {
|
|
92
|
-
const registry = createAssistantChoiceSourceRegistry();
|
|
93
|
-
registry.register(source);
|
|
94
|
-
const transport = suspendingTransport([
|
|
95
|
-
{
|
|
96
|
-
id: 'c1',
|
|
97
|
-
name: 'assistant_offer_picture_adjust',
|
|
98
|
-
args: {},
|
|
99
|
-
effect: 'read',
|
|
100
|
-
},
|
|
101
|
-
]);
|
|
102
|
-
let controller;
|
|
103
|
-
const view = render(AssistantDock, {
|
|
104
|
-
props: {
|
|
105
|
-
transport,
|
|
106
|
-
registry: createDataSurfaceRegistry(),
|
|
107
|
-
choiceSources: registry,
|
|
108
|
-
oncontroller: (c) => {
|
|
109
|
-
controller = c;
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
await until(() => Boolean(controller));
|
|
114
|
-
await controller?.openThread('t1');
|
|
115
|
-
await controller?.send('brighter');
|
|
116
|
-
await screen.findByRole('group', { name: 'Pick a version' });
|
|
117
|
-
return { controller: controller, view };
|
|
118
|
-
}
|
|
119
|
-
describe('previewable choices', () => {
|
|
120
|
-
it('previews a click, swaps the preview, shows the original, and applies nothing', async () => {
|
|
121
|
-
const { source, shown, apply } = previewSource();
|
|
122
|
-
const { controller } = await mount(source);
|
|
123
|
-
// The Original card comes first.
|
|
124
|
-
const cards = screen
|
|
125
|
-
.getAllByRole('button')
|
|
126
|
-
.filter((b) => b.hasAttribute('aria-pressed'));
|
|
127
|
-
expect(cards.map((b) => b.textContent?.trim())).toEqual([
|
|
128
|
-
'Original',
|
|
129
|
-
'Brighter',
|
|
130
|
-
'Much brighter',
|
|
131
|
-
]);
|
|
132
|
-
await userEvent.click(screen.getByRole('button', { name: /^Brighter/ }));
|
|
133
|
-
await until(() => shown.length === 1);
|
|
134
|
-
expect(shown).toEqual(['/p/1?a']);
|
|
135
|
-
expect(screen
|
|
136
|
-
.getByRole('button', { name: /^Brighter/ })
|
|
137
|
-
.getAttribute('aria-pressed')).toBe('true');
|
|
138
|
-
await userEvent.click(screen.getByRole('button', { name: /^Much brighter/ }));
|
|
139
|
-
await until(() => shown.length === 2);
|
|
140
|
-
expect(shown).toEqual(['/p/1?a', '/p/1?b']);
|
|
141
|
-
expect(screen
|
|
142
|
-
.getByRole('button', { name: /^Brighter/ })
|
|
143
|
-
.getAttribute('aria-pressed')).toBe('false');
|
|
144
|
-
await userEvent.click(screen.getByRole('button', { name: /^Original/ }));
|
|
145
|
-
await until(() => shown.length === 3);
|
|
146
|
-
expect(shown).toEqual(['/p/1?a', '/p/1?b', null]);
|
|
147
|
-
expect(apply).not.toHaveBeenCalled();
|
|
148
|
-
expect(controller.choices[0].status).toBe('waiting');
|
|
149
|
-
});
|
|
150
|
-
it('keeps the commit button off until a version is previewed, then applies only the previewed one and resolves', async () => {
|
|
151
|
-
const { source, apply } = previewSource();
|
|
152
|
-
const { controller } = await mount(source);
|
|
153
|
-
const use = screen.getByRole('button', {
|
|
154
|
-
name: 'Use this picture',
|
|
155
|
-
});
|
|
156
|
-
expect(use.disabled).toBe(true);
|
|
157
|
-
await userEvent.click(screen.getByRole('button', { name: /^Much brighter/ }));
|
|
158
|
-
await until(() => !use.disabled);
|
|
159
|
-
expect(use.disabled).toBe(false);
|
|
160
|
-
await userEvent.click(use);
|
|
161
|
-
await until(() => apply.mock.calls.length > 0);
|
|
162
|
-
expect(apply).toHaveBeenCalledTimes(1);
|
|
163
|
-
expect(apply).toHaveBeenCalledWith(expect.objectContaining({ id: 'b' }));
|
|
164
|
-
expect(await screen.findByText('Changed it.')).toBeTruthy();
|
|
165
|
-
expect(controller.choices[0]).toMatchObject({
|
|
166
|
-
status: 'applied',
|
|
167
|
-
chosenOptionId: 'b',
|
|
168
|
-
});
|
|
169
|
-
// The cards collapse once resolved.
|
|
170
|
-
expect(screen.queryByRole('button', { name: /^Brighter/ })).toBeNull();
|
|
171
|
-
});
|
|
172
|
-
it('waits for a slow preview: cards and commit are off until the page shows it', async () => {
|
|
173
|
-
const { source, apply } = previewSource();
|
|
174
|
-
const releases = [];
|
|
175
|
-
const shown = [];
|
|
176
|
-
const slow = {
|
|
177
|
-
...source,
|
|
178
|
-
preview: (option) => new Promise((resolve) => {
|
|
179
|
-
releases.push(() => {
|
|
180
|
-
shown.push(option?.id ?? 'none');
|
|
181
|
-
resolve();
|
|
182
|
-
});
|
|
183
|
-
}),
|
|
184
|
-
};
|
|
185
|
-
const { controller } = await mount(slow);
|
|
186
|
-
const brighter = screen.getByRole('button', { name: /^Brighter/ });
|
|
187
|
-
const muchBrighter = screen.getByRole('button', {
|
|
188
|
-
name: /^Much brighter/,
|
|
189
|
-
});
|
|
190
|
-
const use = screen.getByRole('button', {
|
|
191
|
-
name: 'Use this picture',
|
|
192
|
-
});
|
|
193
|
-
await userEvent.click(brighter);
|
|
194
|
-
await until(() => releases.length === 1);
|
|
195
|
-
// While the page is still changing, nothing else can be picked or committed.
|
|
196
|
-
expect(muchBrighter.disabled).toBe(true);
|
|
197
|
-
expect(use.disabled).toBe(true);
|
|
198
|
-
await userEvent.click(muchBrighter);
|
|
199
|
-
expect(releases).toHaveLength(1);
|
|
200
|
-
releases[0]();
|
|
201
|
-
await until(() => !muchBrighter.disabled);
|
|
202
|
-
expect(shown).toEqual(['a']);
|
|
203
|
-
expect(controller.choices[0].previewOptionId).toBe('a');
|
|
204
|
-
expect(use.disabled).toBe(false);
|
|
205
|
-
await userEvent.click(use);
|
|
206
|
-
await until(() => apply.mock.calls.length > 0);
|
|
207
|
-
expect(apply).toHaveBeenCalledWith(expect.objectContaining({ id: 'a' }));
|
|
208
|
-
});
|
|
209
|
-
it('back-to-back previews queue, the last one shows, and it is what commits', async () => {
|
|
210
|
-
const { source, apply } = previewSource();
|
|
211
|
-
const shown = [];
|
|
212
|
-
const releases = [];
|
|
213
|
-
const slow = {
|
|
214
|
-
...source,
|
|
215
|
-
preview: (option) => new Promise((resolve) => {
|
|
216
|
-
releases.push(() => {
|
|
217
|
-
shown.push(option?.id ?? 'none');
|
|
218
|
-
resolve();
|
|
219
|
-
});
|
|
220
|
-
}),
|
|
221
|
-
};
|
|
222
|
-
const { controller } = await mount(slow);
|
|
223
|
-
const setId = controller.choices[0].id;
|
|
224
|
-
// Two programmatic previews (a double click that beat the disabled
|
|
225
|
-
// state): they queue, a superseded one never reaches the page, the page
|
|
226
|
-
// and the offer both end on the second, and a commit while a preview is
|
|
227
|
-
// pending does nothing.
|
|
228
|
-
const first = controller.previewOption(setId, 'a');
|
|
229
|
-
const second = controller.previewOption(setId, 'b');
|
|
230
|
-
await until(() => releases.length === 1);
|
|
231
|
-
await controller.commitOption(setId);
|
|
232
|
-
expect(apply).not.toHaveBeenCalled();
|
|
233
|
-
releases[0]();
|
|
234
|
-
await Promise.all([first, second]);
|
|
235
|
-
expect(releases).toHaveLength(1);
|
|
236
|
-
expect(shown).toEqual(['b']);
|
|
237
|
-
expect(controller.choices[0].previewOptionId).toBe('b');
|
|
238
|
-
expect(controller.choices[0].previewPendingId).toBeUndefined();
|
|
239
|
-
await controller.commitOption(setId);
|
|
240
|
-
expect(apply).toHaveBeenCalledTimes(1);
|
|
241
|
-
expect(apply).toHaveBeenCalledWith(expect.objectContaining({ id: 'b' }));
|
|
242
|
-
});
|
|
243
|
-
it('cancelling during a slow preview restores the page after it, never before', async () => {
|
|
244
|
-
const { source } = previewSource();
|
|
245
|
-
const shown = [];
|
|
246
|
-
const releases = [];
|
|
247
|
-
const slow = {
|
|
248
|
-
...source,
|
|
249
|
-
preview: (option) => new Promise((resolve) => {
|
|
250
|
-
releases.push(() => {
|
|
251
|
-
shown.push(option?.id === 'original' || !option ? null : option.id);
|
|
252
|
-
resolve();
|
|
253
|
-
});
|
|
254
|
-
}),
|
|
255
|
-
};
|
|
256
|
-
const { controller } = await mount(slow);
|
|
257
|
-
const setId = controller.choices[0].id;
|
|
258
|
-
const pending = controller.previewOption(setId, 'a');
|
|
259
|
-
await until(() => releases.length === 1);
|
|
260
|
-
await userEvent.click(screen.getByRole('button', { name: 'Keep the original' }));
|
|
261
|
-
releases[0]();
|
|
262
|
-
await pending;
|
|
263
|
-
await until(() => releases.length === 2);
|
|
264
|
-
releases[1]();
|
|
265
|
-
await until(() => shown.length === 2);
|
|
266
|
-
expect(shown).toEqual(['a', null]);
|
|
267
|
-
expect(controller.choices[0].status).toBe('dismissed');
|
|
268
|
-
});
|
|
269
|
-
it('cancel restores the original and closes without applying', async () => {
|
|
270
|
-
const { source, shown, apply } = previewSource();
|
|
271
|
-
await mount(source);
|
|
272
|
-
await userEvent.click(screen.getByRole('button', { name: /^Brighter/ }));
|
|
273
|
-
await until(() => shown.length === 1);
|
|
274
|
-
await userEvent.click(screen.getByRole('button', { name: 'Keep the original' }));
|
|
275
|
-
await until(() => shown.length === 2);
|
|
276
|
-
expect(shown).toEqual(['/p/1?a', null]);
|
|
277
|
-
expect(screen.queryByRole('group', { name: 'Pick a version' })).toBeNull();
|
|
278
|
-
expect(apply).not.toHaveBeenCalled();
|
|
279
|
-
});
|
|
280
|
-
it('Escape cancels an open preview', async () => {
|
|
281
|
-
const { source, shown, apply } = previewSource();
|
|
282
|
-
await mount(source);
|
|
283
|
-
const card = screen.getByRole('button', { name: /^Brighter/ });
|
|
284
|
-
await userEvent.click(card);
|
|
285
|
-
await until(() => shown.length === 1);
|
|
286
|
-
card.focus();
|
|
287
|
-
await userEvent.keyboard('{Escape}');
|
|
288
|
-
await until(() => shown.length === 2);
|
|
289
|
-
expect(shown).toEqual(['/p/1?a', null]);
|
|
290
|
-
expect(screen.queryByRole('group', { name: 'Pick a version' })).toBeNull();
|
|
291
|
-
expect(apply).not.toHaveBeenCalled();
|
|
292
|
-
});
|
|
293
|
-
it('puts the page back when the conversation is disposed with an uncommitted preview', async () => {
|
|
294
|
-
const { source, shown, apply } = previewSource();
|
|
295
|
-
const { controller } = await mount(source);
|
|
296
|
-
await userEvent.click(screen.getByRole('button', { name: /^Brighter/ }));
|
|
297
|
-
await until(() => shown.length === 1);
|
|
298
|
-
controller.dispose();
|
|
299
|
-
await until(() => shown.length === 2);
|
|
300
|
-
expect(shown).toEqual(['/p/1?a', null]);
|
|
301
|
-
expect(apply).not.toHaveBeenCalled();
|
|
302
|
-
});
|
|
303
|
-
it('a source without preview still applies on click', async () => {
|
|
304
|
-
const apply = vi.fn(async () => 'Done.');
|
|
305
|
-
const { source } = previewSource();
|
|
306
|
-
await mount({ ...source, preview: undefined, apply });
|
|
307
|
-
await userEvent.click(screen.getByRole('button', { name: /^Brighter/ }));
|
|
308
|
-
await until(() => apply.mock.calls.length > 0);
|
|
309
|
-
expect(apply).toHaveBeenCalledTimes(1);
|
|
310
|
-
expect(screen.queryByRole('button', { name: 'Use this picture' })).toBeNull();
|
|
311
|
-
});
|
|
312
|
-
});
|