@astryxdesign/lab 0.7.6-canary.f73623d → 0.7.6-canary.f77aaf2

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 (33) hide show
  1. package/dist/CommentPin/CommentPin.js +1 -1
  2. package/dist/Comments/CommentComposer.d.ts.map +1 -1
  3. package/dist/Comments/CommentComposer.js +12 -4
  4. package/dist/MobilePowerSearch/MobilePowerSearch.d.ts +3 -6
  5. package/dist/MobilePowerSearch/MobilePowerSearch.d.ts.map +1 -1
  6. package/dist/MobilePowerSearch/MobilePowerSearch.js +12 -76
  7. package/dist/MobileTokenizer/MobileTokenizer.d.ts +10 -7
  8. package/dist/MobileTokenizer/MobileTokenizer.d.ts.map +1 -1
  9. package/dist/MobileTokenizer/MobileTokenizer.js +191 -114
  10. package/dist/internal/MobileSheetConfirmation.d.ts +12 -0
  11. package/dist/internal/MobileSheetConfirmation.d.ts.map +1 -0
  12. package/dist/internal/MobileSheetConfirmation.js +88 -0
  13. package/dist/internal/useDismissSearchOnTouchDrag.d.ts +21 -0
  14. package/dist/internal/useDismissSearchOnTouchDrag.d.ts.map +1 -0
  15. package/dist/internal/useDismissSearchOnTouchDrag.js +43 -0
  16. package/package.json +4 -4
  17. package/src/CommentPin/CommentPin.tsx +1 -1
  18. package/src/Comments/CommentComposer.test.tsx +56 -0
  19. package/src/Comments/CommentComposer.tsx +18 -13
  20. package/src/MobilePowerSearch/MobilePowerSearch.doc.mjs +1 -1
  21. package/src/MobilePowerSearch/MobilePowerSearch.test.tsx +7 -2
  22. package/src/MobilePowerSearch/MobilePowerSearch.tsx +16 -83
  23. package/src/MobileTokenizer/MobileTokenizer.doc.mjs +3 -3
  24. package/src/MobileTokenizer/MobileTokenizer.test.tsx +287 -31
  25. package/src/MobileTokenizer/MobileTokenizer.tsx +242 -137
  26. package/src/Schedule/__tests__/ScheduleTimeDirection.a11y.chromium.spec.ts +4 -0
  27. package/src/TransferList/TransferList.doc.mjs +1 -1
  28. package/src/internal/MobileSheetConfirmation.test.tsx +68 -0
  29. package/src/internal/MobileSheetConfirmation.tsx +97 -0
  30. package/src/internal/useDismissSearchOnTouchDrag.test.tsx +94 -0
  31. package/src/internal/useDismissSearchOnTouchDrag.ts +57 -0
  32. package/src/CommentPin/CommentPin.spec.md +0 -319
  33. package/src/Schedule/Schedule.spec.md +0 -590
