@isi-ui7/bos7-shared 0.3.7 → 0.3.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.
- package/dist/detail-modal-field.d.ts +47 -0
- package/dist/form-types.d.ts +89 -1
- package/dist/i18n.d.ts +9 -0
- package/dist/{index-BI2MxWot.js → index-DqWPV29B.js} +10 -2
- package/dist/{index.es-D-PR3JkH.js → index.es-9cvhYbjn.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{jspdf.es.min-DzRwzM6G.js → jspdf.es.min-CCBzPHw4.js} +1 -1
- package/package.json +1 -1
- package/src/crud-components.tsx +6 -1
- package/src/detail-modal-field.test.tsx +530 -0
- package/src/detail-modal-field.tsx +342 -0
- package/src/form-renderer.tsx +83 -0
- package/src/form-types.ts +94 -1
- package/src/i18n.tsx +19 -0
- package/src/wizard-stepper.tsx +1 -1
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeAll } from "vitest";
|
|
2
|
+
import { render, screen, within } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { SchemaFormRenderer } from "./form-renderer";
|
|
5
|
+
import type { FormSection } from "./form-types";
|
|
6
|
+
|
|
7
|
+
type D = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
// jsdom tidak punya ResizeObserver, dan `ComposedModal` Carbon memakainya. Paket
|
|
10
|
+
// ini belum punya berkas setup vitest karena belum ada satu pun tesnya yang
|
|
11
|
+
// pernah memasang modal. Stub-nya ditaruh di sini, bukan di config global,
|
|
12
|
+
// supaya cakupannya jelas dan tidak diam-diam mengubah lingkungan tes lain.
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
if (!("ResizeObserver" in globalThis)) {
|
|
15
|
+
(globalThis as Record<string, unknown>).ResizeObserver = class {
|
|
16
|
+
observe() {}
|
|
17
|
+
unobserve() {}
|
|
18
|
+
disconnect() {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Baris tabel yang memuat teks tertentu.
|
|
25
|
+
*
|
|
26
|
+
* Aksi per baris dicari lewat barisnya, bukan lewat nama tombol yang unik:
|
|
27
|
+
* nama terakses tombol ikon Carbon berasal dari tooltip-nya, jadi semua tombol
|
|
28
|
+
* Hapus bernama sama. Ini juga cara operator bekerja — ia mencari BARIS "Budi",
|
|
29
|
+
* lalu menekan Hapus di baris itu.
|
|
30
|
+
*/
|
|
31
|
+
function rowWith(text: string): HTMLElement {
|
|
32
|
+
const cell = screen.getByText(text);
|
|
33
|
+
const row = cell.closest("tr");
|
|
34
|
+
if (!row) throw new Error(`Baris untuk "${text}" tidak ditemukan`);
|
|
35
|
+
return row as HTMLElement;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Tipe field `detail-modal` — tabel ringkas + modal berisi form penuh satu baris.
|
|
39
|
+
//
|
|
40
|
+
// Uji ini ditulis dengan KOSAKATA OPERATOR, bukan kosakata API: yang dicari
|
|
41
|
+
// "Tambah Ahli Waris", "Nama Lengkap", "Simpan" — teks yang benar-benar ada di
|
|
42
|
+
// layar. Dua cacat di modul ini lolos justru karena tesnya memakai nama field
|
|
43
|
+
// versi backend, sehingga salah-petakan antara form dan payload tak terlihat.
|
|
44
|
+
//
|
|
45
|
+
// Yang paling dijaga: baris yang tersimpan harus membawa SELURUH field yang
|
|
46
|
+
// diisi operator. Kelas kegagalan termahal di wizard ini bukan galat, melainkan
|
|
47
|
+
// field yang hilang dari payload tanpa satu pun tanda.
|
|
48
|
+
|
|
49
|
+
const heirSections: FormSection<Record<string, unknown>>[] = [
|
|
50
|
+
{
|
|
51
|
+
title: "Data Ahli Waris",
|
|
52
|
+
columns: 2,
|
|
53
|
+
fields: [
|
|
54
|
+
{
|
|
55
|
+
key: "full_name",
|
|
56
|
+
label: "Nama Lengkap",
|
|
57
|
+
type: "text",
|
|
58
|
+
validation: { required: "Nama wajib diisi" },
|
|
59
|
+
},
|
|
60
|
+
{ key: "seq_no", label: "Nomor Urut Prioritas", type: "text" },
|
|
61
|
+
{ key: "notes", label: "Keterangan", type: "text" },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
function heirField(overrides: Record<string, unknown> = {}) {
|
|
67
|
+
return {
|
|
68
|
+
key: "heirs",
|
|
69
|
+
label: "Daftar Ahli Waris",
|
|
70
|
+
type: "detail-modal" as const,
|
|
71
|
+
detailModal: {
|
|
72
|
+
sections: heirSections,
|
|
73
|
+
columns: [
|
|
74
|
+
{ field: "full_name", header: "Nama" },
|
|
75
|
+
{ field: "seq_no", header: "Prioritas" },
|
|
76
|
+
{ field: "notes", header: "Keterangan" },
|
|
77
|
+
],
|
|
78
|
+
newRowFactory: () => ({ full_name: "", seq_no: "", notes: "" }),
|
|
79
|
+
addLabel: "Tambah Ahli Waris",
|
|
80
|
+
addModalTitle: "Tambah Ahli Waris",
|
|
81
|
+
emptyText: 'Belum ada ahli waris. Klik "Tambah Ahli Waris".',
|
|
82
|
+
...overrides,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderHeirs(
|
|
88
|
+
value: D,
|
|
89
|
+
onChange: (patch: Partial<D>) => void,
|
|
90
|
+
fieldOverrides: Record<string, unknown> = {},
|
|
91
|
+
mode: "create" | "edit" | "view" = "create",
|
|
92
|
+
) {
|
|
93
|
+
const section: FormSection<D> = {
|
|
94
|
+
title: "Ahli Waris",
|
|
95
|
+
fields: [heirField(fieldOverrides)],
|
|
96
|
+
};
|
|
97
|
+
return render(
|
|
98
|
+
<SchemaFormRenderer<D>
|
|
99
|
+
mode={mode}
|
|
100
|
+
value={value}
|
|
101
|
+
sections={[section]}
|
|
102
|
+
onChange={onChange}
|
|
103
|
+
/>,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
describe("detail-modal — tabel ringkas", () => {
|
|
108
|
+
it("tabel kosong mengajak menambah, bukan menampilkan baris hantu", () => {
|
|
109
|
+
renderHeirs({ heirs: [] }, () => {});
|
|
110
|
+
expect(
|
|
111
|
+
screen.getByText('Belum ada ahli waris. Klik "Tambah Ahli Waris".'),
|
|
112
|
+
).toBeTruthy();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("baris yang ada tampil sebagai ringkasan, bernomor urut", () => {
|
|
116
|
+
renderHeirs(
|
|
117
|
+
{
|
|
118
|
+
heirs: [
|
|
119
|
+
{ full_name: "Siti Aminah", seq_no: "1", notes: "Istri" },
|
|
120
|
+
{ full_name: "Budi Santoso", seq_no: "2", notes: "Anak" },
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
() => {},
|
|
124
|
+
);
|
|
125
|
+
expect(screen.getByText("Siti Aminah")).toBeTruthy();
|
|
126
|
+
expect(screen.getByText("Budi Santoso")).toBeTruthy();
|
|
127
|
+
// Nomor urut dihitung renderer, bukan diambil dari data.
|
|
128
|
+
expect(screen.getByText("Istri")).toBeTruthy();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("nilai kosong tampil sebagai em-dash, bukan sel kosong yang ambigu", () => {
|
|
132
|
+
renderHeirs({ heirs: [{ full_name: "Siti Aminah", seq_no: "", notes: "" }] }, () => {});
|
|
133
|
+
expect(screen.getAllByText("—").length).toBeGreaterThan(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("nilai simpan bisa dialihkan ke label lewat format()", () => {
|
|
137
|
+
renderHeirs(
|
|
138
|
+
{ heirs: [{ full_name: "Siti", seq_no: "1", notes: "", gender: "M" }] },
|
|
139
|
+
() => {},
|
|
140
|
+
{
|
|
141
|
+
columns: [
|
|
142
|
+
{ field: "full_name", header: "Nama" },
|
|
143
|
+
{
|
|
144
|
+
field: "gender",
|
|
145
|
+
header: "Jenis Kelamin",
|
|
146
|
+
format: (v: unknown) => (v === "M" ? "Laki-laki" : "Perempuan"),
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
// Tanpa format, kolom ini akan menampilkan "M" — kode simpan, bukan
|
|
152
|
+
// sesuatu yang berarti bagi operator.
|
|
153
|
+
expect(screen.getByText("Laki-laki")).toBeTruthy();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("detail-modal — menambah baris lewat modal", () => {
|
|
158
|
+
it("tombol Tambah membuka modal berisi form penuh baris", async () => {
|
|
159
|
+
const user = userEvent.setup();
|
|
160
|
+
renderHeirs({ heirs: [] }, () => {});
|
|
161
|
+
|
|
162
|
+
// Sebelum diklik, form baris tidak ada di layar sama sekali.
|
|
163
|
+
expect(screen.queryByLabelText("Nomor Urut Prioritas")).toBeNull();
|
|
164
|
+
|
|
165
|
+
await user.click(screen.getByRole("button", { name: "Tambah Ahli Waris" }));
|
|
166
|
+
|
|
167
|
+
expect(screen.getByLabelText(/Nama Lengkap/)).toBeTruthy();
|
|
168
|
+
expect(screen.getByLabelText("Nomor Urut Prioritas")).toBeTruthy();
|
|
169
|
+
expect(screen.getByLabelText("Keterangan")).toBeTruthy();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("Simpan menaruh SELURUH field yang diisi ke payload induk", async () => {
|
|
173
|
+
const user = userEvent.setup();
|
|
174
|
+
const onChange = vi.fn();
|
|
175
|
+
renderHeirs({ heirs: [] }, onChange);
|
|
176
|
+
|
|
177
|
+
await user.click(screen.getByRole("button", { name: "Tambah Ahli Waris" }));
|
|
178
|
+
await user.type(screen.getByLabelText(/Nama Lengkap/), "Siti Aminah");
|
|
179
|
+
await user.type(screen.getByLabelText("Nomor Urut Prioritas"), "1");
|
|
180
|
+
await user.type(screen.getByLabelText("Keterangan"), "Istri");
|
|
181
|
+
await user.click(screen.getByRole("button", { name: "Simpan" }));
|
|
182
|
+
|
|
183
|
+
// Inti uji ini: bukan bahwa onChange terpanggil, tapi bahwa KETIGA field
|
|
184
|
+
// sampai utuh. Pemetaan yang meleset tidak menghasilkan galat di mana pun —
|
|
185
|
+
// baris tetap tersimpan, hanya isinya yang bolong.
|
|
186
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
187
|
+
expect(onChange).toHaveBeenCalledWith({
|
|
188
|
+
heirs: [{ full_name: "Siti Aminah", seq_no: "1", notes: "Istri" }],
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("baris baru DITAMBAHKAN, tidak menimpa baris yang sudah ada", async () => {
|
|
193
|
+
const user = userEvent.setup();
|
|
194
|
+
const onChange = vi.fn();
|
|
195
|
+
renderHeirs({ heirs: [{ full_name: "Budi", seq_no: "1", notes: "" }] }, onChange);
|
|
196
|
+
|
|
197
|
+
await user.click(screen.getByRole("button", { name: "Tambah Ahli Waris" }));
|
|
198
|
+
await user.type(screen.getByLabelText(/Nama Lengkap/), "Siti");
|
|
199
|
+
await user.click(screen.getByRole("button", { name: "Simpan" }));
|
|
200
|
+
|
|
201
|
+
expect(onChange).toHaveBeenCalledWith({
|
|
202
|
+
heirs: [
|
|
203
|
+
{ full_name: "Budi", seq_no: "1", notes: "" },
|
|
204
|
+
{ full_name: "Siti", seq_no: "", notes: "" },
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("detail-modal — gerbang validasi", () => {
|
|
211
|
+
it("Nama kosong MENAHAN Simpan: baris tidak pernah masuk payload", async () => {
|
|
212
|
+
const user = userEvent.setup();
|
|
213
|
+
const onChange = vi.fn();
|
|
214
|
+
renderHeirs({ heirs: [] }, onChange);
|
|
215
|
+
|
|
216
|
+
await user.click(screen.getByRole("button", { name: "Tambah Ahli Waris" }));
|
|
217
|
+
await user.type(screen.getByLabelText("Keterangan"), "Istri");
|
|
218
|
+
await user.click(screen.getByRole("button", { name: "Simpan" }));
|
|
219
|
+
|
|
220
|
+
// Yang diukur adalah AKIBATNYA, bukan hanya munculnya pesan: baris yang
|
|
221
|
+
// tidak sah tidak boleh sampai ke data form dengan cara apa pun.
|
|
222
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
223
|
+
expect(screen.getByText("Nama wajib diisi")).toBeTruthy();
|
|
224
|
+
// Modal tetap terbuka supaya operator bisa memperbaikinya.
|
|
225
|
+
expect(screen.getByLabelText(/Nama Lengkap/)).toBeTruthy();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("setelah diperbaiki, Simpan lolos dan barisnya masuk", async () => {
|
|
229
|
+
const user = userEvent.setup();
|
|
230
|
+
const onChange = vi.fn();
|
|
231
|
+
renderHeirs({ heirs: [] }, onChange);
|
|
232
|
+
|
|
233
|
+
await user.click(screen.getByRole("button", { name: "Tambah Ahli Waris" }));
|
|
234
|
+
await user.click(screen.getByRole("button", { name: "Simpan" }));
|
|
235
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
236
|
+
|
|
237
|
+
await user.type(screen.getByLabelText(/Nama Lengkap/), "Siti");
|
|
238
|
+
await user.click(screen.getByRole("button", { name: "Simpan" }));
|
|
239
|
+
|
|
240
|
+
expect(onChange).toHaveBeenCalledWith({
|
|
241
|
+
heirs: [{ full_name: "Siti", seq_no: "", notes: "" }],
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("validateRow berjalan SETELAH validasi per-field dan bisa menahan sendiri", async () => {
|
|
246
|
+
const user = userEvent.setup();
|
|
247
|
+
const onChange = vi.fn();
|
|
248
|
+
renderHeirs({ heirs: [] }, onChange, {
|
|
249
|
+
validateRow: (row: Record<string, unknown>) =>
|
|
250
|
+
String(row.seq_no ?? "") === "" ? { seq_no: "Nomor urut wajib" } : {},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await user.click(screen.getByRole("button", { name: "Tambah Ahli Waris" }));
|
|
254
|
+
await user.type(screen.getByLabelText(/Nama Lengkap/), "Siti");
|
|
255
|
+
await user.click(screen.getByRole("button", { name: "Simpan" }));
|
|
256
|
+
|
|
257
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
258
|
+
expect(screen.getByText("Nomor urut wajib")).toBeTruthy();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("validateRow menerima indeks tempat baris BARU akan mendarat", async () => {
|
|
262
|
+
const user = userEvent.setup();
|
|
263
|
+
const seen: number[] = [];
|
|
264
|
+
renderHeirs(
|
|
265
|
+
{ heirs: [{ full_name: "Budi", seq_no: "1", notes: "" }] },
|
|
266
|
+
() => {},
|
|
267
|
+
{
|
|
268
|
+
validateRow: (_row: Record<string, unknown>, index: number) => {
|
|
269
|
+
seen.push(index);
|
|
270
|
+
return {};
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
await user.click(screen.getByRole("button", { name: "Tambah Ahli Waris" }));
|
|
276
|
+
await user.type(screen.getByLabelText(/Nama Lengkap/), "Siti");
|
|
277
|
+
await user.click(screen.getByRole("button", { name: "Simpan" }));
|
|
278
|
+
|
|
279
|
+
// Sudah ada 1 baris, jadi baris baru akan mendarat di indeks 1 — bukan -1,
|
|
280
|
+
// dan bukan 0. Aturan yang bergantung urutan harus melihat posisi akhirnya.
|
|
281
|
+
expect(seen).toEqual([1]);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("detail-modal — ubah, batal, hapus", () => {
|
|
286
|
+
it("Batal membuang suntingan: payload induk tidak tersentuh", async () => {
|
|
287
|
+
const user = userEvent.setup();
|
|
288
|
+
const onChange = vi.fn();
|
|
289
|
+
renderHeirs({ heirs: [{ full_name: "Budi", seq_no: "1", notes: "" }] }, onChange);
|
|
290
|
+
|
|
291
|
+
await user.click(within(rowWith("Budi")).getByRole("button", { name: "Ubah" }));
|
|
292
|
+
await user.clear(screen.getByLabelText(/Nama Lengkap/));
|
|
293
|
+
await user.type(screen.getByLabelText(/Nama Lengkap/), "DIUBAH");
|
|
294
|
+
await user.click(screen.getByRole("button", { name: "Batal" }));
|
|
295
|
+
|
|
296
|
+
// Draft disimpan terpisah justru untuk ini: menyunting objek baris langsung
|
|
297
|
+
// akan mengubah data induk sebelum Simpan, dan Batal tak bisa lagi
|
|
298
|
+
// mengembalikannya — tanpa galat, dan tanpa cara mengetahuinya.
|
|
299
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
300
|
+
expect(screen.getByText("Budi")).toBeTruthy();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("Ubah menulis kembali ke indeks yang sama, bukan menambah baris", async () => {
|
|
304
|
+
const user = userEvent.setup();
|
|
305
|
+
const onChange = vi.fn();
|
|
306
|
+
renderHeirs(
|
|
307
|
+
{
|
|
308
|
+
heirs: [
|
|
309
|
+
{ full_name: "Budi", seq_no: "1", notes: "" },
|
|
310
|
+
{ full_name: "Siti", seq_no: "2", notes: "" },
|
|
311
|
+
],
|
|
312
|
+
},
|
|
313
|
+
onChange,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
await user.click(within(rowWith("Siti")).getByRole("button", { name: "Ubah" }));
|
|
317
|
+
await user.clear(screen.getByLabelText(/Nama Lengkap/));
|
|
318
|
+
await user.type(screen.getByLabelText(/Nama Lengkap/), "Siti Aminah");
|
|
319
|
+
await user.click(screen.getByRole("button", { name: "Simpan" }));
|
|
320
|
+
|
|
321
|
+
expect(onChange).toHaveBeenCalledWith({
|
|
322
|
+
heirs: [
|
|
323
|
+
{ full_name: "Budi", seq_no: "1", notes: "" },
|
|
324
|
+
{ full_name: "Siti Aminah", seq_no: "2", notes: "" },
|
|
325
|
+
],
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("modal dibuka ulang membawa nilai baris itu, bukan sisa baris sebelumnya", async () => {
|
|
330
|
+
const user = userEvent.setup();
|
|
331
|
+
renderHeirs(
|
|
332
|
+
{
|
|
333
|
+
heirs: [
|
|
334
|
+
{ full_name: "Budi", seq_no: "1", notes: "" },
|
|
335
|
+
{ full_name: "Siti", seq_no: "2", notes: "" },
|
|
336
|
+
],
|
|
337
|
+
},
|
|
338
|
+
() => {},
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
await user.click(within(rowWith("Budi")).getByRole("button", { name: "Ubah" }));
|
|
342
|
+
expect((screen.getByLabelText(/Nama Lengkap/) as HTMLInputElement).value).toBe("Budi");
|
|
343
|
+
await user.click(screen.getByRole("button", { name: "Batal" }));
|
|
344
|
+
|
|
345
|
+
await user.click(within(rowWith("Siti")).getByRole("button", { name: "Ubah" }));
|
|
346
|
+
expect((screen.getByLabelText(/Nama Lengkap/) as HTMLInputElement).value).toBe("Siti");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("Hapus mengeluarkan baris itu saja", async () => {
|
|
350
|
+
const user = userEvent.setup();
|
|
351
|
+
const onChange = vi.fn();
|
|
352
|
+
renderHeirs(
|
|
353
|
+
{
|
|
354
|
+
heirs: [
|
|
355
|
+
{ full_name: "Budi", seq_no: "1", notes: "" },
|
|
356
|
+
{ full_name: "Siti", seq_no: "2", notes: "" },
|
|
357
|
+
],
|
|
358
|
+
},
|
|
359
|
+
onChange,
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
await user.click(within(rowWith("Budi")).getByRole("button", { name: "Hapus" }));
|
|
363
|
+
|
|
364
|
+
expect(onChange).toHaveBeenCalledWith({
|
|
365
|
+
heirs: [{ full_name: "Siti", seq_no: "2", notes: "" }],
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe("detail-modal — mode & batas", () => {
|
|
371
|
+
it("mode view: tabel tetap terbaca, Tambah dan Aksi hilang", () => {
|
|
372
|
+
renderHeirs(
|
|
373
|
+
{ heirs: [{ full_name: "Budi", seq_no: "1", notes: "" }] },
|
|
374
|
+
() => {},
|
|
375
|
+
{},
|
|
376
|
+
"view",
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
expect(screen.getByText("Budi")).toBeTruthy();
|
|
380
|
+
expect(screen.queryByRole("button", { name: "Tambah Ahli Waris" })).toBeNull();
|
|
381
|
+
expect(within(rowWith("Budi")).queryByRole("button", { name: "Ubah" })).toBeNull();
|
|
382
|
+
expect(within(rowWith("Budi")).queryByRole("button", { name: "Hapus" })).toBeNull();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("maxRows menonaktifkan Tambah setelah batas tercapai", () => {
|
|
386
|
+
renderHeirs(
|
|
387
|
+
{ heirs: [{ full_name: "Budi", seq_no: "1", notes: "" }] },
|
|
388
|
+
() => {},
|
|
389
|
+
{ maxRows: 1 },
|
|
390
|
+
);
|
|
391
|
+
const btn = screen.getByRole("button", { name: "Tambah Ahli Waris" }) as HTMLButtonElement;
|
|
392
|
+
expect(btn.disabled).toBe(true);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("allowEdit/allowDelete false menghilangkan aksinya masing-masing", () => {
|
|
396
|
+
renderHeirs(
|
|
397
|
+
{ heirs: [{ full_name: "Budi", seq_no: "1", notes: "" }] },
|
|
398
|
+
() => {},
|
|
399
|
+
{ allowEdit: false },
|
|
400
|
+
);
|
|
401
|
+
expect(within(rowWith("Budi")).queryByRole("button", { name: "Ubah" })).toBeNull();
|
|
402
|
+
expect(within(rowWith("Budi")).getByRole("button", { name: "Hapus" })).toBeTruthy();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("nilai bukan-array tidak meruntuhkan tabel", () => {
|
|
406
|
+
// Field yang belum pernah diisi tiba sebagai undefined dari backend.
|
|
407
|
+
renderHeirs({}, () => {});
|
|
408
|
+
expect(
|
|
409
|
+
screen.getByText('Belum ada ahli waris. Klik "Tambah Ahli Waris".'),
|
|
410
|
+
).toBeTruthy();
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("config yang hilang BERBUNYI, tidak melenyapkan field diam-diam", () => {
|
|
414
|
+
const section: FormSection<D> = {
|
|
415
|
+
title: "Ahli Waris",
|
|
416
|
+
fields: [{ key: "heirs", label: "Daftar Ahli Waris", type: "detail-modal" }],
|
|
417
|
+
};
|
|
418
|
+
render(
|
|
419
|
+
<SchemaFormRenderer<D>
|
|
420
|
+
mode="create"
|
|
421
|
+
value={{ heirs: [] }}
|
|
422
|
+
sections={[section]}
|
|
423
|
+
onChange={() => {}}
|
|
424
|
+
/>,
|
|
425
|
+
);
|
|
426
|
+
// Mengembalikan [] di sini akan membuat field salah-tulis lenyap dari layar
|
|
427
|
+
// tanpa satu pun galat — bentuk kegagalan yang paling mahal di modul ini.
|
|
428
|
+
expect(screen.getByRole("alert").textContent).toContain("detail-modal");
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
describe("KONTROL REGRESI — jalur yang tidak disentuh", () => {
|
|
433
|
+
it("field text biasa tetap terender seperti sebelumnya", () => {
|
|
434
|
+
const section: FormSection<D> = {
|
|
435
|
+
title: "Biasa",
|
|
436
|
+
fields: [{ key: "nama", label: "Nama Lengkap", type: "text", span: 4 }],
|
|
437
|
+
};
|
|
438
|
+
render(
|
|
439
|
+
<SchemaFormRenderer<D> mode="create" value={{}} sections={[section]} onChange={() => {}} />,
|
|
440
|
+
);
|
|
441
|
+
expect(screen.getByLabelText(/Nama Lengkap/)).toBeTruthy();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("detail-rows (EditableTable inline) tetap jalan berdampingan", () => {
|
|
445
|
+
const section: FormSection<D> = {
|
|
446
|
+
title: "Campur",
|
|
447
|
+
fields: [
|
|
448
|
+
{
|
|
449
|
+
key: "limits",
|
|
450
|
+
label: "Limit Transaksi",
|
|
451
|
+
type: "detail-rows",
|
|
452
|
+
detailRows: {
|
|
453
|
+
columns: [
|
|
454
|
+
{ field: "kode", header: "Kode", columnType: { type: "text" } },
|
|
455
|
+
],
|
|
456
|
+
newRowFactory: () => ({ kode: "" }),
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
heirField(),
|
|
460
|
+
],
|
|
461
|
+
};
|
|
462
|
+
render(
|
|
463
|
+
<SchemaFormRenderer<D>
|
|
464
|
+
mode="create"
|
|
465
|
+
value={{ limits: [{ kode: "CASH_IN" }], heirs: [] }}
|
|
466
|
+
sections={[section]}
|
|
467
|
+
onChange={() => {}}
|
|
468
|
+
/>,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// Dua tipe tabel yang berbeda hidup dalam satu form tanpa saling ganggu.
|
|
472
|
+
expect(screen.getByText("Limit Transaksi")).toBeTruthy();
|
|
473
|
+
expect(screen.getByRole("button", { name: "Tambah Ahli Waris" })).toBeTruthy();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("dua detail-modal dalam satu form berdiri sendiri-sendiri", async () => {
|
|
477
|
+
const user = userEvent.setup();
|
|
478
|
+
const onChange = vi.fn();
|
|
479
|
+
const section: FormSection<D> = {
|
|
480
|
+
title: "Langkah 4",
|
|
481
|
+
fields: [
|
|
482
|
+
heirField(),
|
|
483
|
+
{
|
|
484
|
+
key: "relations",
|
|
485
|
+
label: "Daftar Relasi",
|
|
486
|
+
type: "detail-modal" as const,
|
|
487
|
+
detailModal: {
|
|
488
|
+
sections: [
|
|
489
|
+
{
|
|
490
|
+
title: "Data Relasi",
|
|
491
|
+
fields: [
|
|
492
|
+
{
|
|
493
|
+
key: "first_name",
|
|
494
|
+
label: "Nama Relasi",
|
|
495
|
+
type: "text" as const,
|
|
496
|
+
validation: { required: "Nama relasi wajib diisi" },
|
|
497
|
+
},
|
|
498
|
+
],
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
columns: [{ field: "first_name", header: "Nama" }],
|
|
502
|
+
newRowFactory: () => ({ first_name: "" }),
|
|
503
|
+
addLabel: "Tambah Relasi",
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
],
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
render(
|
|
510
|
+
<SchemaFormRenderer<D>
|
|
511
|
+
mode="create"
|
|
512
|
+
value={{ heirs: [], relations: [] }}
|
|
513
|
+
sections={[section]}
|
|
514
|
+
onChange={onChange}
|
|
515
|
+
/>,
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
// Membuka modal Relasi tidak boleh memunculkan form Ahli Waris.
|
|
519
|
+
await user.click(screen.getByRole("button", { name: "Tambah Relasi" }));
|
|
520
|
+
const dialog = screen.getByRole("dialog");
|
|
521
|
+
expect(within(dialog).getByLabelText(/Nama Relasi/)).toBeTruthy();
|
|
522
|
+
expect(within(dialog).queryByLabelText("Nomor Urut Prioritas")).toBeNull();
|
|
523
|
+
|
|
524
|
+
await user.type(screen.getByLabelText(/Nama Relasi/), "Andi");
|
|
525
|
+
await user.click(screen.getByRole("button", { name: "Simpan" }));
|
|
526
|
+
|
|
527
|
+
// Hanya kunci `relations` yang ditulis — `heirs` tidak ikut tersentuh.
|
|
528
|
+
expect(onChange).toHaveBeenCalledWith({ relations: [{ first_name: "Andi" }] });
|
|
529
|
+
});
|
|
530
|
+
});
|