@centreon/ui 26.12.1 → 26.12.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.
Files changed (56) hide show
  1. package/package.json +6 -3
  2. package/public/mockServiceWorker.js +31 -8
  3. package/src/Dashboard/Dashboard.browser.test.tsx +107 -0
  4. package/src/Form/Form.browser.test.tsx +445 -0
  5. package/src/Form/Inputs/ExclusiveCheckboxGroup.browser.test.tsx +304 -0
  6. package/src/Form/Inputs/SegmentedButtons.browser.test.tsx +113 -0
  7. package/src/InputField/Number/Number.browser.test.tsx +109 -0
  8. package/src/InputField/Select/Autocomplete/Connected/Multi/MultiConnectedAutocompleteField.browser.test.tsx +250 -0
  9. package/src/Listing/Listing.browser.test.tsx +476 -0
  10. package/src/Module/Module.browser.test.tsx +145 -0
  11. package/src/Pagination/Pagination.browser.test.tsx +184 -0
  12. package/src/RichTextEditor/RichTextEditor.browser.test.tsx +567 -0
  13. package/src/RichTextEditor/plugins/FloatingLinkEditorPlugin.tsx +3 -1
  14. package/src/TimePeriods/ResolutionTimePeriod.browser.test.tsx +143 -0
  15. package/src/TimePeriods/TimePeriods.browser.test.tsx +468 -0
  16. package/src/Typography/FluidTypography/FluidTypography.browser.test.tsx +54 -0
  17. package/src/api/useMutationQuery/useMutationQuery.browser.test.tsx +154 -0
  18. package/src/components/CollapsibleItem/CollapsibleItem.browser.test.tsx +120 -0
  19. package/src/components/CopyCommand/CopyCommand.browser.test.tsx +287 -0
  20. package/src/components/CrudPage/CrudPage.browser.test.tsx +659 -0
  21. package/src/components/DataTable/DataTable.browser.test.tsx +257 -0
  22. package/src/components/Form/AccessRights/AccessRights.browser.test.tsx +527 -0
  23. package/src/components/ItemComposition/ItemComposition.browser.test.tsx +172 -0
  24. package/src/components/Layout/PageLayout.browser.test.tsx +114 -0
  25. package/src/components/Modal/ConfirmationModal/ConfirmationModal.browser.test.tsx +237 -0
  26. package/src/components/SortableEntriesList/SortableEntriesList.browser.test.tsx +981 -0
  27. package/src/components/Tabs/Tabs.browser.test.tsx +115 -0
  28. package/src/utils/useFullscreen/Fullscreen.browser.test.tsx +176 -0
  29. package/src/utils/useLocale/useLocale.browser.test.tsx +49 -0
  30. package/src/utils/useLocaleTimezoneDate/{LocaleTimezoneDate.cypress.spec.tsx → LocaleTimezoneDate.browser.test.tsx} +24 -11
  31. package/src/Dashboard/Dashboard.cypress.spec.tsx +0 -71
  32. package/src/Form/Form.cypress.spec.tsx +0 -306
  33. package/src/Form/Inputs/ExclusiveCheckboxGroup.cypress.spec.tsx +0 -236
  34. package/src/Form/Inputs/SegmentedButtons.cypress.spec.tsx +0 -101
  35. package/src/InputField/Number/Number.cypress.spec.tsx +0 -89
  36. package/src/InputField/Select/Autocomplete/Connected/Multi/MultiConnectedAutocompleteField.cypress.spec.tsx +0 -182
  37. package/src/Listing/Listing.cypress.spec.tsx +0 -349
  38. package/src/Module/Module.cypress.spec.tsx +0 -127
  39. package/src/Pagination/Pagination.cypress.spec.tsx +0 -140
  40. package/src/RichTextEditor/RichTextEditor.cypress.spec.tsx +0 -408
  41. package/src/TimePeriods/ResolutionTimePeriod.cypress.spec.tsx +0 -74
  42. package/src/TimePeriods/TimePeriods.cypress.spec.tsx +0 -488
  43. package/src/Typography/FluidTypography/FluidTypography.cypress.spec.tsx +0 -27
  44. package/src/api/useMutationQuery/useMutationQuery.cypress.spec.tsx +0 -93
  45. package/src/components/CollapsibleItem/CollapsibleItem.cypress.spec.tsx +0 -76
  46. package/src/components/CopyCommand/CopyCommand.cypress.spec.tsx +0 -167
  47. package/src/components/CrudPage/CrudPage.cypress.spec.tsx +0 -561
  48. package/src/components/DataTable/DataTable.cypress.spec.tsx +0 -174
  49. package/src/components/Form/AccessRights/AccessRights.cypress.spec.tsx +0 -376
  50. package/src/components/ItemComposition/ItemComposition.cypress.spec.tsx +0 -112
  51. package/src/components/Layout/PageLayout.cypress.spec.tsx +0 -64
  52. package/src/components/Modal/ConfirmationModal/ConfirmationModal.cypress.spec.tsx +0 -171
  53. package/src/components/SortableEntriesList/SortableEntriesList.cypress.spec.tsx +0 -792
  54. package/src/components/Tabs/Tabs.cypress.spec.tsx +0 -69
  55. package/src/utils/useFullscreen/Fullscreen.cypress.spec.tsx +0 -130
  56. package/src/utils/useLocale/useLocale.cypress.spec.tsx +0 -40
