@scality/data-browser-library 1.0.0-preview.7 → 1.0.0-preview.9

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 (90) hide show
  1. package/dist/components/__tests__/BucketCreate.test.d.ts +1 -0
  2. package/dist/components/__tests__/BucketCreate.test.js +408 -0
  3. package/dist/components/__tests__/BucketLifecycleFormPage.test.d.ts +1 -0
  4. package/dist/components/__tests__/BucketLifecycleFormPage.test.js +618 -0
  5. package/dist/components/__tests__/BucketLifecycleList.test.d.ts +1 -0
  6. package/dist/components/__tests__/BucketLifecycleList.test.js +325 -0
  7. package/dist/components/__tests__/BucketList.test.js +190 -0
  8. package/dist/components/__tests__/BucketOverview.test.js +298 -8
  9. package/dist/components/__tests__/BucketReplicationFormPage.test.d.ts +1 -0
  10. package/dist/components/__tests__/BucketReplicationFormPage.test.js +1757 -0
  11. package/dist/components/__tests__/BucketReplicationList.test.d.ts +1 -0
  12. package/dist/components/__tests__/BucketReplicationList.test.js +344 -0
  13. package/dist/components/__tests__/DeleteBucketConfigRuleButton.test.d.ts +1 -0
  14. package/dist/components/__tests__/DeleteBucketConfigRuleButton.test.js +196 -0
  15. package/dist/components/__tests__/EmptyBucketButton.test.d.ts +1 -0
  16. package/dist/components/__tests__/EmptyBucketButton.test.js +302 -0
  17. package/dist/components/buckets/BucketCreate.d.ts +49 -0
  18. package/dist/components/buckets/BucketCreate.js +237 -0
  19. package/dist/components/buckets/BucketDetails.js +62 -10
  20. package/dist/components/buckets/BucketLifecycleFormPage.d.ts +15 -0
  21. package/dist/components/buckets/BucketLifecycleFormPage.js +1070 -0
  22. package/dist/components/buckets/BucketLifecycleList.d.ts +10 -0
  23. package/dist/components/buckets/BucketLifecycleList.js +270 -0
  24. package/dist/components/buckets/BucketList.d.ts +5 -2
  25. package/dist/components/buckets/BucketList.js +38 -28
  26. package/dist/components/buckets/BucketOverview.d.ts +65 -4
  27. package/dist/components/buckets/BucketOverview.js +261 -179
  28. package/dist/components/buckets/BucketPage.js +1 -1
  29. package/dist/components/buckets/BucketReplicationFormPage.d.ts +1 -0
  30. package/dist/components/buckets/BucketReplicationFormPage.js +834 -0
  31. package/dist/components/buckets/BucketReplicationList.d.ts +11 -0
  32. package/dist/components/buckets/BucketReplicationList.js +189 -0
  33. package/dist/components/buckets/DeleteBucketConfigRuleButton.d.ts +18 -0
  34. package/dist/components/buckets/DeleteBucketConfigRuleButton.js +53 -0
  35. package/dist/components/buckets/EmptyBucketButton.d.ts +5 -0
  36. package/dist/components/buckets/EmptyBucketButton.js +232 -0
  37. package/dist/components/buckets/EmptyBucketSummary.d.ts +9 -0
  38. package/dist/components/buckets/EmptyBucketSummary.js +60 -0
  39. package/dist/components/buckets/EmptyBucketSummaryList.d.ts +13 -0
  40. package/dist/components/buckets/EmptyBucketSummaryList.js +140 -0
  41. package/dist/components/buckets/notifications/BucketNotificationCreatePage.js +8 -8
  42. package/dist/components/index.d.ts +8 -1
  43. package/dist/components/index.js +9 -2
  44. package/dist/components/objects/ObjectLock/EditRetentionButton.d.ts +4 -0
  45. package/dist/components/objects/ObjectLock/EditRetentionButton.js +32 -0
  46. package/dist/components/objects/ObjectLock/ObjectLockRetentionSettings.d.ts +3 -0
  47. package/dist/components/objects/ObjectLock/ObjectLockRetentionSettings.js +211 -0
  48. package/dist/components/objects/ObjectLock/ObjectLockSettings.d.ts +9 -0
  49. package/dist/components/objects/ObjectLock/ObjectLockSettings.js +158 -0
  50. package/dist/components/objects/ObjectLock/ObjectLockSettingsUtils.d.ts +8 -0
  51. package/dist/components/objects/ObjectLock/ObjectLockSettingsUtils.js +39 -0
  52. package/dist/components/objects/ObjectLock/__tests__/EditRetentionButton.test.d.ts +1 -0
  53. package/dist/components/objects/ObjectLock/__tests__/EditRetentionButton.test.js +204 -0
  54. package/dist/components/objects/ObjectLock/__tests__/ObjectLockSettings.test.d.ts +1 -0
  55. package/dist/components/objects/ObjectLock/__tests__/ObjectLockSettings.test.js +374 -0
  56. package/dist/components/ui/ArrayFieldActions.d.ts +36 -0
  57. package/dist/components/ui/ArrayFieldActions.js +38 -0
  58. package/dist/components/ui/ConfirmDeleteRuleModal.d.ts +16 -0
  59. package/dist/components/ui/ConfirmDeleteRuleModal.js +43 -0
  60. package/dist/components/ui/FilterFormSection.d.ts +44 -0
  61. package/dist/components/ui/FilterFormSection.js +159 -0
  62. package/dist/config/factory.d.ts +13 -2
  63. package/dist/config/factory.js +9 -6
  64. package/dist/hooks/__tests__/useISVBucketDetection.test.d.ts +1 -0
  65. package/dist/hooks/__tests__/useISVBucketDetection.test.js +188 -0
  66. package/dist/hooks/factories/__tests__/useCreateS3QueryHook.test.js +44 -1
  67. package/dist/hooks/factories/useCreateS3QueryHook.js +22 -1
  68. package/dist/hooks/index.d.ts +4 -0
  69. package/dist/hooks/index.js +5 -1
  70. package/dist/hooks/useDeleteBucketConfigRule.d.ts +26 -0
  71. package/dist/hooks/useDeleteBucketConfigRule.js +46 -0
  72. package/dist/hooks/useEmptyBucket.d.ts +27 -0
  73. package/dist/hooks/useEmptyBucket.js +116 -0
  74. package/dist/hooks/useISVBucketDetection.d.ts +15 -0
  75. package/dist/hooks/useISVBucketDetection.js +27 -0
  76. package/dist/hooks/useTableRowSelection.d.ts +9 -0
  77. package/dist/hooks/useTableRowSelection.js +45 -0
  78. package/dist/test/setup.js +8 -0
  79. package/dist/test/testUtils.d.ts +99 -17
  80. package/dist/test/testUtils.js +64 -16
  81. package/dist/test/utils/errorHandling.test.js +39 -1
  82. package/dist/utils/constants.d.ts +12 -0
  83. package/dist/utils/constants.js +9 -0
  84. package/dist/utils/errorHandling.d.ts +9 -0
  85. package/dist/utils/errorHandling.js +6 -1
  86. package/dist/utils/index.d.ts +2 -0
  87. package/dist/utils/index.js +2 -0
  88. package/dist/utils/s3RuleUtils.d.ts +53 -0
  89. package/dist/utils/s3RuleUtils.js +101 -0
  90. package/package.json +1 -1