@@ -129,6 +129,200 @@ beforeEach(() => {
129
129
  afterEach(() => vi.unstubAllGlobals());
130
130
 
131
131
  describe('MobileTokenizer (Lab, single-sheet flow)', () => {
132
+ it('keeps the bordered large search and footer actions visible while searching, and dismisses the keyboard without clearing the query when results move', async () => {
133
+ render(<Harness />);
134
+ fireEvent.click(screen.getByRole('button', {name: /Tags/}));
135
+ const search = await screen.findByLabelText('Search Tags');
136
+ const list = await screen.findByTestId('mobile-tokenizer-list');
137
+ await screen.findByRole('checkbox', {name: 'Energizer'});
138
+
139
+ expect(search.closest('.astryx-text-input')).toBeInTheDocument();
140
+ act(() => search.focus());
141
+ fireEvent.change(search, {target: {value: 'En'}});
142
+ await screen.findByRole('checkbox', {name: 'Eng'});
143
+ expect(screen.getByRole('button', {name: 'Clear all'})).toBeVisible();
144
+ expect(screen.getByRole('button', {name: 'Save'})).toBeVisible();
145
+
146
+ fireEvent.scroll(list);
147
+ expect(search).toHaveFocus();
148
+ expect(search).toHaveValue('En');
149
+
150
+ // Filtering a scrolled list can produce its own scroll event as the
151
+ // browser clamps scrollTop; typing must continue to own focus.
152
+ fireEvent.change(search, {target: {value: 'Eng'}});
153
+ fireEvent.scroll(list);
154
+ expect(search).toHaveFocus();
155
+ fireEvent.change(search, {target: {value: 'Engi'}});
156
+ expect(search).toHaveValue('Engi');
157
+ expect(search).toHaveFocus();
158
+
159
+ fireEvent.touchStart(list, {
160
+ touches: [{identifier: 1, target: list, clientY: 120}],
161
+ });
162
+ fireEvent.touchMove(list, {
163
+ touches: [{identifier: 1, target: list, clientY: 115}],
164
+ });
165
+ expect(search).toHaveFocus();
166
+ fireEvent.touchMove(list, {
167
+ touches: [{identifier: 1, target: list, clientY: 100}],
168
+ });
169
+ expect(search).not.toHaveFocus();
170
+ expect(search).toHaveValue('Engi');
171
+ });
172
+
173
+ it('guards dirty Escape, scrim, and swipe dismissal; keeps the draft after confirmation scrim or Keep editing; and discards only on request', async () => {
174
+ const spy = vi.fn();
175
+ render(<Harness spy={spy} />);
176
+ const trigger = screen.getByRole('button', {name: /Tags/});
177
+ fireEvent.click(trigger);
178
+ const sheet = await screen.findByRole('dialog', {name: 'Tags'});
179
+ const energizer = await screen.findByRole('checkbox', {name: 'Energizer'});
180
+ fireEvent.click(energizer);
181
+ expect(energizer).toBeChecked();
182
+ expect(spy).not.toHaveBeenCalled();
183
+
184
+ fireEvent.keyDown(sheet, {key: 'Escape'});
185
+ let discard = await screen.findByRole('alertdialog', {
186
+ name: 'Discard changes?',
187
+ });
188
+ fireEvent.click(discard);
189
+ await waitFor(() =>
190
+ expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument(),
191
+ );
192
+ expect(energizer).toBeChecked();
193
+ expect(screen.getByLabelText('Search Tags')).toHaveFocus();
194
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
195
+
196
+ fireEvent.click(sheet);
197
+ discard = await screen.findByRole('alertdialog', {
198
+ name: 'Discard changes?',
199
+ });
200
+ fireEvent.click(
201
+ within(discard).getByRole('button', {name: 'Keep editing'}),
202
+ );
203
+ await waitFor(() =>
204
+ expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument(),
205
+ );
206
+ expect(energizer).toBeChecked();
207
+ expect(screen.getByLabelText('Search Tags')).toHaveFocus();
208
+
209
+ const handle = sheet.querySelector<HTMLElement>(
210
+ '.astryx-bottom-sheet > div[aria-hidden="true"]',
211
+ )!;
212
+ fireEvent.pointerDown(handle, {
213
+ pointerId: 1,
214
+ clientY: 0,
215
+ button: 0,
216
+ isPrimary: true,
217
+ pointerType: 'touch',
218
+ });
219
+ fireEvent.pointerMove(handle, {
220
+ pointerId: 1,
221
+ clientY: 40,
222
+ pointerType: 'touch',
223
+ });
224
+ fireEvent.pointerMove(handle, {
225
+ pointerId: 1,
226
+ clientY: 120,
227
+ pointerType: 'touch',
228
+ });
229
+ fireEvent.pointerUp(handle, {
230
+ pointerId: 1,
231
+ clientY: 120,
232
+ pointerType: 'touch',
233
+ });
234
+ discard = await screen.findByRole('alertdialog', {
235
+ name: 'Discard changes?',
236
+ });
237
+ const actions = within(discard).getAllByRole('button');
238
+ expect(actions.map(action => action.textContent)).toEqual([
239
+ 'Discard changes',
240
+ 'Keep editing',
241
+ ]);
242
+ expect(actions[0]).toHaveAttribute('data-variant', 'destructive');
243
+ expect(actions[1]).toHaveAttribute('data-variant', 'secondary');
244
+ fireEvent.click(actions[0]);
245
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
246
+ expect(spy).not.toHaveBeenCalled();
247
+ });
248
+
249
+ it('forced disabled closure also closes a dirty-discard confirmation', async () => {
250
+ const spy = vi.fn();
251
+ const props = {
252
+ label: 'Tags',
253
+ searchSource: source,
254
+ value: [ITEMS[0]],
255
+ debounceMs: 0,
256
+ onChange: spy,
257
+ };
258
+ const {rerender} = render(<MobileTokenizer {...props} />);
259
+ const trigger = screen.getByRole('button', {name: /Tags/});
260
+ fireEvent.click(trigger);
261
+ const sheet = await screen.findByRole('dialog', {name: 'Tags'});
262
+ fireEvent.click(await screen.findByRole('checkbox', {name: 'Energizer'}));
263
+ fireEvent.keyDown(sheet, {key: 'Escape'});
264
+ expect(
265
+ await screen.findByRole('alertdialog', {name: 'Discard changes?'}),
266
+ ).toBeInTheDocument();
267
+
268
+ rerender(<MobileTokenizer {...props} isDisabled />);
269
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
270
+ await waitFor(() =>
271
+ expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument(),
272
+ );
273
+ expect(spy).not.toHaveBeenCalled();
274
+ });
275
+
276
+ it('blocks Save if maxEntries drops below the current draft until the draft returns within the limit', async () => {
277
+ const spy = vi.fn();
278
+ const props = {
279
+ label: 'Tags',
280
+ searchSource: source,
281
+ value: [] as SearchableItem[],
282
+ debounceMs: 0,
283
+ onChange: spy,
284
+ };
285
+ const {rerender} = render(<MobileTokenizer {...props} maxEntries={2} />);
286
+ const trigger = screen.getByRole('button', {name: /Tags/});
287
+ fireEvent.click(trigger);
288
+ const design = await screen.findByRole('checkbox', {name: 'Design'});
289
+ const eng = await screen.findByRole('checkbox', {name: 'Eng'});
290
+ fireEvent.click(design);
291
+ fireEvent.click(eng);
292
+
293
+ rerender(<MobileTokenizer {...props} maxEntries={1} />);
294
+ const save = screen.getByRole('button', {name: 'Save'});
295
+ expect(save).toHaveAttribute('aria-disabled', 'true');
296
+ expect(save).toHaveAccessibleDescription('Select up to 1 item');
297
+ fireEvent.click(save);
298
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
299
+ expect(spy).not.toHaveBeenCalled();
300
+
301
+ fireEvent.click(eng);
302
+ expect(save).not.toHaveAttribute('aria-disabled');
303
+ fireEvent.click(save);
304
+ expect(spy).toHaveBeenCalledWith(
305
+ [ITEMS[0]],
306
+ expect.objectContaining({item: ITEMS[0], type: 'add'}),
307
+ );
308
+ });
309
+
310
+ it('closes directly when edits return the draft to its opening selection', async () => {
311
+ const spy = vi.fn();
312
+ render(<Harness spy={spy} />);
313
+ const trigger = screen.getByRole('button', {name: /Tags/});
314
+ fireEvent.click(trigger);
315
+ const sheet = await screen.findByRole('dialog', {name: 'Tags'});
316
+ const energizer = await screen.findByRole('checkbox', {name: 'Energizer'});
317
+ fireEvent.click(energizer);
318
+ fireEvent.click(energizer);
319
+
320
+ fireEvent.keyDown(sheet, {key: 'Escape'});
321
+ expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
322
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
323
+ expect(spy).not.toHaveBeenCalled();
324
+ });
325
+
132
326
  it('keeps rows stable while open and applies selected-first order on reopen', async () => {
133
327
  const spy = vi.fn();
134
328
  render(<Harness spy={spy} />);
@@ -160,10 +354,7 @@ describe('MobileTokenizer (Lab, single-sheet flow)', () => {
160
354
  expect(design.lastElementChild).toHaveAttribute('data-size', 'md');
161
355
 
162
356
  fireEvent.click(screen.getByRole('checkbox', {name: 'Energizer'}));
163
- expect(spy).toHaveBeenCalledWith(
164
- [ITEMS[0], ITEMS[1], ITEMS[3]],
165
- expect.objectContaining({type: 'add'}),
166
- );
357
+ expect(spy).not.toHaveBeenCalled();
167
358
  await waitFor(() =>
168
359
  expect(screen.getByRole('checkbox', {name: 'Energizer'})).toBeChecked(),
169
360
  );
@@ -175,10 +366,7 @@ describe('MobileTokenizer (Lab, single-sheet flow)', () => {
175
366
  ]);
176
367
 
177
368
  fireEvent.click(screen.getByRole('checkbox', {name: 'Eng'}));
178
- expect(spy).toHaveBeenLastCalledWith(
179
- [ITEMS[0], ITEMS[3]],
180
- expect.objectContaining({type: 'remove'}),
181
- );
369
+ expect(spy).not.toHaveBeenCalled();
182
370
  expect(checkboxLabels()).toEqual([
183
371
  'Design',
184
372
  'Eng',
@@ -186,7 +374,11 @@ describe('MobileTokenizer (Lab, single-sheet flow)', () => {
186
374
  'Energizer',
187
375
  ]);
188
376
 
189
- fireEvent.click(screen.getByRole('button', {name: 'Done'}));
377
+ fireEvent.click(screen.getByRole('button', {name: 'Save'}));
378
+ expect(spy).toHaveBeenCalledWith(
379
+ [ITEMS[0], ITEMS[3]],
380
+ expect.objectContaining({item: ITEMS[1], type: 'remove'}),
381
+ );
190
382
  expect(trigger).toHaveAttribute('aria-expanded', 'false');
191
383
 
192
384
  await act(async () => {
@@ -200,6 +392,38 @@ describe('MobileTokenizer (Lab, single-sheet flow)', () => {
200
392
  ]);
201
393
  });
202
394
 
395
+ it('reports the net changed item when draft actions cancel each other', async () => {
396
+ const spy = vi.fn();
397
+ render(<Harness spy={spy} />);
398
+ const trigger = screen.getByRole('button', {name: /Tags/});
399
+ fireEvent.click(trigger);
400
+ await screen.findByRole('checkbox', {name: 'Energizer'});
401
+
402
+ fireEvent.click(screen.getByRole('checkbox', {name: 'Energizer'}));
403
+ fireEvent.click(screen.getByRole('checkbox', {name: 'Design'}));
404
+ fireEvent.click(screen.getByRole('checkbox', {name: 'Energizer'}));
405
+ fireEvent.click(screen.getByRole('button', {name: 'Save'}));
406
+
407
+ expect(spy).toHaveBeenCalledTimes(1);
408
+ expect(spy).toHaveBeenCalledWith([ITEMS[1]], {
409
+ item: ITEMS[0],
410
+ type: 'remove',
411
+ });
412
+ });
413
+
414
+ it('does not commit a selection toggled back to its opening value', async () => {
415
+ const spy = vi.fn();
416
+ render(<Harness spy={spy} />);
417
+ fireEvent.click(screen.getByRole('button', {name: /Tags/}));
418
+ await screen.findByRole('checkbox', {name: 'Design'});
419
+
420
+ fireEvent.click(screen.getByRole('checkbox', {name: 'Design'}));
421
+ fireEvent.click(screen.getByRole('checkbox', {name: 'Design'}));
422
+ fireEvent.click(screen.getByRole('button', {name: 'Save'}));
423
+
424
+ expect(spy).not.toHaveBeenCalled();
425
+ });
426
+
203
427
  it('creates a custom entry with a trailing Add action', async () => {
204
428
  const spy = vi.fn();
205
429
  render(<CreateHarness spy={spy} />);
@@ -212,12 +436,8 @@ describe('MobileTokenizer (Lab, single-sheet flow)', () => {
212
436
  screen.queryByRole('checkbox', {name: 'Create "Custom"'}),
213
437
  ).not.toBeInTheDocument();
214
438
  const create = await screen.findByRole('button', {name: 'Add Custom'});
215
- expect(
216
- screen.queryByRole('button', {name: 'Clear all'}),
217
- ).not.toBeInTheDocument();
218
- expect(
219
- screen.queryByRole('button', {name: 'Done'}),
220
- ).not.toBeInTheDocument();
439
+ expect(screen.getByRole('button', {name: 'Clear all'})).toBeInTheDocument();
440
+ expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument();
221
441
  expect(create).toHaveAttribute('data-size', 'lg');
222
442
  expect(screen.getByTestId('mobile-tokenizer-create-row')).toHaveTextContent(
223
443
  'Create "Custom"',
@@ -225,14 +445,18 @@ describe('MobileTokenizer (Lab, single-sheet flow)', () => {
225
445
  await act(async () => {
226
446
  fireEvent.click(create);
227
447
  });
448
+ expect(spy).not.toHaveBeenCalled();
449
+ expect(trigger).not.toHaveTextContent('Custom');
450
+ expect(search).toHaveValue('');
451
+ const custom = await screen.findByRole('checkbox', {name: 'Custom'});
452
+ expect(custom).toBeChecked();
453
+
454
+ fireEvent.click(screen.getByRole('button', {name: 'Save'}));
228
455
  expect(spy).toHaveBeenCalledWith(
229
456
  [{id: 'Custom', label: 'Custom'}],
230
457
  expect.objectContaining({type: 'create'}),
231
458
  );
232
459
  await waitFor(() => expect(trigger).toHaveTextContent('Custom'));
233
- expect(search).toHaveValue('');
234
- const custom = await screen.findByRole('checkbox', {name: 'Custom'});
235
- expect(custom).toBeChecked();
236
460
  });
237
461
 
238
462
  it('renders long lists in 50-item batches and resets the window for search', async () => {
@@ -325,30 +549,49 @@ describe('MobileTokenizer (Lab, single-sheet flow)', () => {
325
549
  );
326
550
  });
327
551
 
328
- it('keeps large equal-width Clear all and Done footer actions while guarding the bulk clear', async () => {
552
+ it('keeps large equal-width Clear all and Save footer actions while guarding the bulk clear', async () => {
329
553
  const spy = vi.fn();
330
554
  render(<Harness spy={spy} />);
331
555
  fireEvent.click(screen.getByRole('button', {name: /Tags/}));
332
556
  const list = await screen.findByTestId('mobile-tokenizer-list');
333
557
 
334
558
  const clearAll = screen.getByRole('button', {name: 'Clear all'});
335
- const done = screen.getByRole('button', {name: 'Done'});
559
+ const save = screen.getByRole('button', {name: 'Save'});
336
560
  expect(clearAll).toHaveAttribute('data-size', 'lg');
337
561
  expect(clearAll).toHaveAttribute('data-variant', 'secondary');
338
- expect(done).toHaveAttribute('data-size', 'lg');
562
+ expect(save).toHaveAttribute('data-size', 'lg');
339
563
  expect(list.compareDocumentPosition(clearAll)).toBe(
340
564
  Node.DOCUMENT_POSITION_FOLLOWING,
341
565
  );
342
- expect(list.compareDocumentPosition(done)).toBe(
566
+ expect(list.compareDocumentPosition(save)).toBe(
343
567
  Node.DOCUMENT_POSITION_FOLLOWING,
344
568
  );
345
569
  fireEvent.click(clearAll);
346
570
  const confirmation = screen.getByRole('alertdialog');
571
+ expect(confirmation).toHaveClass('astryx-alert-dialog');
347
572
  expect(confirmation).toHaveTextContent('Clear all selected items?');
573
+ const confirmationActions = within(confirmation).getAllByRole('button');
574
+ expect(confirmationActions.map(action => action.textContent)).toEqual([
575
+ 'Clear selection',
576
+ 'Cancel',
577
+ ]);
578
+ expect(confirmationActions[0]).toHaveAttribute(
579
+ 'data-variant',
580
+ 'destructive',
581
+ );
582
+ expect(confirmationActions[1]).toHaveAttribute('data-variant', 'secondary');
348
583
  expect(spy).not.toHaveBeenCalled();
349
584
  fireEvent.click(
350
585
  within(confirmation).getByRole('button', {name: 'Clear selection'}),
351
586
  );
587
+ expect(spy).not.toHaveBeenCalled();
588
+ await waitFor(() =>
589
+ expect(screen.getByRole('button', {name: 'Clear all'})).toHaveAttribute(
590
+ 'aria-disabled',
591
+ 'true',
592
+ ),
593
+ );
594
+ fireEvent.click(save);
352
595
  expect(spy).toHaveBeenLastCalledWith(
353
596
  [],
354
597
  expect.objectContaining({type: 'remove'}),
@@ -441,6 +684,17 @@ describe('MobileTokenizer (Lab, single-sheet flow)', () => {
441
684
  for (const name of indicatorClasses) {
442
685
  expect(indicator).toHaveClass(name);
443
686
  }
687
+
688
+ fireEvent.click(screen.getByRole('checkbox', {name: 'Design'}));
689
+ expect(row).not.toHaveAttribute('aria-disabled');
690
+ fireEvent.click(row);
691
+ expect(row).toBeChecked();
692
+ expect(spy).not.toHaveBeenCalled();
693
+ fireEvent.click(screen.getByRole('button', {name: 'Save'}));
694
+ expect(spy).toHaveBeenCalledWith(
695
+ [ITEMS[3]],
696
+ expect.objectContaining({item: ITEMS[3], type: 'add'}),
697
+ );
444
698
  });
445
699
 
446
700
  it('paints the placeholder of a disabled empty trigger GrayText in forced colors (AST-085 FR6)', () => {
@@ -569,7 +823,7 @@ describe('MobileTokenizer Core-sheet parity', () => {
569
823
  expect(screen.queryByLabelText('Search Tags')).toBeNull();
570
824
  const sheet = await openSheet();
571
825
  expect(sheet).toHaveAttribute('open');
572
- fireEvent.click(screen.getByRole('button', {name: 'Done'}));
826
+ fireEvent.click(screen.getByRole('button', {name: 'Save'}));
573
827
  // Closed but retained for its exit; the trigger's focus (BottomSheet's
574
828
  // hand-back after the exit) releases it.
575
829
  const trigger = screen.getByRole('button', {name: /Tags/});
@@ -798,7 +1052,7 @@ describe('MobileTokenizer Core-sheet parity', () => {
798
1052
  expect(onBlur).not.toHaveBeenCalled();
799
1053
  // Closing hands focus back to the trigger: that is not focus entering
800
1054
  // from outside, so the caller's onFocus stays quiet for it.
801
- fireEvent.click(screen.getByRole('button', {name: 'Done'}));
1055
+ fireEvent.click(screen.getByRole('button', {name: 'Save'}));
802
1056
  act(() => trigger.focus());
803
1057
  await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
804
1058
  expect(sheet).not.toBeInTheDocument();
@@ -832,9 +1086,9 @@ describe('MobileTokenizer Core-sheet parity', () => {
832
1086
  await screen.findByRole('checkbox', {name: 'Design'});
833
1087
  expect(trigger).not.toHaveFocus();
834
1088
  expect(events).toEqual([]);
835
- // Done: BottomSheet hands focus to the trigger. The trigger never had
1089
+ // Save: BottomSheet hands focus to the trigger. The trigger never had
836
1090
  // focus, so this is the entry.
837
- fireEvent.click(screen.getByRole('button', {name: 'Done'}));
1091
+ fireEvent.click(screen.getByRole('button', {name: 'Save'}));
838
1092
  act(() => trigger.focus());
839
1093
  await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
840
1094
  expect(events).toEqual(['focus']);
@@ -846,7 +1100,7 @@ describe('MobileTokenizer Core-sheet parity', () => {
846
1100
  act(() => trigger.focus());
847
1101
  fireEvent.click(trigger);
848
1102
  await screen.findByRole('dialog');
849
- fireEvent.click(screen.getByRole('button', {name: 'Done'}));
1103
+ fireEvent.click(screen.getByRole('button', {name: 'Save'}));
850
1104
  act(() => trigger.focus());
851
1105
  await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
852
1106
  act(() => screen.getByRole('button', {name: 'Elsewhere'}).focus());
@@ -1034,10 +1288,7 @@ describe('MobileTokenizer Core-sheet parity', () => {
1034
1288
  fireEvent.click(
1035
1289
  within(confirmation).getByRole('button', {name: 'Clear selection'}),
1036
1290
  );
1037
- expect(spy).toHaveBeenCalledWith(
1038
- [],
1039
- expect.objectContaining({type: 'remove'}),
1040
- );
1291
+ expect(spy).not.toHaveBeenCalled();
1041
1292
  await waitFor(() => expect(screen.queryByRole('alertdialog')).toBeNull());
1042
1293
  expect(screen.getByLabelText('Search Tags')).toHaveFocus();
1043
1294
  await waitFor(() => expect(liveText(sheet)).toBe('All items removed'));
@@ -1048,6 +1299,11 @@ describe('MobileTokenizer Core-sheet parity', () => {
1048
1299
  expect(spent).toHaveAccessibleDescription('Nothing selected');
1049
1300
  fireEvent.click(spent);
1050
1301
  expect(screen.queryByRole('alertdialog')).toBeNull();
1302
+ fireEvent.click(screen.getByRole('button', {name: 'Save'}));
1303
+ expect(spy).toHaveBeenCalledWith(
1304
+ [],
1305
+ expect.objectContaining({type: 'remove'}),
1306
+ );
1051
1307
  });
1052
1308
 
1053
1309
  it('announces each toggle and the result count from inside the dialog (AR6, AST-086 FR14)', async () => {