@@ -0,0 +1,154 @@
1
+ import { rs } from '@rstest/core';
2
+ import { interceptAPIRequest, waitForRequest } from '@test-setup/api';
3
+ import { render } from '@test-setup/render';
4
+ import { screen, waitFor } from '@testing-library/react';
5
+ import userEvent from '@testing-library/user-event';
6
+
7
+ import SnackbarProvider from '../../Snackbar/SnackbarProvider';
8
+ import TestQueryProvider from '../TestQueryProvider';
9
+ import useMutationQuery, { Method, type UseMutationQueryProps } from '.';
10
+
11
+ interface Payload {
12
+ a: string;
13
+ b: number;
14
+ c: Array<string>;
15
+ }
16
+
17
+ type MutationProps = Omit<
18
+ UseMutationQueryProps<Payload, unknown>,
19
+ 'getEndpoint'
20
+ >;
21
+
22
+ const TestComponent = (props: MutationProps): JSX.Element => {
23
+ const mutation = useMutationQuery<Payload, unknown>({
24
+ ...props,
25
+ getEndpoint: () => '/endpoint'
26
+ });
27
+
28
+ return (
29
+ <button
30
+ onClick={() =>
31
+ mutation.mutateAsync({ payload: { a: 'a', b: 2, c: ['arr', 'ay'] } })
32
+ }
33
+ type="button"
34
+ >
35
+ Send
36
+ </button>
37
+ );
38
+ };
39
+
40
+ interface InitializeProps {
41
+ isError?: boolean;
42
+ mutationProps: MutationProps & {
43
+ getEndpoint: () => string;
44
+ };
45
+ }
46
+
47
+ const initialize = ({
48
+ mutationProps,
49
+ isError = false
50
+ }: InitializeProps): void => {
51
+ interceptAPIRequest({
52
+ alias: 'mutateEndpoint',
53
+ method: mutationProps.method,
54
+ path: './api/latest/endpoint',
55
+ response: isError
56
+ ? {
57
+ message: 'custom error message'
58
+ }
59
+ : undefined,
60
+ statusCode: isError ? 400 : 204
61
+ });
62
+
63
+ render(
64
+ <SnackbarProvider>
65
+ <TestQueryProvider>
66
+ <TestComponent {...mutationProps} />
67
+ </TestQueryProvider>
68
+ </SnackbarProvider>
69
+ );
70
+ };
71
+
72
+ describe('useMutationQuery', () => {
73
+ beforeEach(() => {
74
+ expect.hasAssertions();
75
+ });
76
+
77
+ afterEach(() => {
78
+ rs.restoreAllMocks();
79
+ });
80
+
81
+ it('sends data to an endpoint', async () => {
82
+ // The recorded request has no headers: they are read from the call to
83
+ // `fetch` (the spy calls through).
84
+ const fetch = rs.spyOn(window, 'fetch');
85
+
86
+ initialize({
87
+ mutationProps: {
88
+ getEndpoint: () => '/endpoint',
89
+ method: Method.POST
90
+ }
91
+ });
92
+
93
+ await userEvent.click(screen.getByRole('button'));
94
+
95
+ const { request } = await waitForRequest('@mutateEndpoint');
96
+
97
+ expect(request.method).toBe('POST');
98
+ expect(request.body).toEqual({ a: 'a', b: 2, c: ['arr', 'ay'] });
99
+ expect(
100
+ new Headers(fetch.mock.calls[0][1]?.headers).get('content-type')
101
+ ).toBe('application/json');
102
+ });
103
+
104
+ it("shows an error from the API via the Snackbar and inside the browser's console when posting data to an endpoint", async () => {
105
+ const consoleError = rs
106
+ .spyOn(console, 'error')
107
+ .mockImplementation(() => undefined);
108
+
109
+ initialize({
110
+ isError: true,
111
+ mutationProps: {
112
+ getEndpoint: () => '/endpoint',
113
+ method: Method.POST
114
+ }
115
+ });
116
+
117
+ await userEvent.click(screen.getByRole('button'));
118
+
119
+ expect(await screen.findByText('custom error message')).toBeVisible();
120
+
121
+ expect(consoleError).toHaveBeenCalledWith(
122
+ 'API Request: custom error message'
123
+ );
124
+ });
125
+
126
+ it('does not show any message via the Snackbar when the httpCodesBypassErrorSnackbar is passed when posting data to an API', async () => {
127
+ const consoleError = rs
128
+ .spyOn(console, 'error')
129
+ .mockImplementation(() => undefined);
130
+
131
+ initialize({
132
+ isError: true,
133
+ mutationProps: {
134
+ getEndpoint: () => '/endpoint',
135
+ httpCodesBypassErrorSnackbar: [400],
136
+ method: Method.POST
137
+ }
138
+ });
139
+
140
+ await userEvent.click(screen.getByRole('button'));
141
+
142
+ // The error is handled (and logged) once the answer is applied. The
143
+ // snackbar would show in a later render: it is given 500 ms to appear.
144
+ await waitFor(() => {
145
+ expect(consoleError).toHaveBeenCalledWith(
146
+ 'API Request: custom error message'
147
+ );
148
+ });
149
+
150
+ await expect(
151
+ screen.findByText('custom error message', {}, { timeout: 500 })
152
+ ).rejects.toThrow();
153
+ });
154
+ });
@@ -0,0 +1,120 @@
1
+ import { render } from '@test-setup/render';
2
+ import { screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+
5
+ import { CollapsibleItem, type Props } from './CollapsibleItem';
6
+
7
+ const title = 'Title';
8
+
9
+ const customizedTitle = <div>Customized title</div>;
10
+
11
+ const initialize = ({ title, ...props }: Omit<Props, 'children'>): void => {
12
+ // The component also takes the accordion summary props, whose `title` is
13
+ // a string: its own `title` accepts an element as well.
14
+ render(
15
+ <CollapsibleItem {...props} title={title as string}>
16
+ Content
17
+ </CollapsibleItem>
18
+ );
19
+ };
20
+
21
+ const getExpandButton = (expanded: boolean): HTMLElement =>
22
+ document.querySelector(`button[aria-expanded="${expanded}"]`) as HTMLElement;
23
+
24
+ const expectExpandIconVisible = (): void => {
25
+ expect(screen.getByTestId('ExpandMoreIcon')).toBeVisible();
26
+ };
27
+
28
+ // A string title is rendered as an h6 heading.
29
+ const expectStringTitleHeading = (): void => {
30
+ expect(screen.getByRole('heading', { level: 6, name: title })).toBeVisible();
31
+ };
32
+
33
+ // The details' horizontal padding: 0 when compact, 16px otherwise.
34
+ const getDetailsPaddingLeft = (): string =>
35
+ getComputedStyle(
36
+ screen.getByText('Content').closest('.MuiAccordionDetails-root') as Element
37
+ ).paddingLeft;
38
+
39
+ describe('CollapsibleItem', () => {
40
+ beforeEach(() => {
41
+ expect.hasAssertions();
42
+ });
43
+
44
+ it('displays the component collapsed by default', async () => {
45
+ initialize({ title });
46
+
47
+ expect(await screen.findByText(title)).toBeVisible();
48
+ expect(screen.getByText('Content')).not.toBeVisible();
49
+ expect(getExpandButton(false)).toBeInTheDocument();
50
+
51
+ expectStringTitleHeading();
52
+ expectExpandIconVisible();
53
+ });
54
+
55
+ it('displays the component expanded when the corresponding prop is set to true', async () => {
56
+ initialize({ defaultExpanded: true, title });
57
+
58
+ expect(await screen.findByText(title)).toBeVisible();
59
+ expect(screen.getByText('Content')).toBeVisible();
60
+ expect(getExpandButton(true)).toBeInTheDocument();
61
+
62
+ expectStringTitleHeading();
63
+ expectExpandIconVisible();
64
+ expect(getDetailsPaddingLeft()).toBe('16px');
65
+ });
66
+
67
+ it('displays a customized title', async () => {
68
+ initialize({ title: customizedTitle });
69
+
70
+ expect(await screen.findByText('Customized title')).toBeVisible();
71
+ expect(getExpandButton(false)).toBeInTheDocument();
72
+
73
+ expect(screen.queryByRole('heading', { level: 6 })).not.toBeInTheDocument();
74
+ expectExpandIconVisible();
75
+ });
76
+
77
+ it('displays the component as compact', async () => {
78
+ initialize({ compact: true, title });
79
+
80
+ expect(await screen.findByText(title)).toBeVisible();
81
+ expect(getExpandButton(false)).toBeInTheDocument();
82
+ expect(
83
+ document.querySelector('div[data-compact="true"]')
84
+ ).toBeInTheDocument();
85
+
86
+ expectStringTitleHeading();
87
+ expectExpandIconVisible();
88
+ });
89
+
90
+ it('displays the component as compact and expanded when the icon is clicked', async () => {
91
+ initialize({ compact: true, title });
92
+
93
+ expect(await screen.findByText(title)).toBeVisible();
94
+ expect(getExpandButton(false)).toBeInTheDocument();
95
+
96
+ await userEvent.click(getExpandButton(false));
97
+
98
+ await waitFor(() => {
99
+ expect(getExpandButton(true)).toBeInTheDocument();
100
+ });
101
+ await waitFor(() => {
102
+ expect(screen.getByText('Content')).toBeVisible();
103
+ });
104
+
105
+ expect(getDetailsPaddingLeft()).toBe('0px');
106
+ });
107
+
108
+ it('displays the component as compact and a customized title', async () => {
109
+ initialize({ compact: true, title: customizedTitle });
110
+
111
+ expect(await screen.findByText('Customized title')).toBeVisible();
112
+ expect(getExpandButton(false)).toBeInTheDocument();
113
+ expect(
114
+ document.querySelector('div[data-compact="true"]')
115
+ ).toBeInTheDocument();
116
+
117
+ expect(screen.queryByRole('heading', { level: 6 })).not.toBeInTheDocument();
118
+ expectExpandIconVisible();
119
+ });
120
+ });
@@ -0,0 +1,287 @@
1
+ import { ThemeMode, type User, userAtom } from '@centreon/ui-context';
2
+
3
+ import { rs } from '@rstest/core';
4
+ import { render } from '@test-setup/render';
5
+ import { screen, waitFor } from '@testing-library/react';
6
+ import userEvent from '@testing-library/user-event';
7
+ import { createStore, Provider } from 'jotai';
8
+
9
+ import SnackbarProvider from '../../Snackbar/SnackbarProvider';
10
+ import ThemeProvider from '../../ThemeProvider';
11
+ import CopyCommand, { type CopyCommandProps } from './CopyCommand';
12
+
13
+ const initialize = (props: CopyCommandProps & { theme?: ThemeMode }): void => {
14
+ const store = createStore();
15
+ store.set(userAtom, { themeMode: props.theme || ThemeMode.light } as User);
16
+ render(
17
+ <Provider store={store}>
18
+ <ThemeProvider>
19
+ <SnackbarProvider>
20
+ <CopyCommand {...props} />
21
+ </SnackbarProvider>
22
+ </ThemeProvider>
23
+ </Provider>
24
+ );
25
+ };
26
+
27
+ const jsonText = `{
28
+ "number": 1,
29
+ "boolean": true,
30
+ "array": [
31
+ {
32
+ "string": "text"
33
+ }
34
+ ]
35
+ }`;
36
+
37
+ const getCode = (): HTMLElement => {
38
+ const code = document.querySelector('pre');
39
+
40
+ if (!code) {
41
+ throw new Error('No highlighted code block');
42
+ }
43
+
44
+ return code;
45
+ };
46
+
47
+ // The highlighted token of the given highlight.js class and exact text, with
48
+ // its computed colour.
49
+ const expectToken = ({
50
+ className,
51
+ color,
52
+ text
53
+ }: {
54
+ className: string;
55
+ color: string;
56
+ text: string;
57
+ }): void => {
58
+ const token = Array.from(
59
+ getCode().querySelectorAll<HTMLElement>(`.${className}`)
60
+ ).find((element) => element.textContent === text);
61
+
62
+ expect(token).toBeDefined();
63
+ expect(token).toBeVisible();
64
+ expect(getComputedStyle(token as HTMLElement).color).toBe(color);
65
+ };
66
+
67
+ // The language chip shows the language, and only it.
68
+ const expectLanguageChip = (language: string): void => {
69
+ expect(screen.getByText(language, { selector: 'div' })).toHaveTextContent(
70
+ new RegExp(`^${language}$`)
71
+ );
72
+ };
73
+
74
+ describe('CopyCommand', () => {
75
+ beforeEach(() => {
76
+ expect.hasAssertions();
77
+ });
78
+
79
+ it('displays bash code when props are set', async () => {
80
+ initialize({
81
+ commandToCopy: 'echo "hello" | grep "hel"',
82
+ language: 'bash',
83
+ text: `# a simple command
84
+ echo "hello" | grep "hel"`
85
+ });
86
+
87
+ expect(await screen.findByText('# a simple command')).toBeVisible();
88
+ expect(screen.getByText('echo')).toBeVisible();
89
+ expect(screen.getByText('bash')).toBeVisible();
90
+ expect(screen.getByTestId('Copy command')).toBeVisible();
91
+
92
+ expect(getCode()).toHaveTextContent(
93
+ /^# a simple command echo "hello" \| grep "hel"$/
94
+ );
95
+ expectLanguageChip('bash');
96
+ expectToken({
97
+ className: 'hljs-comment',
98
+ color: 'rgb(106, 115, 125)',
99
+ text: '# a simple command'
100
+ });
101
+ expectToken({
102
+ className: 'hljs-built_in',
103
+ color: 'rgb(227, 98, 9)',
104
+ text: 'echo'
105
+ });
106
+ expectToken({
107
+ className: 'hljs-string',
108
+ color: 'rgb(3, 47, 98)',
109
+ text: '"hello"'
110
+ });
111
+ expectToken({
112
+ className: 'hljs-string',
113
+ color: 'rgb(3, 47, 98)',
114
+ text: '"hel"'
115
+ });
116
+ });
117
+
118
+ it('displays yaml code when props are set', async () => {
119
+ initialize({
120
+ commandToCopy: 'echo "hello" | grep "hel"',
121
+ language: 'yaml',
122
+ text: `key:
123
+ with:
124
+ input: "input"`
125
+ });
126
+
127
+ expect(await screen.findByText('key:')).toBeVisible();
128
+ expect(screen.getByText('"input"')).toBeVisible();
129
+ expect(screen.getByText('yaml')).toBeVisible();
130
+
131
+ expectLanguageChip('yaml');
132
+ expect(screen.getByTestId('Copy command')).toBeVisible();
133
+ // The indentation is kept, as non-breaking spaces.
134
+ expect(getCode().textContent).toMatch(/\n\u00a0+with:\n\u00a0+input:/);
135
+ expectToken({
136
+ className: 'hljs-attr',
137
+ color: 'rgb(0, 92, 197)',
138
+ text: 'key:'
139
+ });
140
+ expectToken({
141
+ className: 'hljs-attr',
142
+ color: 'rgb(0, 92, 197)',
143
+ text: 'input:'
144
+ });
145
+ expectToken({
146
+ className: 'hljs-string',
147
+ color: 'rgb(3, 47, 98)',
148
+ text: '"input"'
149
+ });
150
+ });
151
+
152
+ it('displays php code when props are set', async () => {
153
+ initialize({
154
+ commandToCopy: 'echo "hello" | grep "hel"',
155
+ language: 'php',
156
+ text: `<?php
157
+ echo 'Hello ' . htmlspecialchars($_POST["name"]) . '!';
158
+ ?>`
159
+ });
160
+
161
+ expect(await screen.findByText('echo')).toBeVisible();
162
+ expect(screen.getByText('"name"')).toBeVisible();
163
+ expect(screen.getByText('php')).toBeVisible();
164
+
165
+ expectLanguageChip('php');
166
+ expect(screen.getByTestId('Copy command')).toBeVisible();
167
+ expect(getCode()).toHaveTextContent(
168
+ `<?php echo 'Hello ' . htmlspecialchars($_POST["name"]) . '!'; ?>`
169
+ );
170
+ expectToken({
171
+ className: 'hljs-keyword',
172
+ color: 'rgb(215, 58, 73)',
173
+ text: 'echo'
174
+ });
175
+ expectToken({
176
+ className: 'hljs-title',
177
+ color: 'rgb(111, 66, 193)',
178
+ text: 'htmlspecialchars'
179
+ });
180
+ expectToken({
181
+ className: 'hljs-variable',
182
+ color: 'rgb(0, 92, 197)',
183
+ text: '$_POST'
184
+ });
185
+ });
186
+
187
+ it('displays json code when props are set', async () => {
188
+ initialize({
189
+ commandToCopy: 'echo "hello" | grep "hel"',
190
+ language: 'json',
191
+ text: jsonText
192
+ });
193
+
194
+ expect(await screen.findByText('"number"')).toBeVisible();
195
+ expect(screen.getByText('true')).toBeVisible();
196
+ expect(screen.getByText('json')).toBeVisible();
197
+
198
+ expectLanguageChip('json');
199
+ expect(screen.getByTestId('Copy command')).toBeVisible();
200
+ expectToken({
201
+ className: 'hljs-attr',
202
+ color: 'rgb(0, 92, 197)',
203
+ text: '"number"'
204
+ });
205
+ expectToken({
206
+ className: 'hljs-number',
207
+ color: 'rgb(0, 92, 197)',
208
+ text: '1'
209
+ });
210
+ expectToken({
211
+ className: 'hljs-keyword',
212
+ color: 'rgb(215, 58, 73)',
213
+ text: 'true'
214
+ });
215
+ expectToken({
216
+ className: 'hljs-string',
217
+ color: 'rgb(3, 47, 98)',
218
+ text: '"text"'
219
+ });
220
+ });
221
+
222
+ it('does not display the copy button when the corresponding prop is not passed', async () => {
223
+ initialize({
224
+ language: 'json',
225
+ text: jsonText
226
+ });
227
+
228
+ expect(await screen.findByText('"number"')).toBeVisible();
229
+ expect(screen.queryByTestId('Copy command')).not.toBeInTheDocument();
230
+
231
+ expectLanguageChip('json');
232
+ });
233
+
234
+ it('displays the highlighted code in dark mode when the theme is changed', async () => {
235
+ initialize({
236
+ language: 'json',
237
+ text: jsonText,
238
+ theme: ThemeMode.dark
239
+ });
240
+
241
+ await waitFor(() => {
242
+ const keyword = document.querySelector('.hljs-keyword');
243
+
244
+ expect(keyword).not.toBeNull();
245
+ expect(getComputedStyle(keyword as Element).color).toBe(
246
+ 'rgb(255, 123, 114)'
247
+ );
248
+ });
249
+
250
+ // The rest of the dark palette.
251
+ expectToken({
252
+ className: 'hljs-attr',
253
+ color: 'rgb(121, 192, 255)',
254
+ text: '"number"'
255
+ });
256
+ expectToken({
257
+ className: 'hljs-string',
258
+ color: 'rgb(165, 214, 255)',
259
+ text: '"text"'
260
+ });
261
+ expectLanguageChip('json');
262
+ expect(screen.queryByTestId('Copy command')).not.toBeInTheDocument();
263
+ });
264
+
265
+ it('copies the command to the clipboard when the button is clicked', async () => {
266
+ const writeText = rs
267
+ .spyOn(navigator.clipboard, 'writeText')
268
+ .mockResolvedValue(undefined);
269
+
270
+ initialize({
271
+ commandToCopy: 'echo "hello" | grep "hel"',
272
+ language: 'bash',
273
+ text: `# a simple command
274
+ echo "hello" | grep "hel"`
275
+ });
276
+
277
+ await userEvent.click(await screen.findByTestId('Copy command'));
278
+
279
+ await waitFor(() => {
280
+ expect(writeText).toHaveBeenCalledTimes(1);
281
+ });
282
+ expect(writeText).toHaveBeenCalledWith('echo "hello" | grep "hel"');
283
+
284
+ expect(screen.getByText('# a simple command')).toBeVisible();
285
+ expectLanguageChip('bash');
286
+ });
287
+ });