@@ -0,0 +1,344 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+ import { MemoryRouter } from "react-router";
4
+ import { createTestWrapper, mockOffsetSize } from "../../test/testUtils.js";
5
+ import { BucketReplicationList } from "../buckets/BucketReplicationList.js";
6
+ const renderBucketReplicationList = (props = {})=>{
7
+ const Wrapper = createTestWrapper();
8
+ return render(/*#__PURE__*/ jsx(MemoryRouter, {
9
+ children: /*#__PURE__*/ jsx(Wrapper, {
10
+ children: /*#__PURE__*/ jsx(BucketReplicationList, {
11
+ bucketName: "test-bucket",
12
+ replicationRules: [],
13
+ replicationRole: "arn:aws:iam::123456789012:role/replication-role",
14
+ ...props
15
+ })
16
+ })
17
+ }));
18
+ };
19
+ const mockReplicationRules = [
20
+ {
21
+ ID: "rule-1",
22
+ Status: "Enabled",
23
+ Priority: 1,
24
+ Destination: {
25
+ Bucket: "arn:aws:s3:::destination-bucket-1",
26
+ StorageClass: "STANDARD"
27
+ }
28
+ },
29
+ {
30
+ ID: "rule-2",
31
+ Status: "Disabled",
32
+ Priority: 2,
33
+ Destination: {
34
+ Bucket: "arn:aws:s3:::123456789012:backup-bucket",
35
+ StorageClass: "GLACIER",
36
+ Account: "123456789012"
37
+ }
38
+ },
39
+ {
40
+ ID: "rule-3",
41
+ Status: "Enabled",
42
+ Priority: 100,
43
+ Destination: {
44
+ Bucket: "arn:aws:s3:::logs-backup",
45
+ StorageClass: "ONEZONE_IA"
46
+ }
47
+ }
48
+ ];
49
+ const mockRuleWithoutPriority = {
50
+ ID: "no-priority-rule",
51
+ Status: "Enabled",
52
+ Destination: {
53
+ Bucket: "arn:aws:s3:::simple-backup"
54
+ }
55
+ };
56
+ const mockRuleWithoutStorageClass = {
57
+ ID: "no-storage-class-rule",
58
+ Status: "Enabled",
59
+ Priority: 5,
60
+ Destination: {
61
+ Bucket: "arn:aws:s3:::mirror-bucket"
62
+ }
63
+ };
64
+ describe("BucketReplicationList", ()=>{
65
+ beforeEach(()=>{
66
+ jest.clearAllMocks();
67
+ mockOffsetSize(800, 600);
68
+ });
69
+ it("shows a table with proper headers", ()=>{
70
+ renderBucketReplicationList({
71
+ replicationRules: mockReplicationRules
72
+ });
73
+ expect(screen.getByRole("grid")).toBeInTheDocument();
74
+ expect(screen.getByText("Rule ID")).toBeInTheDocument();
75
+ expect(screen.getByText("Status")).toBeInTheDocument();
76
+ expect(screen.getByText("Destination")).toBeInTheDocument();
77
+ expect(screen.getByText("Priority")).toBeInTheDocument();
78
+ });
79
+ it("displays replication rules with their IDs", ()=>{
80
+ renderBucketReplicationList({
81
+ replicationRules: mockReplicationRules
82
+ });
83
+ expect(screen.getByText("rule-1")).toBeInTheDocument();
84
+ expect(screen.getByText("rule-2")).toBeInTheDocument();
85
+ expect(screen.getByText("rule-3")).toBeInTheDocument();
86
+ });
87
+ it("displays status as Active for Enabled rules", ()=>{
88
+ renderBucketReplicationList({
89
+ replicationRules: mockReplicationRules
90
+ });
91
+ const activeStatuses = screen.getAllByText("Active");
92
+ expect(activeStatuses.length).toBe(2);
93
+ });
94
+ it("displays status as Inactive for Disabled rules", ()=>{
95
+ renderBucketReplicationList({
96
+ replicationRules: mockReplicationRules
97
+ });
98
+ expect(screen.getByText("Inactive")).toBeInTheDocument();
99
+ });
100
+ it("displays replication action with bucket name", ()=>{
101
+ renderBucketReplicationList({
102
+ replicationRules: mockReplicationRules
103
+ });
104
+ expect(screen.getByText(/Replicate to destination-bucket-1/)).toBeInTheDocument();
105
+ expect(screen.getByText(/Replicate to backup-bucket/)).toBeInTheDocument();
106
+ expect(screen.getByText(/Replicate to logs-backup/)).toBeInTheDocument();
107
+ });
108
+ it("displays storage class in actions column", ()=>{
109
+ renderBucketReplicationList({
110
+ replicationRules: mockReplicationRules
111
+ });
112
+ const actionTexts = screen.getAllByText(/Replicate to/);
113
+ expect(actionTexts.length).toBeGreaterThanOrEqual(3);
114
+ expect(screen.getByText(/STANDARD/, {
115
+ exact: false
116
+ })).toBeInTheDocument();
117
+ expect(screen.getByText(/GLACIER/, {
118
+ exact: false
119
+ })).toBeInTheDocument();
120
+ expect(screen.getByText(/ONEZONE_IA/, {
121
+ exact: false
122
+ })).toBeInTheDocument();
123
+ });
124
+ it("displays shortened account ID for cross-account replication", ()=>{
125
+ renderBucketReplicationList({
126
+ replicationRules: mockReplicationRules
127
+ });
128
+ expect(screen.getByText(/account: 123\.\.\.012/)).toBeInTheDocument();
129
+ });
130
+ it("displays priority values correctly", ()=>{
131
+ renderBucketReplicationList({
132
+ replicationRules: mockReplicationRules
133
+ });
134
+ expect(screen.getByText("1")).toBeInTheDocument();
135
+ expect(screen.getByText("2")).toBeInTheDocument();
136
+ expect(screen.getByText("100")).toBeInTheDocument();
137
+ });
138
+ it("displays 'None' for rules without priority", ()=>{
139
+ renderBucketReplicationList({
140
+ replicationRules: [
141
+ mockRuleWithoutPriority
142
+ ]
143
+ });
144
+ expect(screen.getByText("None")).toBeInTheDocument();
145
+ });
146
+ it("displays action without storage class when not provided", ()=>{
147
+ renderBucketReplicationList({
148
+ replicationRules: [
149
+ mockRuleWithoutStorageClass
150
+ ]
151
+ });
152
+ const actionText = screen.getByText(/Replicate to mirror-bucket/);
153
+ expect(actionText).toBeInTheDocument();
154
+ expect(actionText.textContent).toBe("Replicate to mirror-bucket");
155
+ });
156
+ it("renders create rule button as enabled", ()=>{
157
+ renderBucketReplicationList({
158
+ replicationRules: mockReplicationRules
159
+ });
160
+ const createButton = screen.getByRole("button", {
161
+ name: /create rule/i
162
+ });
163
+ expect(createButton).toBeInTheDocument();
164
+ expect(createButton).toBeEnabled();
165
+ });
166
+ it("calls create rule callback when button is clicked", ()=>{
167
+ const onCreateRule = jest.fn();
168
+ renderBucketReplicationList({
169
+ replicationRules: mockReplicationRules,
170
+ onCreateRule
171
+ });
172
+ const createButton = screen.getByRole("button", {
173
+ name: /create rule/i
174
+ });
175
+ expect(createButton).toBeEnabled();
176
+ fireEvent.click(createButton);
177
+ expect(onCreateRule).toHaveBeenCalled();
178
+ });
179
+ it("handles edit button click", ()=>{
180
+ const onEditRule = jest.fn();
181
+ renderBucketReplicationList({
182
+ replicationRules: mockReplicationRules,
183
+ onEditRule
184
+ });
185
+ const editButtons = screen.getAllByRole("button", {
186
+ name: /edit rule/i
187
+ });
188
+ expect(editButtons.length).toBeGreaterThanOrEqual(3);
189
+ fireEvent.click(editButtons[0]);
190
+ expect(onEditRule).toHaveBeenCalledWith(expect.any(String));
191
+ });
192
+ it("renders delete buttons for each rule", ()=>{
193
+ renderBucketReplicationList({
194
+ replicationRules: mockReplicationRules
195
+ });
196
+ const deleteButtons = screen.getAllByRole("button", {
197
+ name: /delete rule/i
198
+ });
199
+ expect(deleteButtons.length).toBeGreaterThanOrEqual(3);
200
+ });
201
+ it("handles empty replication rules list", ()=>{
202
+ renderBucketReplicationList({
203
+ replicationRules: []
204
+ });
205
+ expect(screen.getByRole("grid")).toBeInTheDocument();
206
+ expect(screen.getByText(/no replication rules found/i)).toBeInTheDocument();
207
+ });
208
+ it("handles replication rules without IDs", ()=>{
209
+ const ruleWithoutId = {
210
+ Status: "Enabled",
211
+ Priority: 1,
212
+ Destination: {
213
+ Bucket: "arn:aws:s3:::test-bucket"
214
+ }
215
+ };
216
+ renderBucketReplicationList({
217
+ replicationRules: [
218
+ ruleWithoutId
219
+ ]
220
+ });
221
+ expect(screen.getByText("-")).toBeInTheDocument();
222
+ });
223
+ it("shows loading state when replicationStatus is loading", ()=>{
224
+ renderBucketReplicationList({
225
+ replicationRules: [],
226
+ replicationStatus: "loading"
227
+ });
228
+ expect(screen.getByRole("grid")).toBeInTheDocument();
229
+ expect(screen.queryByText("rule-1")).not.toBeInTheDocument();
230
+ });
231
+ it("shows error state when replicationStatus is error", ()=>{
232
+ renderBucketReplicationList({
233
+ replicationRules: [],
234
+ replicationStatus: "error"
235
+ });
236
+ expect(screen.getByRole("grid")).toBeInTheDocument();
237
+ expect(screen.queryByText("rule-1")).not.toBeInTheDocument();
238
+ });
239
+ it("shows data when replicationStatus is success", ()=>{
240
+ renderBucketReplicationList({
241
+ replicationRules: mockReplicationRules,
242
+ replicationStatus: "success"
243
+ });
244
+ expect(screen.getByRole("grid")).toBeInTheDocument();
245
+ expect(screen.getByText("rule-1")).toBeInTheDocument();
246
+ expect(screen.getByText("rule-2")).toBeInTheDocument();
247
+ });
248
+ it("works when no callbacks are provided", ()=>{
249
+ expect(()=>{
250
+ renderBucketReplicationList({
251
+ replicationRules: mockReplicationRules
252
+ });
253
+ }).not.toThrow();
254
+ });
255
+ it("renders rows that can be selected", ()=>{
256
+ renderBucketReplicationList({
257
+ replicationRules: mockReplicationRules
258
+ });
259
+ const rows = screen.getAllByRole("row");
260
+ expect(rows.length).toBeGreaterThan(3);
261
+ const dataRows = rows.slice(1);
262
+ dataRows.forEach((row)=>{
263
+ expect(row).toHaveAttribute("aria-selected");
264
+ });
265
+ });
266
+ it("displays complex cross-account replication with all details", ()=>{
267
+ const complexRule = {
268
+ ID: "complex-rule",
269
+ Status: "Enabled",
270
+ Priority: 25,
271
+ Destination: {
272
+ Bucket: "arn:aws:s3:::999888777666555:shared-bucket",
273
+ Account: "999888777666555",
274
+ StorageClass: "GLACIER_IR"
275
+ }
276
+ };
277
+ renderBucketReplicationList({
278
+ replicationRules: [
279
+ complexRule
280
+ ]
281
+ });
282
+ expect(screen.getByText(/account: 999\.\.\.555/)).toBeInTheDocument();
283
+ expect(screen.getByText(/GLACIER_IR/)).toBeInTheDocument();
284
+ expect(screen.getByText(/Replicate to shared-bucket \(account: 999\.\.\.555, GLACIER_IR\)/)).toBeInTheDocument();
285
+ });
286
+ it("displays action text without account ID for same-account replication", ()=>{
287
+ renderBucketReplicationList({
288
+ replicationRules: [
289
+ mockReplicationRules[0]
290
+ ]
291
+ });
292
+ const actionText = screen.getByText(/Replicate to destination-bucket-1/);
293
+ expect(actionText.textContent).not.toContain("account:");
294
+ });
295
+ it("renders with custom bucket name and replication role", ()=>{
296
+ renderBucketReplicationList({
297
+ replicationRules: mockReplicationRules,
298
+ bucketName: "custom-bucket",
299
+ replicationRole: "arn:aws:iam::999888777666:role/custom-role"
300
+ });
301
+ expect(screen.getByText("rule-1")).toBeInTheDocument();
302
+ expect(screen.getByText("rule-2")).toBeInTheDocument();
303
+ expect(screen.getByText("rule-3")).toBeInTheDocument();
304
+ });
305
+ it("sorts rules by priority from low to high by default", ()=>{
306
+ const unsortedRules = [
307
+ {
308
+ ID: "high-priority-rule",
309
+ Status: "Enabled",
310
+ Priority: 100,
311
+ Destination: {
312
+ Bucket: "arn:aws:s3:::bucket-high"
313
+ }
314
+ },
315
+ {
316
+ ID: "low-priority-rule",
317
+ Status: "Enabled",
318
+ Priority: 1,
319
+ Destination: {
320
+ Bucket: "arn:aws:s3:::bucket-low"
321
+ }
322
+ },
323
+ {
324
+ ID: "mid-priority-rule",
325
+ Status: "Enabled",
326
+ Priority: 50,
327
+ Destination: {
328
+ Bucket: "arn:aws:s3:::bucket-mid"
329
+ }
330
+ }
331
+ ];
332
+ renderBucketReplicationList({
333
+ replicationRules: unsortedRules
334
+ });
335
+ const rows = screen.getAllByRole("row");
336
+ const dataRows = rows.slice(1);
337
+ expect(dataRows[0]).toHaveTextContent("low-priority-rule");
338
+ expect(dataRows[0]).toHaveTextContent("1");
339
+ expect(dataRows[1]).toHaveTextContent("mid-priority-rule");
340
+ expect(dataRows[1]).toHaveTextContent("50");
341
+ expect(dataRows[2]).toHaveTextContent("high-priority-rule");
342
+ expect(dataRows[2]).toHaveTextContent("100");
343
+ });
344
+ });
@@ -0,0 +1,196 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3
+ import { MemoryRouter } from "react-router-dom";
4
+ import { createTestWrapper } from "../../test/testUtils.js";
5
+ import { DeleteBucketConfigRuleButton } from "../buckets/DeleteBucketConfigRuleButton.js";
6
+ const mockLifecycleRules = [
7
+ {
8
+ ID: "rule-1",
9
+ Status: "Enabled",
10
+ Expiration: {
11
+ Days: 30
12
+ }
13
+ },
14
+ {
15
+ ID: "rule-2",
16
+ Status: "Disabled",
17
+ NoncurrentVersionExpiration: {
18
+ NoncurrentDays: 90
19
+ }
20
+ }
21
+ ];
22
+ describe("DeleteBucketConfigRuleButton", ()=>{
23
+ let mockUpdate;
24
+ let mockDelete;
25
+ const renderButton = (props = {})=>{
26
+ const defaultProps = {
27
+ bucketName: "test-bucket",
28
+ ruleId: "rule-1",
29
+ rules: mockLifecycleRules,
30
+ ruleType: "lifecycle",
31
+ updateMutation: {
32
+ mutate: mockUpdate,
33
+ status: "idle"
34
+ },
35
+ deleteMutation: {
36
+ mutate: mockDelete,
37
+ status: "idle"
38
+ },
39
+ buildUpdateInput: (remainingRules)=>({
40
+ LifecycleConfiguration: {
41
+ Rules: remainingRules
42
+ }
43
+ }),
44
+ invalidationKeys: [
45
+ "GetBucketLifecycle"
46
+ ],
47
+ successMessage: "Rule deleted successfully",
48
+ errorMessage: "Failed to delete rule"
49
+ };
50
+ const Wrapper = createTestWrapper();
51
+ return render(/*#__PURE__*/ jsx(MemoryRouter, {
52
+ children: /*#__PURE__*/ jsx(Wrapper, {
53
+ children: /*#__PURE__*/ jsx(DeleteBucketConfigRuleButton, {
54
+ ...defaultProps,
55
+ ...props
56
+ })
57
+ })
58
+ }));
59
+ };
60
+ beforeEach(()=>{
61
+ jest.clearAllMocks();
62
+ mockUpdate = jest.fn();
63
+ mockDelete = jest.fn();
64
+ });
65
+ it("renders delete button", ()=>{
66
+ renderButton();
67
+ const deleteButton = screen.getByRole("button", {
68
+ name: /delete rule/i
69
+ });
70
+ expect(deleteButton).toBeInTheDocument();
71
+ });
72
+ it("opens confirmation modal when clicked", ()=>{
73
+ renderButton();
74
+ const deleteButton = screen.getByRole("button", {
75
+ name: /delete rule/i
76
+ });
77
+ fireEvent.click(deleteButton);
78
+ expect(screen.getByText(/are you sure you want to delete the lifecycle rule/i)).toBeInTheDocument();
79
+ expect(screen.getByText(/rule-1/)).toBeInTheDocument();
80
+ });
81
+ it("closes modal when cancel is clicked", ()=>{
82
+ renderButton();
83
+ fireEvent.click(screen.getByRole("button", {
84
+ name: /delete rule/i
85
+ }));
86
+ expect(screen.getByText(/are you sure/i)).toBeInTheDocument();
87
+ fireEvent.click(screen.getByRole("button", {
88
+ name: /cancel/i
89
+ }));
90
+ expect(screen.queryByText(/are you sure/i)).not.toBeInTheDocument();
91
+ });
92
+ it("calls update when deleting a rule with remaining rules", async ()=>{
93
+ renderButton();
94
+ fireEvent.click(screen.getByRole("button", {
95
+ name: /delete rule/i
96
+ }));
97
+ fireEvent.click(screen.getByRole("button", {
98
+ name: /^delete$/i
99
+ }));
100
+ await waitFor(()=>{
101
+ expect(mockUpdate).toHaveBeenCalled();
102
+ const callArgs = mockUpdate.mock.calls[0][0];
103
+ expect(callArgs.Bucket).toBe("test-bucket");
104
+ expect(callArgs.LifecycleConfiguration.Rules).toHaveLength(1);
105
+ expect(callArgs.LifecycleConfiguration.Rules[0].ID).toBe("rule-2");
106
+ });
107
+ });
108
+ it("calls delete when deleting the last rule", async ()=>{
109
+ renderButton({
110
+ rules: [
111
+ mockLifecycleRules[0]
112
+ ]
113
+ });
114
+ fireEvent.click(screen.getByRole("button", {
115
+ name: /delete rule/i
116
+ }));
117
+ fireEvent.click(screen.getByRole("button", {
118
+ name: /^delete$/i
119
+ }));
120
+ await waitFor(()=>{
121
+ expect(mockDelete).toHaveBeenCalledWith({
122
+ Bucket: "test-bucket"
123
+ }, expect.any(Object));
124
+ });
125
+ });
126
+ it("closes modal after successful deletion", async ()=>{
127
+ mockUpdate.mockImplementation((_, options)=>{
128
+ options.onSuccess();
129
+ });
130
+ renderButton();
131
+ fireEvent.click(screen.getByRole("button", {
132
+ name: /delete rule/i
133
+ }));
134
+ fireEvent.click(screen.getByRole("button", {
135
+ name: /^delete$/i
136
+ }));
137
+ await waitFor(()=>{
138
+ expect(screen.queryByText(/are you sure/i)).not.toBeInTheDocument();
139
+ });
140
+ });
141
+ it("closes modal after failed deletion", async ()=>{
142
+ const consoleError = jest.spyOn(console, "error").mockImplementation(()=>{});
143
+ mockUpdate.mockImplementation((_, options)=>{
144
+ options.onError(new Error("Delete failed"));
145
+ });
146
+ renderButton();
147
+ fireEvent.click(screen.getByRole("button", {
148
+ name: /delete rule/i
149
+ }));
150
+ fireEvent.click(screen.getByRole("button", {
151
+ name: /^delete$/i
152
+ }));
153
+ await waitFor(()=>{
154
+ expect(mockUpdate).toHaveBeenCalled();
155
+ });
156
+ await waitFor(()=>{
157
+ expect(screen.queryByText(/are you sure/i)).not.toBeInTheDocument();
158
+ });
159
+ consoleError.mockRestore();
160
+ });
161
+ it("disables buttons while deletion is in progress", ()=>{
162
+ renderButton({
163
+ updateMutation: {
164
+ mutate: mockUpdate,
165
+ status: "pending"
166
+ }
167
+ });
168
+ fireEvent.click(screen.getByRole("button", {
169
+ name: /delete rule/i
170
+ }));
171
+ const cancelButton = screen.getByRole("button", {
172
+ name: /cancel/i
173
+ });
174
+ const confirmButton = screen.getByRole("button", {
175
+ name: /^delete$/i
176
+ });
177
+ expect(cancelButton).toBeDisabled();
178
+ expect(confirmButton).toBeDisabled();
179
+ });
180
+ it("stops event propagation when delete button is clicked", ()=>{
181
+ renderButton();
182
+ const deleteButton = screen.getByRole("button", {
183
+ name: /delete rule/i
184
+ });
185
+ const mockStopPropagation = jest.fn();
186
+ const clickEvent = new MouseEvent("click", {
187
+ bubbles: true
188
+ });
189
+ Object.defineProperty(clickEvent, "stopPropagation", {
190
+ value: mockStopPropagation,
191
+ writable: true
192
+ });
193
+ deleteButton.dispatchEvent(clickEvent);
194
+ expect(mockStopPropagation).toHaveBeenCalled();
195
+ });
196
+ });