@scality/data-browser-library 1.0.4 → 1.0.6

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 (60) hide show
  1. package/dist/components/DataBrowserUI.js +18 -8
  2. package/dist/components/__tests__/BucketCreate.test.js +60 -20
  3. package/dist/components/__tests__/BucketList.test.js +91 -6
  4. package/dist/components/__tests__/BucketNotificationFormPage.test.js +54 -19
  5. package/dist/components/__tests__/BucketReplicationFormPage.test.js +183 -61
  6. package/dist/components/__tests__/MetadataSearch.test.js +18 -12
  7. package/dist/components/__tests__/ObjectList.test.js +94 -2
  8. package/dist/components/buckets/BucketCreate.d.ts +1 -0
  9. package/dist/components/buckets/BucketCreate.js +57 -7
  10. package/dist/components/buckets/BucketDetails.js +0 -1
  11. package/dist/components/buckets/BucketLifecycleFormPage.js +209 -213
  12. package/dist/components/buckets/BucketList.js +25 -4
  13. package/dist/components/buckets/BucketReplicationFormPage.js +9 -3
  14. package/dist/components/buckets/DeleteBucketConfigRuleButton.js +1 -1
  15. package/dist/components/buckets/notifications/BucketNotificationList.js +1 -1
  16. package/dist/components/objects/DeleteObjectButton.d.ts +1 -0
  17. package/dist/components/objects/DeleteObjectButton.js +11 -5
  18. package/dist/components/objects/ObjectDetails/FormComponents.d.ts +9 -0
  19. package/dist/components/objects/ObjectDetails/FormComponents.js +37 -0
  20. package/dist/components/objects/ObjectDetails/ObjectMetadata.js +182 -204
  21. package/dist/components/objects/ObjectDetails/ObjectSummary.js +22 -5
  22. package/dist/components/objects/ObjectDetails/ObjectTags.js +109 -154
  23. package/dist/components/objects/ObjectDetails/__tests__/ObjectDetails.test.js +3 -3
  24. package/dist/components/objects/ObjectDetails/__tests__/ObjectMetadata.test.d.ts +1 -0
  25. package/dist/components/objects/ObjectDetails/__tests__/ObjectMetadata.test.js +230 -0
  26. package/dist/components/objects/ObjectDetails/__tests__/ObjectTags.test.d.ts +1 -0
  27. package/dist/components/objects/ObjectDetails/__tests__/ObjectTags.test.js +342 -0
  28. package/dist/components/objects/ObjectDetails/__tests__/formUtils.test.d.ts +1 -0
  29. package/dist/components/objects/ObjectDetails/__tests__/formUtils.test.js +202 -0
  30. package/dist/components/objects/ObjectDetails/index.d.ts +2 -1
  31. package/dist/components/objects/ObjectDetails/index.js +12 -16
  32. package/dist/components/objects/ObjectList.d.ts +3 -2
  33. package/dist/components/objects/ObjectList.js +204 -104
  34. package/dist/components/objects/ObjectLock/__tests__/ObjectLockSettings.test.js +78 -26
  35. package/dist/components/objects/ObjectPage.js +22 -5
  36. package/dist/components/ui/ArrayFieldActions.js +0 -2
  37. package/dist/components/ui/FilterFormSection.js +17 -36
  38. package/dist/components/ui/FormGrid.d.ts +7 -0
  39. package/dist/components/ui/FormGrid.js +37 -0
  40. package/dist/components/ui/Table.elements.js +1 -0
  41. package/dist/config/__tests__/factory.test.js +29 -1
  42. package/dist/config/factory.d.ts +2 -0
  43. package/dist/config/factory.js +3 -1
  44. package/dist/config/types.d.ts +45 -2
  45. package/dist/hooks/__tests__/usePresigningS3Client.test.d.ts +1 -0
  46. package/dist/hooks/__tests__/usePresigningS3Client.test.js +104 -0
  47. package/dist/hooks/factories/index.d.ts +1 -1
  48. package/dist/hooks/factories/index.js +2 -2
  49. package/dist/hooks/factories/useCreateS3MutationHook.d.ts +2 -1
  50. package/dist/hooks/factories/useCreateS3MutationHook.js +10 -3
  51. package/dist/hooks/factories/useCreateS3QueryHook.d.ts +1 -0
  52. package/dist/hooks/factories/useCreateS3QueryHook.js +9 -6
  53. package/dist/hooks/index.d.ts +1 -0
  54. package/dist/hooks/index.js +2 -1
  55. package/dist/hooks/presignedOperations.js +4 -4
  56. package/dist/hooks/useBucketLocations.d.ts +6 -0
  57. package/dist/hooks/useBucketLocations.js +45 -0
  58. package/dist/hooks/usePresigningS3Client.d.ts +13 -0
  59. package/dist/hooks/usePresigningS3Client.js +21 -0
  60. package/package.json +4 -3
