@clef-sh/ui 0.1.14 → 0.1.15-beta.97
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.
- package/dist/client/assets/index-rBYybJbt.js +26 -0
- package/dist/client/index.html +1 -1
- package/dist/client-lib/components/Sidebar.d.ts +1 -1
- package/dist/client-lib/components/Sidebar.d.ts.map +1 -1
- package/dist/server/api.d.ts.map +1 -1
- package/dist/server/api.js +275 -87
- package/dist/server/api.js.map +1 -1
- package/package.json +1 -1
- package/src/client/App.tsx +8 -0
- package/src/client/components/Sidebar.tsx +15 -1
- package/src/client/screens/ManifestScreen.test.tsx +394 -0
- package/src/client/screens/ManifestScreen.tsx +977 -0
- package/src/client/screens/MatrixView.tsx +10 -1
- package/src/client/screens/NamespaceEditor.tsx +13 -3
- package/src/client/screens/ResetScreen.test.tsx +397 -0
- package/src/client/screens/ResetScreen.tsx +614 -0
- package/dist/client/assets/index-CVpAmirt.js +0 -26
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
|
|
3
|
+
import "@testing-library/jest-dom";
|
|
4
|
+
import { ManifestScreen } from "./ManifestScreen";
|
|
5
|
+
import type { ClefManifest } from "@clef-sh/core";
|
|
6
|
+
|
|
7
|
+
jest.mock("../api", () => ({
|
|
8
|
+
apiFetch: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
12
|
+
const { apiFetch } = require("../api") as { apiFetch: jest.Mock };
|
|
13
|
+
|
|
14
|
+
const baseManifest: ClefManifest = {
|
|
15
|
+
version: 1,
|
|
16
|
+
environments: [
|
|
17
|
+
{ name: "dev", description: "Development" },
|
|
18
|
+
{ name: "production", description: "Production", protected: true },
|
|
19
|
+
],
|
|
20
|
+
namespaces: [
|
|
21
|
+
{ name: "payments", description: "Payment secrets" },
|
|
22
|
+
{ name: "auth", description: "Auth secrets" },
|
|
23
|
+
],
|
|
24
|
+
sops: { default_backend: "age" },
|
|
25
|
+
file_pattern: "{namespace}/{environment}.enc.yaml",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const reloadManifest = jest.fn();
|
|
29
|
+
|
|
30
|
+
function mockOk(body: unknown = {}): { ok: true; json: () => Promise<unknown>; status: number } {
|
|
31
|
+
return { ok: true, status: 200, json: () => Promise.resolve(body) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function mockErr(
|
|
35
|
+
status: number,
|
|
36
|
+
body: { error: string; code?: string },
|
|
37
|
+
): { ok: false; status: number; json: () => Promise<unknown> } {
|
|
38
|
+
return { ok: false, status, json: () => Promise.resolve(body) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
jest.clearAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("ManifestScreen — list rendering", () => {
|
|
46
|
+
it("renders both namespaces and environments from the manifest", () => {
|
|
47
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
48
|
+
|
|
49
|
+
// Section headers
|
|
50
|
+
expect(screen.getByText("Namespaces")).toBeInTheDocument();
|
|
51
|
+
expect(screen.getByText("Environments")).toBeInTheDocument();
|
|
52
|
+
|
|
53
|
+
// Each namespace shows up as a row
|
|
54
|
+
expect(screen.getByTestId("namespace-row-payments")).toBeInTheDocument();
|
|
55
|
+
expect(screen.getByTestId("namespace-row-auth")).toBeInTheDocument();
|
|
56
|
+
|
|
57
|
+
// Each environment shows up as a row
|
|
58
|
+
expect(screen.getByTestId("environment-row-dev")).toBeInTheDocument();
|
|
59
|
+
expect(screen.getByTestId("environment-row-production")).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("shows the protected badge for protected environments", () => {
|
|
63
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
64
|
+
|
|
65
|
+
const prodRow = screen.getByTestId("environment-row-production");
|
|
66
|
+
expect(prodRow).toHaveTextContent("protected");
|
|
67
|
+
const devRow = screen.getByTestId("environment-row-dev");
|
|
68
|
+
expect(devRow).not.toHaveTextContent("protected");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("shows the schema badge for namespaces with a schema set", () => {
|
|
72
|
+
const manifestWithSchema: ClefManifest = {
|
|
73
|
+
...baseManifest,
|
|
74
|
+
namespaces: [{ name: "payments", description: "Payments", schema: "schemas/payments.yaml" }],
|
|
75
|
+
};
|
|
76
|
+
render(<ManifestScreen manifest={manifestWithSchema} reloadManifest={reloadManifest} />);
|
|
77
|
+
expect(screen.getByTestId("namespace-row-payments")).toHaveTextContent("schemas/payments.yaml");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("renders an empty-state message when there are no namespaces", () => {
|
|
81
|
+
const empty: ClefManifest = { ...baseManifest, namespaces: [] };
|
|
82
|
+
render(<ManifestScreen manifest={empty} reloadManifest={reloadManifest} />);
|
|
83
|
+
expect(screen.getByText("No namespaces declared yet.")).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("ManifestScreen — add namespace flow", () => {
|
|
88
|
+
it("opens the modal when '+ Namespace' is clicked", () => {
|
|
89
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
90
|
+
fireEvent.click(screen.getByTestId("add-namespace-btn"));
|
|
91
|
+
expect(screen.getByTestId("namespace-name-input")).toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("submits and reloads the manifest on success", async () => {
|
|
95
|
+
apiFetch.mockResolvedValueOnce(mockOk({ name: "billing", description: "" }));
|
|
96
|
+
|
|
97
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
98
|
+
fireEvent.click(screen.getByTestId("add-namespace-btn"));
|
|
99
|
+
fireEvent.change(screen.getByTestId("namespace-name-input"), {
|
|
100
|
+
target: { value: "billing" },
|
|
101
|
+
});
|
|
102
|
+
await act(async () => {
|
|
103
|
+
fireEvent.click(screen.getByTestId("namespace-add-submit"));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(apiFetch).toHaveBeenCalledWith(
|
|
107
|
+
"/api/namespaces",
|
|
108
|
+
expect.objectContaining({
|
|
109
|
+
method: "POST",
|
|
110
|
+
body: expect.stringContaining('"name":"billing"'),
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
expect(reloadManifest).toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("disables submit on duplicate name and shows local error", () => {
|
|
117
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
118
|
+
fireEvent.click(screen.getByTestId("add-namespace-btn"));
|
|
119
|
+
fireEvent.change(screen.getByTestId("namespace-name-input"), {
|
|
120
|
+
target: { value: "payments" },
|
|
121
|
+
});
|
|
122
|
+
expect(screen.getByText(/already exists/)).toBeInTheDocument();
|
|
123
|
+
expect(screen.getByTestId("namespace-add-submit")).toBeDisabled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("disables submit on invalid identifier", () => {
|
|
127
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
128
|
+
fireEvent.click(screen.getByTestId("add-namespace-btn"));
|
|
129
|
+
fireEvent.change(screen.getByTestId("namespace-name-input"), {
|
|
130
|
+
target: { value: "has spaces" },
|
|
131
|
+
});
|
|
132
|
+
expect(screen.getByText(/letters, numbers/)).toBeInTheDocument();
|
|
133
|
+
expect(screen.getByTestId("namespace-add-submit")).toBeDisabled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("surfaces server error when API call fails", async () => {
|
|
137
|
+
apiFetch.mockResolvedValueOnce(mockErr(409, { error: "Namespace 'billing' already exists." }));
|
|
138
|
+
|
|
139
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
140
|
+
fireEvent.click(screen.getByTestId("add-namespace-btn"));
|
|
141
|
+
fireEvent.change(screen.getByTestId("namespace-name-input"), {
|
|
142
|
+
target: { value: "billing" },
|
|
143
|
+
});
|
|
144
|
+
await act(async () => {
|
|
145
|
+
fireEvent.click(screen.getByTestId("namespace-add-submit"));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(await screen.findByTestId("manifest-modal-error")).toHaveTextContent("already exists");
|
|
149
|
+
expect(reloadManifest).not.toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("ManifestScreen — edit namespace flow", () => {
|
|
154
|
+
it("opens the edit modal pre-filled with the current values", () => {
|
|
155
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
156
|
+
fireEvent.click(screen.getByTestId("namespace-row-payments-edit"));
|
|
157
|
+
|
|
158
|
+
const renameInput = screen.getByTestId("namespace-rename-input") as HTMLInputElement;
|
|
159
|
+
expect(renameInput.value).toBe("payments");
|
|
160
|
+
const descInput = screen.getByTestId("namespace-description-input") as HTMLInputElement;
|
|
161
|
+
expect(descInput.value).toBe("Payment secrets");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("submits a rename with the new name and previousName", async () => {
|
|
165
|
+
apiFetch.mockResolvedValueOnce(mockOk({ name: "billing", previousName: "payments" }));
|
|
166
|
+
|
|
167
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
168
|
+
fireEvent.click(screen.getByTestId("namespace-row-payments-edit"));
|
|
169
|
+
fireEvent.change(screen.getByTestId("namespace-rename-input"), {
|
|
170
|
+
target: { value: "billing" },
|
|
171
|
+
});
|
|
172
|
+
await act(async () => {
|
|
173
|
+
fireEvent.click(screen.getByTestId("namespace-edit-submit"));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(apiFetch).toHaveBeenCalledWith(
|
|
177
|
+
"/api/namespaces/payments",
|
|
178
|
+
expect.objectContaining({
|
|
179
|
+
method: "PATCH",
|
|
180
|
+
body: expect.stringContaining('"rename":"billing"'),
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
expect(reloadManifest).toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("disables submit when nothing has changed", () => {
|
|
187
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
188
|
+
fireEvent.click(screen.getByTestId("namespace-row-payments-edit"));
|
|
189
|
+
expect(screen.getByTestId("namespace-edit-submit")).toBeDisabled();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("disables submit when rename target collides with another namespace", () => {
|
|
193
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
194
|
+
fireEvent.click(screen.getByTestId("namespace-row-payments-edit"));
|
|
195
|
+
fireEvent.change(screen.getByTestId("namespace-rename-input"), {
|
|
196
|
+
target: { value: "auth" },
|
|
197
|
+
});
|
|
198
|
+
expect(screen.getByText(/already exists/)).toBeInTheDocument();
|
|
199
|
+
expect(screen.getByTestId("namespace-edit-submit")).toBeDisabled();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("ManifestScreen — remove namespace flow", () => {
|
|
204
|
+
it("opens the confirm modal and disables submit until name is typed", () => {
|
|
205
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
206
|
+
fireEvent.click(screen.getByTestId("namespace-row-payments-delete"));
|
|
207
|
+
|
|
208
|
+
// Modal H3 title — match exactly to disambiguate from the button text
|
|
209
|
+
expect(screen.getByRole("heading", { name: "Delete namespace" })).toBeInTheDocument();
|
|
210
|
+
expect(screen.getByTestId("namespace-remove-submit")).toBeDisabled();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("enables submit only when the typed name matches", () => {
|
|
214
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
215
|
+
fireEvent.click(screen.getByTestId("namespace-row-payments-delete"));
|
|
216
|
+
|
|
217
|
+
fireEvent.change(screen.getByTestId("namespace-remove-confirm-input"), {
|
|
218
|
+
target: { value: "wrong" },
|
|
219
|
+
});
|
|
220
|
+
expect(screen.getByTestId("namespace-remove-submit")).toBeDisabled();
|
|
221
|
+
|
|
222
|
+
fireEvent.change(screen.getByTestId("namespace-remove-confirm-input"), {
|
|
223
|
+
target: { value: "payments" },
|
|
224
|
+
});
|
|
225
|
+
expect(screen.getByTestId("namespace-remove-submit")).toBeEnabled();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("calls DELETE and reloads on confirm", async () => {
|
|
229
|
+
apiFetch.mockResolvedValueOnce(mockOk({ ok: true }));
|
|
230
|
+
|
|
231
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
232
|
+
fireEvent.click(screen.getByTestId("namespace-row-payments-delete"));
|
|
233
|
+
fireEvent.change(screen.getByTestId("namespace-remove-confirm-input"), {
|
|
234
|
+
target: { value: "payments" },
|
|
235
|
+
});
|
|
236
|
+
await act(async () => {
|
|
237
|
+
fireEvent.click(screen.getByTestId("namespace-remove-submit"));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(apiFetch).toHaveBeenCalledWith(
|
|
241
|
+
"/api/namespaces/payments",
|
|
242
|
+
expect.objectContaining({ method: "DELETE" }),
|
|
243
|
+
);
|
|
244
|
+
expect(reloadManifest).toHaveBeenCalled();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("surfaces a 412 orphan-SI error from the server", async () => {
|
|
248
|
+
apiFetch.mockResolvedValueOnce(
|
|
249
|
+
mockErr(412, {
|
|
250
|
+
error:
|
|
251
|
+
"Cannot remove namespace 'payments': it is the only scope of service identity 'web-app'.",
|
|
252
|
+
}),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
256
|
+
fireEvent.click(screen.getByTestId("namespace-row-payments-delete"));
|
|
257
|
+
fireEvent.change(screen.getByTestId("namespace-remove-confirm-input"), {
|
|
258
|
+
target: { value: "payments" },
|
|
259
|
+
});
|
|
260
|
+
await act(async () => {
|
|
261
|
+
fireEvent.click(screen.getByTestId("namespace-remove-submit"));
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await waitFor(() => {
|
|
265
|
+
expect(screen.getByTestId("manifest-modal-error")).toHaveTextContent(
|
|
266
|
+
"only scope of service identity",
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
expect(reloadManifest).not.toHaveBeenCalled();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("ManifestScreen — add environment flow", () => {
|
|
274
|
+
it("opens the modal and submits with protected: true when checked", async () => {
|
|
275
|
+
apiFetch.mockResolvedValueOnce(mockOk({ name: "canary", protected: true }));
|
|
276
|
+
|
|
277
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
278
|
+
fireEvent.click(screen.getByTestId("add-environment-btn"));
|
|
279
|
+
fireEvent.change(screen.getByTestId("environment-name-input"), {
|
|
280
|
+
target: { value: "canary" },
|
|
281
|
+
});
|
|
282
|
+
fireEvent.click(screen.getByTestId("environment-protected-checkbox"));
|
|
283
|
+
await act(async () => {
|
|
284
|
+
fireEvent.click(screen.getByTestId("environment-add-submit"));
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(apiFetch).toHaveBeenCalledWith(
|
|
288
|
+
"/api/environments",
|
|
289
|
+
expect.objectContaining({
|
|
290
|
+
method: "POST",
|
|
291
|
+
body: expect.stringContaining('"protected":true'),
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
expect(reloadManifest).toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("disables submit on duplicate environment name", () => {
|
|
298
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
299
|
+
fireEvent.click(screen.getByTestId("add-environment-btn"));
|
|
300
|
+
fireEvent.change(screen.getByTestId("environment-name-input"), {
|
|
301
|
+
target: { value: "production" },
|
|
302
|
+
});
|
|
303
|
+
expect(screen.getByText(/already exists/)).toBeInTheDocument();
|
|
304
|
+
expect(screen.getByTestId("environment-add-submit")).toBeDisabled();
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("ManifestScreen — edit environment flow", () => {
|
|
309
|
+
it("renames an environment and submits the rename payload", async () => {
|
|
310
|
+
apiFetch.mockResolvedValueOnce(mockOk({ name: "development", previousName: "dev" }));
|
|
311
|
+
|
|
312
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
313
|
+
fireEvent.click(screen.getByTestId("environment-row-dev-edit"));
|
|
314
|
+
fireEvent.change(screen.getByTestId("environment-rename-input"), {
|
|
315
|
+
target: { value: "development" },
|
|
316
|
+
});
|
|
317
|
+
await act(async () => {
|
|
318
|
+
fireEvent.click(screen.getByTestId("environment-edit-submit"));
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
expect(apiFetch).toHaveBeenCalledWith(
|
|
322
|
+
"/api/environments/dev",
|
|
323
|
+
expect.objectContaining({
|
|
324
|
+
method: "PATCH",
|
|
325
|
+
body: expect.stringContaining('"rename":"development"'),
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("toggles protected and submits the protected change only", async () => {
|
|
331
|
+
apiFetch.mockResolvedValueOnce(mockOk({ name: "production" }));
|
|
332
|
+
|
|
333
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
334
|
+
fireEvent.click(screen.getByTestId("environment-row-production-edit"));
|
|
335
|
+
// production starts protected; uncheck to test the unprotect path
|
|
336
|
+
fireEvent.click(screen.getByTestId("environment-protected-checkbox"));
|
|
337
|
+
await act(async () => {
|
|
338
|
+
fireEvent.click(screen.getByTestId("environment-edit-submit"));
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const lastCall = apiFetch.mock.calls[0];
|
|
342
|
+
expect(lastCall[0]).toBe("/api/environments/production");
|
|
343
|
+
const body = JSON.parse((lastCall[1] as { body: string }).body);
|
|
344
|
+
expect(body).toEqual({ protected: false });
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe("ManifestScreen — remove environment flow", () => {
|
|
349
|
+
it("warns about protected envs in the impact description", () => {
|
|
350
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
351
|
+
fireEvent.click(screen.getByTestId("environment-row-production-delete"));
|
|
352
|
+
expect(screen.getByText(/protected environment/)).toBeInTheDocument();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("calls DELETE on confirm and reloads", async () => {
|
|
356
|
+
apiFetch.mockResolvedValueOnce(mockOk({ ok: true }));
|
|
357
|
+
|
|
358
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
359
|
+
fireEvent.click(screen.getByTestId("environment-row-dev-delete"));
|
|
360
|
+
fireEvent.change(screen.getByTestId("environment-remove-confirm-input"), {
|
|
361
|
+
target: { value: "dev" },
|
|
362
|
+
});
|
|
363
|
+
await act(async () => {
|
|
364
|
+
fireEvent.click(screen.getByTestId("environment-remove-submit"));
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
expect(apiFetch).toHaveBeenCalledWith(
|
|
368
|
+
"/api/environments/dev",
|
|
369
|
+
expect.objectContaining({ method: "DELETE" }),
|
|
370
|
+
);
|
|
371
|
+
expect(reloadManifest).toHaveBeenCalled();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("surfaces a 412 protected-env error from the server", async () => {
|
|
375
|
+
apiFetch.mockResolvedValueOnce(
|
|
376
|
+
mockErr(412, {
|
|
377
|
+
error: "Environment 'production' is protected. Cannot remove a protected environment.",
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
render(<ManifestScreen manifest={baseManifest} reloadManifest={reloadManifest} />);
|
|
382
|
+
fireEvent.click(screen.getByTestId("environment-row-production-delete"));
|
|
383
|
+
fireEvent.change(screen.getByTestId("environment-remove-confirm-input"), {
|
|
384
|
+
target: { value: "production" },
|
|
385
|
+
});
|
|
386
|
+
await act(async () => {
|
|
387
|
+
fireEvent.click(screen.getByTestId("environment-remove-submit"));
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
await waitFor(() => {
|
|
391
|
+
expect(screen.getByTestId("manifest-modal-error")).toHaveTextContent("protected");
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
});
|