@gntik-ai/blocks 0.6.0 → 0.8.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.
Files changed (58) hide show
  1. package/dist/index.d.ts +202 -74
  2. package/dist/index.js +849 -283
  3. package/dist/index.js.map +1 -1
  4. package/package.json +2 -2
  5. package/src/feedback/ConfirmDestructive/ConfirmDestructive.examples.test.tsx +25 -0
  6. package/src/feedback/ConfirmDestructive/ConfirmDestructive.extended.test.tsx +252 -0
  7. package/src/feedback/ConfirmDestructive/ConfirmDestructive.focus.test.tsx +178 -0
  8. package/src/feedback/ConfirmDestructive/ConfirmDestructive.tsx +219 -28
  9. package/src/feedback/ConfirmDestructive/block.meta.ts +2 -2
  10. package/src/feedback/ConfirmDestructive/examples/FailureStates.tsx +39 -0
  11. package/src/feedback/ConfirmDestructive/examples/PlainDeletion.tsx +6 -0
  12. package/src/feedback/ConfirmDestructive/examples/SecretRevocation.tsx +34 -0
  13. package/src/feedback/EmptyStates/EmptyStates.test.tsx +11 -1
  14. package/src/feedback/EmptyStates/EmptyStates.tsx +31 -1
  15. package/src/feedback/EmptyStates/block.meta.ts +1 -1
  16. package/src/feedback/EmptyStates/fixtures.ts +5 -0
  17. package/src/feedback/ErrorPanel/ErrorPanel.test.tsx +7 -0
  18. package/src/feedback/ErrorPanel/ErrorPanel.tsx +8 -3
  19. package/src/forms/CredentialDisclosureDialog/CredentialDisclosureDialog.additional.test.tsx +467 -0
  20. package/src/forms/CredentialDisclosureDialog/CredentialDisclosureDialog.tsx +175 -106
  21. package/src/forms/CredentialDisclosureDialog/IssueOnClick.test.tsx +56 -0
  22. package/src/forms/CredentialDisclosureDialog/SecretCopy.tsx +83 -0
  23. package/src/forms/CredentialDisclosureDialog/block.meta.ts +1 -1
  24. package/src/forms/CredentialDisclosureDialog/examples/Inline.tsx +19 -0
  25. package/src/forms/CredentialDisclosureDialog/examples/IssueOnClick.tsx +87 -0
  26. package/src/forms/CredentialDisclosureDialog/examples/MultiSecret.tsx +29 -0
  27. package/src/forms/CredentialDisclosureDialog/focus.ts +18 -0
  28. package/src/forms/CredentialDisclosureDialog/types.ts +5 -0
  29. package/src/tables/CursorPagerFooter/CursorPagerFooter.examples.test.tsx +33 -0
  30. package/src/tables/CursorPagerFooter/CursorPagerFooter.test.tsx +51 -0
  31. package/src/tables/CursorPagerFooter/CursorPagerFooter.tsx +10 -0
  32. package/src/tables/CursorPagerFooter/block.meta.ts +23 -0
  33. package/src/tables/CursorPagerFooter/examples/Busy.tsx +7 -0
  34. package/src/tables/CursorPagerFooter/examples/CursorStack.tsx +40 -0
  35. package/src/tables/CursorPagerFooter/examples/Hidden.tsx +16 -0
  36. package/src/tables/CursorPagerFooter/examples/RowsPerPage.tsx +21 -0
  37. package/src/tables/CursorPagerFooter/fixtures.ts +13 -0
  38. package/src/tables/DataTable/DataTable.server-pagination.test.tsx +173 -0
  39. package/src/tables/DataTable/DataTable.tsx +20 -12
  40. package/src/tables/DataTable/block.meta.ts +2 -1
  41. package/src/tables/DataTable/data-table-props.ts +27 -4
  42. package/src/tables/DataTable/examples/server-pagination.tsx +68 -0
  43. package/src/tables/LoadMoreFooter/LoadMoreFooter.announcements.test.tsx +106 -0
  44. package/src/tables/LoadMoreFooter/LoadMoreFooter.examples.test.tsx +36 -0
  45. package/src/tables/LoadMoreFooter/LoadMoreFooter.labels.test.tsx +71 -0
  46. package/src/tables/LoadMoreFooter/LoadMoreFooter.test.tsx +184 -0
  47. package/src/tables/LoadMoreFooter/LoadMoreFooter.tsx +151 -0
  48. package/src/tables/LoadMoreFooter/block.meta.ts +28 -0
  49. package/src/tables/LoadMoreFooter/examples/cards.tsx +22 -0
  50. package/src/tables/LoadMoreFooter/examples/cursor.tsx +25 -0
  51. package/src/tables/LoadMoreFooter/examples/exhausted.tsx +6 -0
  52. package/src/tables/LoadMoreFooter/examples/failed.tsx +13 -0
  53. package/src/tables/LoadMoreFooter/examples/flag.tsx +33 -0
  54. package/src/tables/LoadMoreFooter/examples/useMockCursor.ts +30 -0
  55. package/src/tables/LoadMoreFooter/fixtures.ts +9 -0
  56. package/src/tables/LoadMoreFooter/useLoadMoreFooter.ts +56 -0
  57. package/src/tables/index.ts +4 -0
  58. package/src/test/CredentialDisclosureDialog.compatibility.tsx +30 -0