@@ -1,5 +1,5 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { AppContainer } from "@scality/core-ui";
2
+ import { AppContainer, ScrollbarWrapper } from "@scality/core-ui";
3
3
  import { Route, Routes } from "react-router";
4
4
  import styled_components from "styled-components";
5
5
  import { DataBrowserUICustomizationProvider } from "../contexts/DataBrowserUICustomizationContext.js";
@@ -12,14 +12,22 @@ const RoutesContainer = styled_components.div`
12
12
  width: 100%;
13
13
  overflow: hidden;
14
14
  `;
15
+ const HeaderSlot = styled_components.div`
16
+ &:has([data-header-content]:empty) {
17
+ display: none;
18
+ }
19
+ `;
15
20
  const DataBrowserUIContent = ({ header })=>{
16
21
  useS3ConfigSwitch();
17
22
  return /*#__PURE__*/ jsx(AppContainer, {
18
23
  children: /*#__PURE__*/ jsxs(Fragment, {
19
24
  children: [
20
- header && /*#__PURE__*/ jsx(AppContainer.ContextContainer, {
21
- children: /*#__PURE__*/ jsx("div", {
22
- children: header
25
+ header && /*#__PURE__*/ jsx(HeaderSlot, {
26
+ children: /*#__PURE__*/ jsx(AppContainer.ContextContainer, {
27
+ children: /*#__PURE__*/ jsx("div", {
28
+ "data-header-content": true,
29
+ children: header
30
+ })
23
31
  })
24
32
  }),
25
33
  /*#__PURE__*/ jsx(AppContainer.MainContent, {
@@ -90,10 +98,12 @@ const DataBrowserUIContent = ({ header })=>{
90
98
  })
91
99
  });
92
100
  };
93
- const DataBrowserUI = (props)=>/*#__PURE__*/ jsx(DataBrowserUICustomizationProvider, {
94
- config: props,
95
- children: /*#__PURE__*/ jsx(DataBrowserUIContent, {
96
- header: props.header
101
+ const DataBrowserUI = (props)=>/*#__PURE__*/ jsx(ScrollbarWrapper, {
102
+ children: /*#__PURE__*/ jsx(DataBrowserUICustomizationProvider, {
103
+ config: props,
104
+ children: /*#__PURE__*/ jsx(DataBrowserUIContent, {
105
+ header: props.header
106
+ })
97
107
  })
98
108
  });
99
109
  export { DataBrowserUI };
@@ -76,7 +76,9 @@ const setupMocks = (overrides = {})=>{
76
76
  mockUseSetBucketEncryption.mockReturnValue(createMockMutationResult(mockMutations.setBucketEncryption));
77
77
  };
78
78
  const fillBucketName = async (name)=>{
79
- const nameInput = screen.getByLabelText(/bucket name/i);
79
+ const nameInput = screen.getByRole('textbox', {
80
+ name: /bucket name/i
81
+ });
80
82
  await user_event.type(nameInput, name);
81
83
  };
82
84
  const submitForm = async ()=>{
@@ -101,13 +103,21 @@ describe('BucketCreate', ()=>{
101
103
  it('renders form with required fields', ()=>{
102
104
  renderBucketCreate();
103
105
  expect(screen.getByText('Create a New Bucket')).toBeInTheDocument();
104
- expect(screen.getByLabelText(/bucket name/i)).toBeInTheDocument();
105
- expect(screen.getByText('Versioning')).toBeInTheDocument();
106
- expect(screen.getByText('Object-lock')).toBeInTheDocument();
106
+ expect(screen.getByRole('textbox', {
107
+ name: /bucket name/i
108
+ })).toBeInTheDocument();
109
+ expect(screen.getByRole('checkbox', {
110
+ name: /versioning/i
111
+ })).toBeInTheDocument();
112
+ expect(screen.getByRole('checkbox', {
113
+ name: /object-lock/i
114
+ })).toBeInTheDocument();
107
115
  });
108
116
  it('shows validation error for invalid bucket name with uppercase', async ()=>{
109
117
  renderBucketCreate();
110
- await user_event.type(screen.getByLabelText(/bucket name/i), 'INVALID');
118
+ await user_event.type(screen.getByRole('textbox', {
119
+ name: /bucket name/i
120
+ }), 'INVALID');
111
121
  await user_event.tab();
112
122
  await waitFor(()=>{
113
123
  expect(screen.getByText(/bucket names can include only lowercase/i)).toBeInTheDocument();
@@ -115,7 +125,9 @@ describe('BucketCreate', ()=>{
115
125
  });
116
126
  it('shows validation error for bucket name starting with hyphen', async ()=>{
117
127
  renderBucketCreate();
118
- await user_event.type(screen.getByLabelText(/bucket name/i), '-invalid');
128
+ await user_event.type(screen.getByRole('textbox', {
129
+ name: /bucket name/i
130
+ }), '-invalid');
119
131
  await user_event.tab();
120
132
  await waitFor(()=>{
121
133
  expect(screen.getByText(/bucket names can include only lowercase/i)).toBeInTheDocument();
@@ -123,7 +135,9 @@ describe('BucketCreate', ()=>{
123
135
  });
124
136
  it('shows validation error for bucket name ending with hyphen', async ()=>{
125
137
  renderBucketCreate();
126
- await user_event.type(screen.getByLabelText(/bucket name/i), 'invalid-');
138
+ await user_event.type(screen.getByRole('textbox', {
139
+ name: /bucket name/i
140
+ }), 'invalid-');
127
141
  await user_event.tab();
128
142
  await waitFor(()=>{
129
143
  expect(screen.getByText(/bucket names can include only lowercase/i)).toBeInTheDocument();
@@ -131,7 +145,9 @@ describe('BucketCreate', ()=>{
131
145
  });
132
146
  it('shows validation error for bucket name with adjacent periods', async ()=>{
133
147
  renderBucketCreate();
134
- await user_event.type(screen.getByLabelText(/bucket name/i), 'test..bucket');
148
+ await user_event.type(screen.getByRole('textbox', {
149
+ name: /bucket name/i
150
+ }), 'test..bucket');
135
151
  await user_event.tab();
136
152
  await waitFor(()=>{
137
153
  expect(screen.getByText(/bucket names cannot contain two adjacent periods/i)).toBeInTheDocument();
@@ -139,7 +155,9 @@ describe('BucketCreate', ()=>{
139
155
  });
140
156
  it('shows validation error for bucket name formatted as IP address', async ()=>{
141
157
  renderBucketCreate();
142
- await user_event.type(screen.getByLabelText(/bucket name/i), '192.168.1.1');
158
+ await user_event.type(screen.getByRole('textbox', {
159
+ name: /bucket name/i
160
+ }), '192.168.1.1');
143
161
  await user_event.tab();
144
162
  await waitFor(()=>{
145
163
  expect(screen.getByText(/bucket names must not be formatted as an ip address/i)).toBeInTheDocument();
@@ -147,7 +165,9 @@ describe('BucketCreate', ()=>{
147
165
  });
148
166
  it('shows validation error for bucket name with forbidden prefix', async ()=>{
149
167
  renderBucketCreate();
150
- await user_event.type(screen.getByLabelText(/bucket name/i), 'xn--test');
168
+ await user_event.type(screen.getByRole('textbox', {
169
+ name: /bucket name/i
170
+ }), 'xn--test');
151
171
  await user_event.tab();
152
172
  await waitFor(()=>{
153
173
  expect(screen.getByText(/bucket names must not start with/i)).toBeInTheDocument();
@@ -155,7 +175,9 @@ describe('BucketCreate', ()=>{
155
175
  });
156
176
  it('shows validation error for bucket name with forbidden suffix', async ()=>{
157
177
  renderBucketCreate();
158
- await user_event.type(screen.getByLabelText(/bucket name/i), 'test-s3alias');
178
+ await user_event.type(screen.getByRole('textbox', {
179
+ name: /bucket name/i
180
+ }), 'test-s3alias');
159
181
  await user_event.tab();
160
182
  await waitFor(()=>{
161
183
  expect(screen.getByText(/bucket names must not end with/i)).toBeInTheDocument();
@@ -170,7 +192,9 @@ describe('BucketCreate', ()=>{
170
192
  ]
171
193
  }
172
194
  });
173
- await user_event.type(screen.getByLabelText(/bucket name/i), 'existing-bucket');
195
+ await user_event.type(screen.getByRole('textbox', {
196
+ name: /bucket name/i
197
+ }), 'existing-bucket');
174
198
  await user_event.tab();
175
199
  await waitFor(()=>{
176
200
  expect(screen.getByText(/a bucket with this name already exists/i)).toBeInTheDocument();
@@ -185,7 +209,9 @@ describe('BucketCreate', ()=>{
185
209
  '123-bucket-456'
186
210
  ];
187
211
  for (const name of validNames){
188
- const input = screen.getByLabelText(/bucket name/i);
212
+ const input = screen.getByRole('textbox', {
213
+ name: /bucket name/i
214
+ });
189
215
  await user_event.clear(input);
190
216
  await user_event.type(input, name);
191
217
  await user_event.tab();
@@ -469,7 +495,9 @@ describe('BucketCreate', ()=>{
469
495
  });
470
496
  renderBucketCreate();
471
497
  await fillBucketName('test-bucket');
472
- const encryptionCheckbox = screen.getByLabelText(/encryption/i);
498
+ const encryptionCheckbox = screen.getByRole('checkbox', {
499
+ name: /encryption/i
500
+ });
473
501
  await user_event.click(encryptionCheckbox);
474
502
  await submitForm();
475
503
  const createSuccessCallback = mockMutations.createBucket.mock.calls[0][1].onSuccess;
@@ -516,8 +544,12 @@ describe('BucketCreate', ()=>{
516
544
  });
517
545
  renderBucketCreate();
518
546
  await fillBucketName('test-bucket');
519
- await user_event.click(screen.getByLabelText(/versioning/i));
520
- await user_event.click(screen.getByLabelText(/encryption/i));
547
+ await user_event.click(screen.getByRole('checkbox', {
548
+ name: /versioning/i
549
+ }));
550
+ await user_event.click(screen.getByRole('checkbox', {
551
+ name: /encryption/i
552
+ }));
521
553
  await submitForm();
522
554
  const createSuccessCallback = mockMutations.createBucket.mock.calls[0][1].onSuccess;
523
555
  createSuccessCallback();
@@ -536,9 +568,15 @@ describe('BucketCreate', ()=>{
536
568
  });
537
569
  renderBucketCreate();
538
570
  await fillBucketName('test-bucket');
539
- await user_event.click(screen.getByLabelText(/object-lock/i));
540
- await user_event.click(screen.getByLabelText(/default retention/i));
541
- await user_event.click(screen.getByLabelText(/encryption/i));
571
+ await user_event.click(screen.getByRole('checkbox', {
572
+ name: /object-lock/i
573
+ }));
574
+ await user_event.click(screen.getByRole('checkbox', {
575
+ name: /default retention/i
576
+ }));
577
+ await user_event.click(screen.getByRole('checkbox', {
578
+ name: /encryption/i
579
+ }));
542
580
  await submitForm();
543
581
  const createSuccessCallback = mockMutations.createBucket.mock.calls[0][1].onSuccess;
544
582
  createSuccessCallback();
@@ -557,7 +595,9 @@ describe('BucketCreate', ()=>{
557
595
  });
558
596
  renderBucketCreate();
559
597
  await fillBucketName('test-bucket');
560
- await user_event.click(screen.getByLabelText(/encryption/i));
598
+ await user_event.click(screen.getByRole('checkbox', {
599
+ name: /encryption/i
600
+ }));
561
601
  await submitForm();
562
602
  const createSuccessCallback = mockMutations.createBucket.mock.calls[0][1].onSuccess;
563
603
  createSuccessCallback();
@@ -3,6 +3,7 @@ import { fireEvent, render, screen } from "@testing-library/react";
3
3
  import { MemoryRouter } from "react-router";
4
4
  import { createTestWrapper, mockOffsetSize } from "../../test/testUtils.js";
5
5
  import { BucketList } from "../buckets/BucketList.js";
6
+ import * as __rspack_external__hooks_index_js_a0d26731 from "../../hooks/index.js";
6
7
  import * as __rspack_external__contexts_DataBrowserUICustomizationContext_js_f267b01c from "../../contexts/DataBrowserUICustomizationContext.js";
7
8
  const mockUseDataBrowserUICustomization = (config = {})=>{
8
9
  jest.spyOn(__rspack_external__contexts_DataBrowserUICustomizationContext_js_f267b01c, 'useDataBrowserUICustomization').mockReturnValue(config);
@@ -37,15 +38,22 @@ describe('BucketList', ()=>{
37
38
  jest.clearAllMocks();
38
39
  mockOffsetSize(800, 600);
39
40
  mockUseDataBrowserUICustomization({});
41
+ jest.spyOn(__rspack_external__hooks_index_js_a0d26731, 'useBucketLocations').mockReturnValue(new Map());
40
42
  });
41
43
  it('shows a table with proper headers', ()=>{
42
44
  renderBucketList({
43
45
  buckets: mockBuckets
44
46
  });
45
47
  expect(screen.getByRole('grid')).toBeInTheDocument();
46
- expect(screen.getByText('Bucket Name')).toBeInTheDocument();
47
- expect(screen.getByText('Storage Location')).toBeInTheDocument();
48
- expect(screen.getByText('Created on')).toBeInTheDocument();
48
+ expect(screen.getByRole('columnheader', {
49
+ name: /bucket name/i
50
+ })).toBeInTheDocument();
51
+ expect(screen.getByRole('columnheader', {
52
+ name: /storage location/i
53
+ })).toBeInTheDocument();
54
+ expect(screen.getByRole('columnheader', {
55
+ name: /created on/i
56
+ })).toBeInTheDocument();
49
57
  });
50
58
  it('displays buckets with their names as clickable links', ()=>{
51
59
  const onNavigateToBucket = jest.fn();
@@ -76,7 +84,7 @@ describe('BucketList', ()=>{
76
84
  });
77
85
  const rows = screen.getAllByRole('row');
78
86
  fireEvent.click(rows[1]);
79
- expect(onBucketSelect).toHaveBeenCalledWith('test-bucket-3');
87
+ expect(onBucketSelect).toHaveBeenCalledWith('test-bucket-1');
80
88
  });
81
89
  it('shows selected bucket when selectedBucketName is provided', ()=>{
82
90
  renderBucketList({
@@ -107,14 +115,16 @@ describe('BucketList', ()=>{
107
115
  });
108
116
  const searchInput = screen.getByRole('searchbox');
109
117
  expect(searchInput).toBeInTheDocument();
110
- expect(searchInput).toHaveAttribute('placeholder', 'Example: Search');
118
+ expect(searchInput).toHaveAttribute('placeholder', 'Search');
111
119
  });
112
120
  it('handles empty buckets list', ()=>{
113
121
  renderBucketList({
114
122
  buckets: []
115
123
  });
116
124
  expect(screen.getByRole('grid')).toBeInTheDocument();
117
- expect(screen.getByText('Bucket Name')).toBeInTheDocument();
125
+ expect(screen.getByRole('columnheader', {
126
+ name: /bucket name/i
127
+ })).toBeInTheDocument();
118
128
  });
119
129
  it('handles buckets without names gracefully', ()=>{
120
130
  const bucketsWithMissingNames = [
@@ -205,6 +215,81 @@ describe('BucketList', ()=>{
205
215
  fireEvent.click(bucketName.closest('[role="row"]') || bucketName);
206
216
  expect(onBucketSelect).not.toHaveBeenCalled();
207
217
  });
218
+ it('sorts by creation date by default, not by bucket name', ()=>{
219
+ const bucketsWithReversedDateOrder = [
220
+ {
221
+ Name: 'alpha-bucket',
222
+ CreationDate: new Date('2024-03-01T00:00:00Z')
223
+ },
224
+ {
225
+ Name: 'beta-bucket',
226
+ CreationDate: new Date('2024-01-01T00:00:00Z')
227
+ },
228
+ {
229
+ Name: 'gamma-bucket',
230
+ CreationDate: new Date('2024-02-01T00:00:00Z')
231
+ }
232
+ ];
233
+ renderBucketList({
234
+ buckets: bucketsWithReversedDateOrder
235
+ });
236
+ const rows = screen.getAllByRole('row');
237
+ expect(rows[1]).toHaveTextContent('beta-bucket');
238
+ expect(rows[2]).toHaveTextContent('gamma-bucket');
239
+ expect(rows[3]).toHaveTextContent('alpha-bucket');
240
+ });
241
+ it('toggles to date descending when Created on header is clicked', ()=>{
242
+ const bucketsWithReversedDateOrder = [
243
+ {
244
+ Name: 'alpha-bucket',
245
+ CreationDate: new Date('2024-03-01T00:00:00Z')
246
+ },
247
+ {
248
+ Name: 'beta-bucket',
249
+ CreationDate: new Date('2024-01-01T00:00:00Z')
250
+ },
251
+ {
252
+ Name: 'gamma-bucket',
253
+ CreationDate: new Date('2024-02-01T00:00:00Z')
254
+ }
255
+ ];
256
+ renderBucketList({
257
+ buckets: bucketsWithReversedDateOrder
258
+ });
259
+ fireEvent.click(screen.getByRole('columnheader', {
260
+ name: /created on/i
261
+ }));
262
+ const rows = screen.getAllByRole('row');
263
+ expect(rows[1]).toHaveTextContent('alpha-bucket');
264
+ expect(rows[2]).toHaveTextContent('gamma-bucket');
265
+ expect(rows[3]).toHaveTextContent('beta-bucket');
266
+ });
267
+ it('sorts by storage location when location column header is clicked', ()=>{
268
+ jest.spyOn(__rspack_external__hooks_index_js_a0d26731, 'useBucketLocations').mockReturnValue(new Map([
269
+ [
270
+ 'test-bucket-1',
271
+ 'us-west-2'
272
+ ],
273
+ [
274
+ 'test-bucket-2',
275
+ 'eu-west-1'
276
+ ],
277
+ [
278
+ 'test-bucket-3',
279
+ 'ap-southeast-1'
280
+ ]
281
+ ]));
282
+ renderBucketList({
283
+ buckets: mockBuckets
284
+ });
285
+ fireEvent.click(screen.getByRole('columnheader', {
286
+ name: /storage location/i
287
+ }));
288
+ const rows = screen.getAllByRole('row');
289
+ expect(rows[1]).toHaveTextContent('test-bucket-3');
290
+ expect(rows[2]).toHaveTextContent('test-bucket-2');
291
+ expect(rows[3]).toHaveTextContent('test-bucket-1');
292
+ });
208
293
  describe('extraBucketListColumns support', ()=>{
209
294
  it('renders extra columns when extraBucketListColumns is configured', ()=>{
210
295
  const CustomColumn = ({ data })=>/*#__PURE__*/ jsxs("span", {
@@ -66,11 +66,18 @@ describe('BucketNotificationFormPage', ()=>{
66
66
  it('renders create notification form with all fields', ()=>{
67
67
  renderCreatePage();
68
68
  expect(screen.getByText('Create Bucket Notification')).toBeInTheDocument();
69
- expect(screen.getByLabelText(/rule name/i)).toBeInTheDocument();
70
- expect(screen.getByText('Events')).toBeInTheDocument();
71
- expect(screen.getByLabelText(/destination queue/i)).toBeInTheDocument();
72
- expect(screen.getByLabelText(/prefix/i)).toBeInTheDocument();
73
- expect(screen.getByLabelText(/suffix/i)).toBeInTheDocument();
69
+ expect(screen.getByRole('textbox', {
70
+ name: /rule name/i
71
+ })).toBeInTheDocument();
72
+ expect(screen.getByRole('textbox', {
73
+ name: /destination queue/i
74
+ })).toBeInTheDocument();
75
+ expect(screen.getByRole('textbox', {
76
+ name: /^prefix$/i
77
+ })).toBeInTheDocument();
78
+ expect(screen.getByRole('textbox', {
79
+ name: /^suffix$/i
80
+ })).toBeInTheDocument();
74
81
  });
75
82
  it('disables create button when form is pristine', ()=>{
76
83
  renderCreatePage();
@@ -80,7 +87,9 @@ describe('BucketNotificationFormPage', ()=>{
80
87
  });
81
88
  it('shows validation error when rule name is empty', async ()=>{
82
89
  renderCreatePage();
83
- const ruleNameInput = screen.getByLabelText(/rule name/i);
90
+ const ruleNameInput = screen.getByRole('textbox', {
91
+ name: /rule name/i
92
+ });
84
93
  await user_event.type(ruleNameInput, 'a');
85
94
  await user_event.clear(ruleNameInput);
86
95
  await waitFor(()=>{
@@ -104,7 +113,9 @@ describe('BucketNotificationFormPage', ()=>{
104
113
  status: 'success'
105
114
  });
106
115
  renderCreatePage();
107
- const ruleNameInput = screen.getByLabelText(/rule name/i);
116
+ const ruleNameInput = screen.getByRole('textbox', {
117
+ name: /rule name/i
118
+ });
108
119
  await user_event.type(ruleNameInput, 'existing-rule');
109
120
  await waitFor(()=>{
110
121
  expect(screen.getByText(/a rule with this name already exists/i)).toBeInTheDocument();
@@ -112,7 +123,9 @@ describe('BucketNotificationFormPage', ()=>{
112
123
  });
113
124
  it('shows validation error when queue ARN is invalid', async ()=>{
114
125
  renderCreatePage();
115
- const queueArnInput = screen.getByLabelText(/destination queue/i);
126
+ const queueArnInput = screen.getByRole('textbox', {
127
+ name: /destination queue/i
128
+ });
116
129
  await user_event.type(queueArnInput, 'invalid-arn');
117
130
  await waitFor(()=>{
118
131
  expect(screen.getByText(/must be a valid arn/i)).toBeInTheDocument();
@@ -120,8 +133,12 @@ describe('BucketNotificationFormPage', ()=>{
120
133
  });
121
134
  it('enables create button when form is valid', async ()=>{
122
135
  renderCreatePage();
123
- await user_event.type(screen.getByLabelText(/rule name/i), 'test-rule');
124
- await user_event.type(screen.getByLabelText(/destination queue/i), 'arn:aws:sqs:us-east-1:123:queue');
136
+ await user_event.type(screen.getByRole('textbox', {
137
+ name: /rule name/i
138
+ }), 'test-rule');
139
+ await user_event.type(screen.getByRole('textbox', {
140
+ name: /destination queue/i
141
+ }), 'arn:aws:sqs:us-east-1:123:queue');
125
142
  const checkbox = screen.getByRole('checkbox', {
126
143
  name: /s3:ObjectCreated:\*/i
127
144
  });
@@ -134,13 +151,21 @@ describe('BucketNotificationFormPage', ()=>{
134
151
  });
135
152
  it('creates notification with all fields and navigates on success', async ()=>{
136
153
  renderCreatePage();
137
- await user_event.type(screen.getByLabelText(/rule name/i), 'test-rule');
138
- await user_event.type(screen.getByLabelText(/destination queue/i), 'arn:aws:sqs:us-east-1:123:my-queue');
154
+ await user_event.type(screen.getByRole('textbox', {
155
+ name: /rule name/i
156
+ }), 'test-rule');
157
+ await user_event.type(screen.getByRole('textbox', {
158
+ name: /destination queue/i
159
+ }), 'arn:aws:sqs:us-east-1:123:my-queue');
139
160
  fireEvent.click(screen.getByRole('checkbox', {
140
161
  name: /s3:ObjectCreated:Put/i
141
162
  }));
142
- await user_event.type(screen.getByLabelText(/prefix/i), 'uploads/');
143
- await user_event.type(screen.getByLabelText(/suffix/i), '.jpg');
163
+ await user_event.type(screen.getByRole('textbox', {
164
+ name: /^prefix$/i
165
+ }), 'uploads/');
166
+ await user_event.type(screen.getByRole('textbox', {
167
+ name: /^suffix$/i
168
+ }), '.jpg');
144
169
  mockMutate.mockImplementation((_, options)=>{
145
170
  options?.onSuccess?.();
146
171
  });
@@ -263,9 +288,15 @@ describe('BucketNotificationFormPage', ()=>{
263
288
  it('pre-fills form with existing rule data', async ()=>{
264
289
  renderEditPage();
265
290
  await waitFor(()=>{
266
- expect(screen.getByLabelText(/destination queue/i)).toHaveValue('arn:aws:sqs:us-east-1:123:existing-queue');
267
- expect(screen.getByLabelText(/prefix/i)).toHaveValue('data/');
268
- expect(screen.getByLabelText(/suffix/i)).toHaveValue('.json');
291
+ expect(screen.getByRole('textbox', {
292
+ name: /destination queue/i
293
+ })).toHaveValue('arn:aws:sqs:us-east-1:123:existing-queue');
294
+ expect(screen.getByRole('textbox', {
295
+ name: /^prefix$/i
296
+ })).toHaveValue('data/');
297
+ expect(screen.getByRole('textbox', {
298
+ name: /^suffix$/i
299
+ })).toHaveValue('.json');
269
300
  });
270
301
  });
271
302
  it('shows Save button instead of Create in edit mode', ()=>{
@@ -280,9 +311,13 @@ describe('BucketNotificationFormPage', ()=>{
280
311
  it('updates notification and navigates on success', async ()=>{
281
312
  renderEditPage();
282
313
  await waitFor(()=>{
283
- expect(screen.getByLabelText(/destination queue/i)).toHaveValue('arn:aws:sqs:us-east-1:123:existing-queue');
314
+ expect(screen.getByRole('textbox', {
315
+ name: /destination queue/i
316
+ })).toHaveValue('arn:aws:sqs:us-east-1:123:existing-queue');
317
+ });
318
+ const queueInput = screen.getByRole('textbox', {
319
+ name: /destination queue/i
284
320
  });
285
- const queueInput = screen.getByLabelText(/destination queue/i);
286
321
  await user_event.clear(queueInput);
287
322
  await user_event.type(queueInput, 'arn:aws:sqs:us-east-1:123:updated-queue');
288
323
  mockMutate.mockImplementation((_, options)=>{