@@ -0,0 +1,467 @@
1
+ import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { I18nProvider } from '@gntik-ai/ui';
4
+ import { CredentialDisclosureDialog } from './CredentialDisclosureDialog';
5
+ import { storedDisclosure } from './fixtures';
6
+ import { expectNoAxeViolations } from '../../test/a11y';
7
+
8
+ const additionalSecrets = [
9
+ { id: 'dsn', label: 'DSN', value: 'synthetic_dsn_value' },
10
+ { id: 'password', label: 'Password', value: 'synthetic_password_value' },
11
+ ];
12
+
13
+ it.each([
14
+ ['en', 'DSN: Copied'],
15
+ ['es', 'DSN: Copiado'],
16
+ ['override', 'Saved DSN'],
17
+ ])('masks independent additional secrets and copies with label-only feedback (%s)', async (locale, announcement) => {
18
+ const user = userEvent.setup();
19
+ const writeText = vi.fn().mockResolvedValue(undefined);
20
+ Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
21
+ const onCopy = vi.fn();
22
+ render(
23
+ <I18nProvider locale={locale!}>
24
+ <CredentialDisclosureDialog
25
+ disclosure={storedDisclosure}
26
+ onClose={() => {}}
27
+ additionalSecrets={additionalSecrets}
28
+ showLabel="Reveal"
29
+ hideLabel="Hide"
30
+ copyLabel="Copy"
31
+ onCopy={onCopy}
32
+ successAnnouncement="Unused single-secret feedback"
33
+ copiedAnnouncementFor={locale === 'override' ? (label) => `Saved ${label}` : undefined}
34
+ />
35
+ </I18nProvider>,
36
+ );
37
+ const dsn = await screen.findByRole('group', { name: 'DSN' });
38
+ const password = screen.getByRole('group', { name: 'Password' });
39
+ expect(within(dsn).getByRole('textbox')).not.toHaveValue(additionalSecrets[0]!.value);
40
+ await user.click(within(dsn).getByRole('button', { name: 'Reveal' }));
41
+ expect(within(dsn).getByRole('textbox')).toHaveValue(additionalSecrets[0]!.value);
42
+ expect(within(password).getByRole('button', { name: 'Reveal' })).toHaveAttribute('aria-pressed', 'false');
43
+ await user.click(within(dsn).getByRole('button', { name: 'Copy' }));
44
+ expect(writeText).toHaveBeenCalledWith('synthetic_dsn_value');
45
+ expect(onCopy).toHaveBeenCalledExactlyOnceWith('dsn');
46
+ expect(within(dsn).getByRole('status').textContent).toBe(announcement);
47
+ expect(within(dsn).getByRole('textbox').closest('[data-kit-redact]')).not.toBeNull();
48
+ expect(within(dsn).getByRole('status')).not.toHaveTextContent('synthetic_');
49
+ });
50
+
51
+ it('masks a fresh primary for the session and redacts the reveal slot, keeping its warning', async () => {
52
+ const user = userEvent.setup();
53
+ const props = { disclosure: { ...storedDisclosure, variant: 'fresh' as const }, onClose: vi.fn() };
54
+ const { rerender } = render(
55
+ <CredentialDisclosureDialog {...props} maskFresh revealContent={<p>synthetic_slot_value</p>} />,
56
+ );
57
+ await screen.findByRole('dialog');
58
+ expect(screen.getByRole('button', { name: 'Show secret' })).toHaveAttribute('aria-pressed', 'false');
59
+ expect(screen.getByRole('alert')).toBeInTheDocument();
60
+ expect(screen.getByText('synthetic_slot_value').closest('[data-kit-redact]')).not.toBeNull();
61
+ rerender(<CredentialDisclosureDialog {...props} maskFresh={false} />);
62
+ expect(screen.getByRole('button', { name: 'Show secret' })).toBeInTheDocument();
63
+ await user.click(screen.getByRole('button', { name: 'Show secret' }));
64
+ expect(screen.getByRole('textbox').closest('[data-kit-redact]')).not.toBeNull();
65
+ });
66
+
67
+ it.each(['stored', 'masked-fresh', 'fresh'] as const)(
68
+ 'dismisses retained %s props and resets a cleared or changed session in both presentations',
69
+ async (kind) => {
70
+ const user = userEvent.setup();
71
+ for (const presentation of ['dialog', 'inline'] as const) {
72
+ const props = {
73
+ disclosure: { ...storedDisclosure, variant: kind === 'stored' ? ('stored' as const) : ('fresh' as const) },
74
+ maskFresh: kind === 'masked-fresh',
75
+ presentation,
76
+ onClose: vi.fn(),
77
+ };
78
+ const { rerender, unmount } = render(<CredentialDisclosureDialog {...props} />);
79
+ await screen.findByRole(presentation === 'dialog' ? 'dialog' : 'region', { name: 'Credential secret' });
80
+ if (kind !== 'fresh') await user.click(screen.getByRole('button', { name: 'Show secret' }));
81
+ await user.click(screen.getByRole('button', { name: 'Close' }));
82
+ expect(props.onClose).toHaveBeenCalledOnce();
83
+ rerender(<CredentialDisclosureDialog {...props} />);
84
+ expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
85
+ expect(document.body.textContent).not.toContain(storedDisclosure.secret);
86
+ rerender(<CredentialDisclosureDialog {...props} disclosure={null} />);
87
+ rerender(<CredentialDisclosureDialog {...props} />);
88
+ await screen.findByRole(presentation === 'dialog' ? 'dialog' : 'region', { name: 'Credential secret' });
89
+ if (kind !== 'fresh') {
90
+ expect(screen.getByRole('button', { name: 'Show secret' })).toHaveAttribute('aria-pressed', 'false');
91
+ await user.click(screen.getByRole('button', { name: 'Show secret' }));
92
+ } else expect(screen.getByRole('region', { name: 'Secret' })).toHaveTextContent(storedDisclosure.secret);
93
+ rerender(
94
+ <CredentialDisclosureDialog {...props} disclosure={{ ...props.disclosure, secret: 'synthetic_replaced' }} />,
95
+ );
96
+ if (kind !== 'fresh')
97
+ expect(screen.getByRole('button', { name: 'Show secret' })).toHaveAttribute('aria-pressed', 'false');
98
+ unmount();
99
+ }
100
+ },
101
+ );
102
+
103
+ it('renders an inline named section without moving focus, trapping Tab or handling Escape; presentation is fixed per opening', async () => {
104
+ const user = userEvent.setup();
105
+ const outside = document.createElement('button');
106
+ document.body.append(outside);
107
+ outside.focus();
108
+ const onClose = vi.fn();
109
+ const props = {
110
+ disclosure: { ...storedDisclosure, variant: 'fresh' as const },
111
+ onClose,
112
+ title: 'Inline credentials',
113
+ presentation: 'inline' as const,
114
+ };
115
+ const { rerender, unmount } = render(<CredentialDisclosureDialog {...props} />);
116
+ const section = screen.getByRole('region', { name: 'Inline credentials' });
117
+ expect(section.tagName).toBe('SECTION');
118
+ expect(outside).toHaveFocus();
119
+ expect(screen.getByRole('note')).toBeInTheDocument();
120
+ await user.keyboard('{Escape}');
121
+ expect(onClose).not.toHaveBeenCalled();
122
+ rerender(<CredentialDisclosureDialog {...props} presentation="dialog" />);
123
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
124
+ const close = screen.getByRole('button', { name: 'Close' });
125
+ close.focus();
126
+ await user.tab();
127
+ expect(section.contains(document.activeElement)).toBe(false);
128
+ await user.click(close);
129
+ expect(outside).toHaveFocus();
130
+ unmount();
131
+ outside.remove();
132
+ });
133
+
134
+ it.each(['inline', 'dialog'] as const)(
135
+ 'preserves surviving focus and recovers removed slot/session focus (%s)',
136
+ async (presentation) => {
137
+ const outside = document.createElement('button');
138
+ document.body.append(outside);
139
+ const props = {
140
+ disclosure: storedDisclosure,
141
+ onClose: vi.fn(),
142
+ presentation,
143
+ revealContent: <button>Slot action</button>,
144
+ };
145
+ const { rerender, unmount } = render(<CredentialDisclosureDialog {...props} />);
146
+ await screen.findByRole(presentation === 'dialog' ? 'dialog' : 'region', { name: 'Credential secret' });
147
+ const copy = screen.getByRole('button', { name: 'Copy secret' });
148
+ copy.focus();
149
+ rerender(<CredentialDisclosureDialog {...props} revealContent={<button>Replacement slot</button>} />);
150
+ expect(copy).toHaveFocus();
151
+ screen.getByRole('button', { name: 'Replacement slot' }).focus();
152
+ rerender(<CredentialDisclosureDialog {...props} revealContent={null} />);
153
+ const fallback = () =>
154
+ presentation === 'dialog'
155
+ ? screen.getByRole('dialog')
156
+ : screen.getByRole('heading', { name: 'Credential secret' });
157
+ expect(fallback()).toHaveFocus();
158
+ screen.getByRole('button', { name: 'Show secret' }).focus();
159
+ rerender(
160
+ <CredentialDisclosureDialog {...props} disclosure={{ ...storedDisclosure, secret: 'synthetic_next_session' }} />,
161
+ );
162
+ await waitFor(() => expect(fallback()).toHaveFocus());
163
+ unmount();
164
+ outside.remove();
165
+ },
166
+ );
167
+
168
+ it('leaves outside focus alone during inline updates and returns focused inline unmounts to a live fallback', () => {
169
+ const outside = document.createElement('button');
170
+ document.body.append(outside);
171
+ outside.focus();
172
+ const props = {
173
+ disclosure: storedDisclosure,
174
+ presentation: 'inline' as const,
175
+ onClose: vi.fn(),
176
+ resolveReturnFocus: () => null,
177
+ fallbackFocus: () => outside,
178
+ };
179
+ const { rerender, unmount } = render(<CredentialDisclosureDialog {...props} revealContent={<button>Slot</button>} />);
180
+ rerender(
181
+ <CredentialDisclosureDialog
182
+ {...props}
183
+ revealContent={null}
184
+ disclosure={{ ...storedDisclosure, secret: 'synthetic_changed' }}
185
+ />,
186
+ );
187
+ expect(outside).toHaveFocus();
188
+ screen.getByRole('button', { name: 'Copy secret' }).focus();
189
+ unmount();
190
+ expect(outside).toHaveFocus();
191
+ outside.remove();
192
+ });
193
+
194
+ it('filters invalid ids and first duplicate wins; changed additional values reset every mask', async () => {
195
+ const user = userEvent.setup();
196
+ const entries = [
197
+ { id: '', label: 'Empty', value: 'bad' },
198
+ { id: 'secret', label: 'Reserved', value: 'bad' },
199
+ ...additionalSecrets,
200
+ { id: 'dsn', label: 'Duplicate', value: 'bad' },
201
+ ];
202
+ const props = { disclosure: storedDisclosure, onClose: vi.fn(), additionalSecrets: entries };
203
+ const { rerender } = render(<CredentialDisclosureDialog {...props} />);
204
+ await screen.findByRole('dialog');
205
+ expect(screen.getAllByRole('group').map((node) => node.getAttribute('aria-label'))).toEqual([
206
+ 'Secret',
207
+ 'DSN',
208
+ 'Password',
209
+ ]);
210
+ await user.click(within(screen.getByRole('group', { name: 'DSN' })).getByRole('button', { name: 'Show secret' }));
211
+ rerender(
212
+ <CredentialDisclosureDialog
213
+ {...props}
214
+ additionalSecrets={[{ ...additionalSecrets[0]!, value: 'synthetic_changed_dsn' }]}
215
+ />,
216
+ );
217
+ for (const button of screen.getAllByRole('button', { name: 'Show secret' }))
218
+ expect(button).toHaveAttribute('aria-pressed', 'false');
219
+ });
220
+
221
+ function observeAnnouncements() {
222
+ const messages: string[] = [];
223
+ const observer = new MutationObserver((records) => {
224
+ for (const record of records) {
225
+ const region =
226
+ record.target instanceof Element
227
+ ? record.target.closest('[data-live-announcer]')
228
+ : record.target.parentElement?.closest('[data-live-announcer]');
229
+ if (region?.textContent) messages.push(region.textContent.trim());
230
+ }
231
+ });
232
+ observer.observe(document.body, { subtree: true, childList: true, characterData: true });
233
+ return { messages, stop: () => observer.disconnect() };
234
+ }
235
+
236
+ it.each(['stored', 'fresh', 'masked-fresh'] as const)(
237
+ 'keeps a pending single-secret copy interactive without busy feedback (%s)',
238
+ async (kind) => {
239
+ const user = userEvent.setup();
240
+ const settlements: (() => void)[] = [];
241
+ const writeText = vi.fn(() => new Promise<void>((resolve) => settlements.push(resolve)));
242
+ Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
243
+ const onCopy = vi.fn();
244
+ render(
245
+ <CredentialDisclosureDialog
246
+ disclosure={{ ...storedDisclosure, variant: kind === 'stored' ? 'stored' : 'fresh' }}
247
+ maskFresh={kind === 'masked-fresh'}
248
+ onClose={() => {}}
249
+ onCopy={onCopy}
250
+ />,
251
+ );
252
+ const button = await screen.findByRole('button', { name: 'Copy secret' });
253
+ const observation = observeAnnouncements();
254
+ await user.click(button);
255
+ expect(writeText).toHaveBeenCalledExactlyOnceWith(storedDisclosure.secret);
256
+ expect(button).not.toHaveAttribute('aria-busy');
257
+ expect(button).toBeEnabled();
258
+ await user.click(button);
259
+ expect(writeText).toHaveBeenCalledTimes(2);
260
+ expect(observation.messages).toEqual([]);
261
+ expect(onCopy).not.toHaveBeenCalled();
262
+ await act(async () => settlements[0]!());
263
+ await act(async () => settlements[1]!());
264
+ expect(observation.messages).toEqual(['Secret copied to clipboard', 'Secret copied to clipboard']);
265
+ expect(onCopy.mock.calls).toEqual([['secret'], ['secret']]);
266
+ observation.stop();
267
+ },
268
+ );
269
+
270
+ it('announces each settled copy once, including repeats, failures and deferred attempts; serializes pending activations', async () => {
271
+ const user = userEvent.setup();
272
+ let settle!: () => void;
273
+ const writeText = vi.fn<() => Promise<void>>().mockResolvedValue(undefined);
274
+ Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
275
+ const onCopy = vi.fn();
276
+ render(
277
+ <CredentialDisclosureDialog
278
+ disclosure={storedDisclosure}
279
+ onClose={() => {}}
280
+ additionalSecrets={additionalSecrets}
281
+ onCopy={onCopy}
282
+ />,
283
+ );
284
+ const dsn = await screen.findByRole('group', { name: 'DSN' });
285
+ const observation = observeAnnouncements();
286
+ const button = within(dsn).getByRole('button', { name: 'Copy secret' });
287
+ await user.click(button);
288
+ await user.click(button);
289
+ expect(observation.messages).toEqual(['DSN: Copied', 'DSN: Copied']);
290
+ writeText.mockRejectedValueOnce(new Error('Synthetic denial'));
291
+ await user.click(button);
292
+ expect(observation.messages).toHaveLength(3);
293
+ expect(observation.messages[2]).not.toContain('synthetic_');
294
+ for (let attempt = 0; attempt < 2; attempt++) {
295
+ writeText.mockClear();
296
+ onCopy.mockClear();
297
+ observation.messages.length = 0;
298
+ writeText.mockImplementationOnce(
299
+ () =>
300
+ new Promise<void>((resolve) => {
301
+ settle = resolve;
302
+ }),
303
+ );
304
+ await user.click(button);
305
+ fireEvent.click(button);
306
+ expect(button).toHaveAttribute('aria-busy', 'true');
307
+ expect(writeText).toHaveBeenCalledExactlyOnceWith('synthetic_dsn_value');
308
+ expect(onCopy).not.toHaveBeenCalled();
309
+ expect(observation.messages).toEqual([]);
310
+ await act(async () => settle());
311
+ expect(writeText).toHaveBeenCalledOnce();
312
+ expect(onCopy).toHaveBeenCalledExactlyOnceWith('dsn');
313
+ expect(observation.messages).toEqual(['DSN: Copied']);
314
+ }
315
+ observation.stop();
316
+ });
317
+
318
+ it('announces two secrets settling in the same tick through separate regions', async () => {
319
+ const user = userEvent.setup();
320
+ const settlements: (() => void)[] = [];
321
+ const writeText = vi.fn(() => new Promise<void>((resolve) => settlements.push(resolve)));
322
+ Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
323
+ const onCopy = vi.fn();
324
+ render(
325
+ <CredentialDisclosureDialog
326
+ disclosure={storedDisclosure}
327
+ onClose={() => {}}
328
+ additionalSecrets={additionalSecrets}
329
+ onCopy={onCopy}
330
+ />,
331
+ );
332
+ await screen.findByRole('dialog');
333
+ const observation = observeAnnouncements();
334
+ await user.click(within(screen.getByRole('group', { name: 'Secret' })).getByRole('button', { name: 'Copy secret' }));
335
+ await user.click(within(screen.getByRole('group', { name: 'DSN' })).getByRole('button', { name: 'Copy secret' }));
336
+ await act(async () => settlements.forEach((resolve) => resolve()));
337
+ expect(observation.messages.sort()).toEqual(['DSN: Copied', 'Secret: Copied']);
338
+ expect(onCopy.mock.calls).toEqual([['secret'], ['dsn']]);
339
+ observation.stop();
340
+ });
341
+
342
+ it('keeps synthetic values out of persistence, navigation, announcements and console across reveal/copy/close/remount', async () => {
343
+ const user = userEvent.setup();
344
+ const logs = ['log', 'info', 'warn', 'error', 'debug'].map((method) => vi.spyOn(console, method as 'log'));
345
+ const localWrite = vi.spyOn(Storage.prototype, 'setItem');
346
+ const cookieWrite = vi.spyOn(document, 'cookie', 'set');
347
+ const push = vi.spyOn(history, 'pushState');
348
+ const replace = vi.spyOn(history, 'replaceState');
349
+ const indexedOpen = vi.fn();
350
+ vi.stubGlobal('indexedDB', { open: indexedOpen });
351
+ const initialLocation = location.href;
352
+ const writeText = vi.fn().mockResolvedValue(undefined);
353
+ Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
354
+ try {
355
+ for (const presentation of ['dialog', 'inline'] as const) {
356
+ const { unmount } = render(
357
+ <CredentialDisclosureDialog
358
+ disclosure={storedDisclosure}
359
+ additionalSecrets={additionalSecrets}
360
+ presentation={presentation}
361
+ onClose={() => {}}
362
+ revealContent={<p>synthetic_slot_value</p>}
363
+ />,
364
+ );
365
+ await screen.findByRole(presentation === 'dialog' ? 'dialog' : 'region', { name: 'Credential secret' });
366
+ for (const button of screen.getAllByRole('button', { name: 'Show secret' })) await user.click(button);
367
+ for (const button of screen.getAllByRole('button', { name: 'Copy secret' })) await user.click(button);
368
+ for (const textbox of screen.getAllByRole('textbox')) expect(textbox.closest('[data-kit-redact]')).not.toBeNull();
369
+ for (const region of document.querySelectorAll('[data-live-announcer]'))
370
+ expect(region.textContent).not.toContain('synthetic_');
371
+ await user.click(screen.getByRole('button', { name: 'Close' }));
372
+ unmount();
373
+ }
374
+ expect(localWrite).not.toHaveBeenCalled();
375
+ expect(cookieWrite).not.toHaveBeenCalled();
376
+ expect(indexedOpen).not.toHaveBeenCalled();
377
+ expect(push).not.toHaveBeenCalled();
378
+ expect(replace).not.toHaveBeenCalled();
379
+ expect(location.href).toBe(initialLocation);
380
+ for (const value of [
381
+ JSON.stringify(Object.entries(localStorage)),
382
+ JSON.stringify(Object.entries(sessionStorage)),
383
+ document.cookie,
384
+ JSON.stringify(history.state),
385
+ ])
386
+ expect(String(value)).not.toContain('synthetic_');
387
+ for (const log of logs) expect(JSON.stringify(log.mock.calls)).not.toContain('synthetic_');
388
+ } finally {
389
+ logs.forEach((log) => log.mockRestore());
390
+ localWrite.mockRestore();
391
+ cookieWrite.mockRestore();
392
+ push.mockRestore();
393
+ replace.mockRestore();
394
+ vi.unstubAllGlobals();
395
+ }
396
+ });
397
+
398
+ it('returns modal focus when the caller clears disclosure directly', async () => {
399
+ const outside = document.createElement('button');
400
+ document.body.append(outside);
401
+ outside.focus();
402
+ const props = { disclosure: storedDisclosure, onClose: vi.fn(), resolveReturnFocus: () => outside };
403
+ const { rerender } = render(<CredentialDisclosureDialog {...props} />);
404
+ await waitFor(() => expect(screen.getByRole('dialog')).toHaveFocus());
405
+ rerender(<CredentialDisclosureDialog {...props} disclosure={null} />);
406
+ await waitFor(() => expect(outside).toHaveFocus());
407
+ outside.remove();
408
+ });
409
+
410
+ it('keeps single-secret defaults when no additional id is valid, including the fresh CodeBlock and announcement', async () => {
411
+ const user = userEvent.setup();
412
+ const writeText = vi.fn().mockResolvedValue(undefined);
413
+ Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
414
+ const onCopy = vi.fn();
415
+ render(
416
+ <CredentialDisclosureDialog
417
+ disclosure={{ ...storedDisclosure, variant: 'fresh' }}
418
+ onClose={() => {}}
419
+ additionalSecrets={[
420
+ { id: '', label: 'Empty', value: 'bad' },
421
+ { id: 'secret', label: 'Reserved', value: 'bad' },
422
+ ]}
423
+ copiedAnnouncementFor={() => 'Unused override'}
424
+ onCopy={onCopy}
425
+ />,
426
+ );
427
+ await screen.findByRole('dialog');
428
+ const code = screen.getByRole('region', { name: 'Secret' });
429
+ expect(code).toHaveTextContent(storedDisclosure.secret);
430
+ expect(code.closest('[data-kit-redact]')).not.toBeNull();
431
+ expect(screen.queryByRole('group')).not.toBeInTheDocument();
432
+ expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
433
+ await user.click(screen.getByRole('button', { name: 'Copy secret' }));
434
+ expect(document.querySelector('[data-live-announcer="polite"]')?.textContent).toBe('Secret copied to clipboard');
435
+ expect(onCopy).toHaveBeenCalledExactlyOnceWith('secret');
436
+ });
437
+
438
+ it('does not claim focus when an unrelated focused control outside the inline section is removed', () => {
439
+ const outside = document.createElement('button');
440
+ document.body.append(outside);
441
+ const props = { disclosure: storedDisclosure, presentation: 'inline' as const, onClose: vi.fn() };
442
+ const { rerender } = render(<CredentialDisclosureDialog {...props} />);
443
+ outside.focus();
444
+ outside.remove();
445
+ rerender(<CredentialDisclosureDialog {...props} revealContent={<p>Updated content</p>} />);
446
+ expect(screen.getByRole('region', { name: 'Credential secret' }).contains(document.activeElement)).toBe(false);
447
+ });
448
+
449
+ it.each(['dialog', 'inline'] as const)('multi-secret %s is axe-clean in all three themes', async (presentation) => {
450
+ const { unmount } = render(
451
+ <CredentialDisclosureDialog
452
+ disclosure={{ ...storedDisclosure, variant: 'fresh' }}
453
+ presentation={presentation}
454
+ maskFresh
455
+ additionalSecrets={additionalSecrets}
456
+ onClose={() => {}}
457
+ />,
458
+ );
459
+ const surface = await screen.findByRole(presentation === 'dialog' ? 'dialog' : 'region', {
460
+ name: 'Credential secret',
461
+ });
462
+ for (const theme of ['dark', 'light', 'high_contrast']) {
463
+ document.documentElement.className = theme;
464
+ await expectNoAxeViolations(surface);
465
+ }
466
+ unmount();
467
+ });