@isi-ui7/bos7-shared 0.3.9 → 0.3.10
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/{index-DqWPV29B.js → index-C_114Ysm.js} +71 -71
- package/dist/{index.es-9cvhYbjn.js → index.es-B-MluT1E.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{jspdf.es.min-CCBzPHw4.js → jspdf.es.min-C5VEsLZd.js} +2 -2
- package/dist/{jspdf.plugin.autotable-DFaknRns.js → jspdf.plugin.autotable-DcntYaes.js} +22 -19
- package/dist/purify.es-CiEWEeUM.js +605 -0
- package/dist/vitest.setup.d.ts +0 -0
- package/package.json +30 -81
- package/src/crud-components.tsx +108 -4
- package/src/vitest.setup.ts +22 -0
- package/src/wizard-mode-nav.test.tsx +136 -0
- package/dist/purify.es-Cm3utOpm.js +0 -560
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isi-ui7/bos7-shared",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "Shared auth7 and layout primitives for bos7 applications.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"directory": "packages/bos7-shared"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
|
-
"main": "
|
|
13
|
-
"module": "
|
|
14
|
-
"types": "
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"module": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
15
|
"exports": {
|
|
16
|
-
".": "./
|
|
16
|
+
".": "./dist/index.js",
|
|
17
17
|
"./auth7": "./src/auth7/index.ts",
|
|
18
18
|
"./page-layout": "./src/page-layout.tsx",
|
|
19
19
|
"./modal-layout": "./src/modal-layout.tsx",
|
|
@@ -50,85 +50,28 @@
|
|
|
50
50
|
"./bff/health": "./src/bff/health.ts",
|
|
51
51
|
"./observability/otel-init": "./src/observability/otel-init.ts",
|
|
52
52
|
"./form-types": "./src/form-types.ts",
|
|
53
|
-
"./form-value-schema": "./src/form-value-schema.ts",
|
|
54
53
|
"./form-numeric": "./src/form-numeric.ts",
|
|
55
54
|
"./form-renderer": "./src/form-renderer.tsx",
|
|
56
55
|
"./form-renderer-utils": "./src/form-renderer-utils.ts",
|
|
57
56
|
"./form-rules": "./src/form-rules.ts",
|
|
58
57
|
"./i18n": "./src/i18n.tsx",
|
|
59
58
|
"./workflow/use-crud-form": "./src/workflow/use-crud-form.ts",
|
|
60
|
-
"./styles": "./src/form-contract.css"
|
|
59
|
+
"./styles": "./src/form-contract.css",
|
|
60
|
+
"./form-value-schema": "./src/form-value-schema.ts"
|
|
61
61
|
},
|
|
62
62
|
"publishConfig": {
|
|
63
63
|
"registry": "https://registry.npmjs.org",
|
|
64
|
-
"access": "public"
|
|
65
|
-
"main": "dist/index.js",
|
|
66
|
-
"module": "dist/index.js",
|
|
67
|
-
"types": "dist/index.d.ts",
|
|
68
|
-
"exports": {
|
|
69
|
-
".": "./dist/index.js",
|
|
70
|
-
"./auth7": "./src/auth7/index.ts",
|
|
71
|
-
"./page-layout": "./src/page-layout.tsx",
|
|
72
|
-
"./modal-layout": "./src/modal-layout.tsx",
|
|
73
|
-
"./form-layout": "./src/form-layout.tsx",
|
|
74
|
-
"./backend": "./src/backend.ts",
|
|
75
|
-
"./export": "./src/export.ts",
|
|
76
|
-
"./types/notification": "./src/types/notification.ts",
|
|
77
|
-
"./nats/client": "./src/nats/client.ts",
|
|
78
|
-
"./event-bus": "./src/event-bus/index.ts",
|
|
79
|
-
"./event-bus/useEventBus": "./src/event-bus/useEventBus.ts",
|
|
80
|
-
"./api": "./src/api.ts",
|
|
81
|
-
"./types/core": "./src/types/core.ts",
|
|
82
|
-
"./style-contract": "./src/style-contract.ts",
|
|
83
|
-
"./crud-components": "./src/crud-components.tsx",
|
|
84
|
-
"./workflow/backend": "./src/workflow/backend.ts",
|
|
85
|
-
"./workflow/notif7": "./src/workflow/notif7.ts",
|
|
86
|
-
"./workflow/api-client": "./src/workflow/api-client.ts",
|
|
87
|
-
"./workflow/starter": "./src/workflow/starter.ts",
|
|
88
|
-
"./workflow/use-workflow-tracker": "./src/workflow/use-workflow-tracker.ts",
|
|
89
|
-
"./workflow/crud-client": "./src/workflow/crud-client.ts",
|
|
90
|
-
"./data-table/cursor-adapter": "./src/data-table/cursor-adapter.ts",
|
|
91
|
-
"./data-table/proxy": "./src/data-table/proxy.ts",
|
|
92
|
-
"./notification/routing": "./src/notification/routing.ts",
|
|
93
|
-
"./shell/interaction": "./src/shell/interaction.ts",
|
|
94
|
-
"./shell/branch": "./src/shell/branch.ts",
|
|
95
|
-
"./notif7": "./src/notif7.ts",
|
|
96
|
-
"./shell/provider": "./src/shell/provider.tsx",
|
|
97
|
-
"./shell/app-shell-layout": "./src/shell/app-shell-layout.tsx",
|
|
98
|
-
"./notifications-bff": "./src/notifications-bff.ts",
|
|
99
|
-
"./bff/auth-me": "./src/bff/auth-me.ts",
|
|
100
|
-
"./bff/auth-refresh": "./src/bff/auth-refresh.ts",
|
|
101
|
-
"./bff/auth-logout": "./src/bff/auth-logout.ts",
|
|
102
|
-
"./bff/auth-preferences": "./src/bff/auth-preferences.ts",
|
|
103
|
-
"./bff/health": "./src/bff/health.ts",
|
|
104
|
-
"./observability/otel-init": "./src/observability/otel-init.ts",
|
|
105
|
-
"./form-types": "./src/form-types.ts",
|
|
106
|
-
"./form-numeric": "./src/form-numeric.ts",
|
|
107
|
-
"./form-renderer": "./src/form-renderer.tsx",
|
|
108
|
-
"./form-renderer-utils": "./src/form-renderer-utils.ts",
|
|
109
|
-
"./form-rules": "./src/form-rules.ts",
|
|
110
|
-
"./i18n": "./src/i18n.tsx",
|
|
111
|
-
"./workflow/use-crud-form": "./src/workflow/use-crud-form.ts",
|
|
112
|
-
"./styles": "./src/form-contract.css",
|
|
113
|
-
"./form-value-schema": "./src/form-value-schema.ts"
|
|
114
|
-
}
|
|
64
|
+
"access": "public"
|
|
115
65
|
},
|
|
116
66
|
"files": [
|
|
117
67
|
"dist",
|
|
118
68
|
"src"
|
|
119
69
|
],
|
|
120
|
-
"scripts": {
|
|
121
|
-
"build": "vite build",
|
|
122
|
-
"prepublishOnly": "pnpm run build",
|
|
123
|
-
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
124
|
-
"test": "vitest run --passWithNoTests",
|
|
125
|
-
"typecheck": "tsc --noEmit"
|
|
126
|
-
},
|
|
127
70
|
"peerDependencies": {
|
|
128
71
|
"@carbon/icons-react": "^11.71.0",
|
|
129
72
|
"@carbon/react": "^1.97.0",
|
|
130
|
-
"@isi-ui7/corporate-themes": ">=0.2.
|
|
131
|
-
"@isi-ui7/data-table": ">=0.2.
|
|
73
|
+
"@isi-ui7/corporate-themes": ">=0.2.7",
|
|
74
|
+
"@isi-ui7/data-table": ">=0.2.5",
|
|
132
75
|
"@isi-ui7/editable-table": ">=0.2.3",
|
|
133
76
|
"@isi-ui7/i18n": ">=0.2.3",
|
|
134
77
|
"@isi-ui7/lookup-input": ">=0.2.4",
|
|
@@ -144,14 +87,6 @@
|
|
|
144
87
|
"@carbon/react": "^1.97.0",
|
|
145
88
|
"@eslint/eslintrc": "^3.3.1",
|
|
146
89
|
"@eslint/js": "^9.39.0",
|
|
147
|
-
"@isi-ui7/corporate-themes": "workspace:*",
|
|
148
|
-
"@isi-ui7/data-table": "workspace:*",
|
|
149
|
-
"@isi-ui7/editable-table": "workspace:*",
|
|
150
|
-
"@isi-ui7/i18n": "workspace:*",
|
|
151
|
-
"@isi-ui7/lookup-input": "workspace:*",
|
|
152
|
-
"@isi-ui7/modal-manager": "workspace:*",
|
|
153
|
-
"@isi-ui7/realtime": "workspace:*",
|
|
154
|
-
"@isi-ui7/ui-shell": "workspace:*",
|
|
155
90
|
"@testing-library/dom": "^10.0.0",
|
|
156
91
|
"@testing-library/react": "^16.0.0",
|
|
157
92
|
"@testing-library/user-event": "^14.5.0",
|
|
@@ -170,14 +105,19 @@
|
|
|
170
105
|
"typescript": "^5.3.3",
|
|
171
106
|
"vite": "^5.0.0",
|
|
172
107
|
"vite-plugin-dts": "^3.6.0",
|
|
173
|
-
"vitest": "^1.0.0"
|
|
108
|
+
"vitest": "^1.0.0",
|
|
109
|
+
"@isi-ui7/corporate-themes": "0.2.7",
|
|
110
|
+
"@isi-ui7/data-table": "0.2.5",
|
|
111
|
+
"@isi-ui7/editable-table": "0.2.3",
|
|
112
|
+
"@isi-ui7/i18n": "0.2.3",
|
|
113
|
+
"@isi-ui7/lookup-input": "0.2.4",
|
|
114
|
+
"@isi-ui7/modal-manager": "0.2.2",
|
|
115
|
+
"@isi-ui7/ui-shell": "0.2.8",
|
|
116
|
+
"@isi-ui7/realtime": "0.2.3"
|
|
174
117
|
},
|
|
175
118
|
"dependencies": {
|
|
176
119
|
"@carbon/icons-react": "^11.71.0",
|
|
177
120
|
"@carbon/react": "^1.97.0",
|
|
178
|
-
"@isi-ui7/data-table": "workspace:*",
|
|
179
|
-
"@isi-ui7/editable-table": "workspace:*",
|
|
180
|
-
"@isi-ui7/lookup-input": "workspace:*",
|
|
181
121
|
"@opentelemetry/auto-instrumentations-node": "^0.57.0",
|
|
182
122
|
"@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
|
|
183
123
|
"@opentelemetry/sdk-node": "^0.57.0",
|
|
@@ -185,6 +125,15 @@
|
|
|
185
125
|
"exceljs": "^4.4.0",
|
|
186
126
|
"jose": "^5.9.6",
|
|
187
127
|
"jspdf": "^4.2.1",
|
|
188
|
-
"jspdf-autotable": "^5.0.7"
|
|
128
|
+
"jspdf-autotable": "^5.0.7",
|
|
129
|
+
"@isi-ui7/data-table": "0.2.5",
|
|
130
|
+
"@isi-ui7/editable-table": "0.2.3",
|
|
131
|
+
"@isi-ui7/lookup-input": "0.2.4"
|
|
132
|
+
},
|
|
133
|
+
"scripts": {
|
|
134
|
+
"build": "vite build",
|
|
135
|
+
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
136
|
+
"test": "vitest run --passWithNoTests",
|
|
137
|
+
"typecheck": "tsc --noEmit"
|
|
189
138
|
}
|
|
190
|
-
}
|
|
139
|
+
}
|
package/src/crud-components.tsx
CHANGED
|
@@ -10,8 +10,6 @@ import {
|
|
|
10
10
|
ModalBody,
|
|
11
11
|
ModalFooter,
|
|
12
12
|
ModalHeader,
|
|
13
|
-
ProgressIndicator,
|
|
14
|
-
ProgressStep,
|
|
15
13
|
Select,
|
|
16
14
|
SelectItem,
|
|
17
15
|
TextArea,
|
|
@@ -489,6 +487,34 @@ export function CrudSchemaPage<TData extends Record<string, unknown>>({
|
|
|
489
487
|
|
|
490
488
|
const layout = schema.layout;
|
|
491
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Wizard adalah alat bantu PENGISIAN, bukan cara menyusun halaman.
|
|
492
|
+
*
|
|
493
|
+
* Ia ada untuk memandu petugas melalui formulir panjang yang belum pernah
|
|
494
|
+
* diisi, dan — pada registrasi nasabah — untuk MENAHAN langkah berikutnya
|
|
495
|
+
* sampai gerbang `canLeavePage` terpenuhi. Kedua alasan itu hanya berlaku
|
|
496
|
+
* saat data DIBUAT.
|
|
497
|
+
*
|
|
498
|
+
* Pada `edit` dan `view` datanya sudah ada dan sudah lolos gerbangnya.
|
|
499
|
+
* Memaksa operator melangkah lima kali untuk membaca satu field di langkah
|
|
500
|
+
* 5 bukan panduan, ia penghalang — dan pada `view` bahkan tidak bisa
|
|
501
|
+
* dilewati: footer mode-view sengaja nol tombol Lanjut, jadi satu-satunya
|
|
502
|
+
* jalan maju adalah menebak bahwa stepper-nya bisa diklik.
|
|
503
|
+
*
|
|
504
|
+
* Karena itu di luar `create` wizard dirender DATAR: seluruh halaman
|
|
505
|
+
* berurutan dalam satu aliran, tanpa stepper dan tanpa Lanjut/Kembali.
|
|
506
|
+
* Yang HILANG hanya navigasinya; isinya tidak ada yang dibuang.
|
|
507
|
+
*
|
|
508
|
+
* Aman terhadap validasi: `handleSave` memakai `validateAll()`, yang sudah
|
|
509
|
+
* mengumpulkan field dari SELURUH halaman tanpa peduli halaman aktif —
|
|
510
|
+
* jadi menyimpan dari bentuk datar memeriksa hal yang sama persis.
|
|
511
|
+
*
|
|
512
|
+
* Keputusan PM 2026-08-19. Sengaja dipasang di sini, BUKAN di skema tiap
|
|
513
|
+
* app: ini keputusan tentang cara me-render sebuah layout, dan menaruhnya
|
|
514
|
+
* di skema berarti tiap app harus mengingat untuk mengulanginya.
|
|
515
|
+
*/
|
|
516
|
+
const wizardNav = layout.type === "wizard" && mode === "create";
|
|
517
|
+
|
|
492
518
|
const renderBody = () => {
|
|
493
519
|
if (layout.type === "single-page") {
|
|
494
520
|
return (
|
|
@@ -570,6 +596,76 @@ export function CrudSchemaPage<TData extends Record<string, unknown>>({
|
|
|
570
596
|
);
|
|
571
597
|
}
|
|
572
598
|
|
|
599
|
+
// Wizard di luar `create` — datar, tanpa stepper. Lihat `wizardNav`.
|
|
600
|
+
//
|
|
601
|
+
// Syaratnya ditulis LENGKAP (`layout.type === "wizard" && !wizardNav`),
|
|
602
|
+
// bukan `!wizardNav` saja. Keduanya setara hari ini, tapi bentuk pendek
|
|
603
|
+
// benar hanya karena `single-page` dan `tabs` sudah return di atas — ia
|
|
604
|
+
// cabang *sisa*, bukan cabang *wizard*. Varian layout ke-5 yang lupa
|
|
605
|
+
// ditaruh di atas sini akan ikut dirender datar tanpa galat, tanpa lint,
|
|
606
|
+
// tanpa uji yang menahan. Dengan bentuk lengkap, kelalaian yang sama
|
|
607
|
+
// menjadi galat `tsc` di `layout.pages` beberapa baris di bawah.
|
|
608
|
+
if (layout.type === "wizard" && !wizardNav) {
|
|
609
|
+
return (
|
|
610
|
+
<PageBody>
|
|
611
|
+
{errBanner}
|
|
612
|
+
{layout.pages.map((pg, i) => (
|
|
613
|
+
<div key={i} style={{ marginBottom: "2.5rem" }}>
|
|
614
|
+
{/* Label halaman tetap dirender — sebagai JUDUL KELOMPOK, bukan
|
|
615
|
+
navigasi. Membuang steppernya tidak boleh ikut membuang satu-
|
|
616
|
+
satunya tempat `pg.label`/`pg.sublabel` pernah tampil: pada
|
|
617
|
+
form 5 langkah, hasilnya 13 panel beruntun tanpa satu pun
|
|
618
|
+
pemisah tingkat-halaman. Yang dilarang PM adalah navigasinya,
|
|
619
|
+
bukan judulnya. */}
|
|
620
|
+
<div
|
|
621
|
+
style={{
|
|
622
|
+
marginBottom: "1rem",
|
|
623
|
+
paddingBottom: "0.5rem",
|
|
624
|
+
borderBottom: "1px solid var(--cds-border-subtle-01, #e0e0e0)",
|
|
625
|
+
}}
|
|
626
|
+
>
|
|
627
|
+
<div
|
|
628
|
+
style={{
|
|
629
|
+
fontSize: "1rem",
|
|
630
|
+
fontWeight: 600,
|
|
631
|
+
color: "var(--cds-text-primary, #161616)",
|
|
632
|
+
}}
|
|
633
|
+
>
|
|
634
|
+
{pg.label}
|
|
635
|
+
</div>
|
|
636
|
+
{pg.sublabel ? (
|
|
637
|
+
<div
|
|
638
|
+
style={{
|
|
639
|
+
fontSize: "0.75rem",
|
|
640
|
+
color: "var(--cds-text-secondary, #525252)",
|
|
641
|
+
marginTop: "0.125rem",
|
|
642
|
+
}}
|
|
643
|
+
>
|
|
644
|
+
{pg.sublabel}
|
|
645
|
+
</div>
|
|
646
|
+
) : null}
|
|
647
|
+
</div>
|
|
648
|
+
<SchemaFormRenderer
|
|
649
|
+
mode={mode}
|
|
650
|
+
value={form}
|
|
651
|
+
sections={pg.sections}
|
|
652
|
+
errors={fieldErrors}
|
|
653
|
+
disabled={saving}
|
|
654
|
+
onChange={setForm}
|
|
655
|
+
density={density}
|
|
656
|
+
// `width` apa adanya, BUKAN `width ?? "full"` seperti cabang
|
|
657
|
+
// wizard. `full` di sana berarti "penuhi kolom KIRI", jalan
|
|
658
|
+
// keluar untuk ruang yang dipersempit stepper. Di sini nol
|
|
659
|
+
// stepper, jadi alasannya hilang dan default kontrak
|
|
660
|
+
// ("two-thirds") kembali berlaku.
|
|
661
|
+
width={width}
|
|
662
|
+
/>
|
|
663
|
+
</div>
|
|
664
|
+
))}
|
|
665
|
+
</PageBody>
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
573
669
|
const currentPage = layout.pages[page];
|
|
574
670
|
return (
|
|
575
671
|
<PageBody>
|
|
@@ -640,7 +736,9 @@ export function CrudSchemaPage<TData extends Record<string, unknown>>({
|
|
|
640
736
|
);
|
|
641
737
|
}
|
|
642
738
|
|
|
643
|
-
|
|
739
|
+
// Lanjut/Kembali hanya saat wizard benar-benar memandu — lihat `wizardNav`.
|
|
740
|
+
// Di `edit`, footer jatuh ke bentuk baku: satu tombol Simpan.
|
|
741
|
+
if (wizardNav) {
|
|
644
742
|
return (
|
|
645
743
|
<PageFooter spread>
|
|
646
744
|
<Button kind="secondary" size="sm" onClick={onBack} disabled={saving}>
|
|
@@ -657,7 +755,13 @@ export function CrudSchemaPage<TData extends Record<string, unknown>>({
|
|
|
657
755
|
kind="primary"
|
|
658
756
|
size="sm"
|
|
659
757
|
onClick={handleSave}
|
|
660
|
-
|
|
758
|
+
// Nol penjaga `isDirty` di sini: blok ini kini hanya terjangkau
|
|
759
|
+
// saat `create` (lihat `wizardNav`), dan form baru yang belum
|
|
760
|
+
// disentuh memang belum "kotor" — memakai penjaga edit di sini
|
|
761
|
+
// akan mematikan tombol Simpan pada satu-satunya mode yang
|
|
762
|
+
// masih memakainya. tsc menandainya sebagai perbandingan yang
|
|
763
|
+
// tak mungkin benar; ia benar.
|
|
764
|
+
disabled={saving}
|
|
661
765
|
>
|
|
662
766
|
{saving ? labels.saving : submitLabel}
|
|
663
767
|
</Button>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Tambalan lingkungan jsdom — BUKAN tambalan produk.
|
|
2
|
+
//
|
|
3
|
+
// jsdom nol mengimplementasikan `Element.prototype.scrollIntoView`. Carbon
|
|
4
|
+
// `ComposedModal` memanggilnya saat memindahkan fokus, jadi setiap uji yang
|
|
5
|
+
// membuka modal melempar `TypeError` di luar rantai promise uji. Vitest
|
|
6
|
+
// menghitungnya sebagai *unhandled error*, bukan sebagai uji gagal — hasilnya
|
|
7
|
+
// `Tests 108 passed` berdampingan dengan `Errors 11` dan **exit code 1**.
|
|
8
|
+
//
|
|
9
|
+
// 🔑 Itu bentuk kegagalan yang paling mudah dilaporkan keliru: ringkasan yang
|
|
10
|
+
// dibaca manusia berbunyi hijau sepenuhnya, sementara gerbang CI yang membaca
|
|
11
|
+
// exit code menolak paket ini. Sesi yang menambahkan tambalan ini sempat
|
|
12
|
+
// melaporkan "105/105 lulus" atas suite yang MERAH, dan baru tahu setelah
|
|
13
|
+
// verifikasi adversarial memeriksa exit code-nya secara terpisah.
|
|
14
|
+
//
|
|
15
|
+
// Fungsinya no-op: yang diuji adalah apa yang terender, bukan apakah browser
|
|
16
|
+
// menggulir. Menambal di lapisan lingkungan menjaga kode produk tetap bersih
|
|
17
|
+
// dari pengetahuan tentang jsdom.
|
|
18
|
+
if (typeof Element !== "undefined" && !Element.prototype.scrollIntoView) {
|
|
19
|
+
Element.prototype.scrollIntoView = function scrollIntoView() {
|
|
20
|
+
/* no-op: jsdom nol punya viewport untuk digulir */
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
// Dimock supaya mode `edit`/`view` tidak menggantung di InlineLoading: tanpa
|
|
5
|
+
// `loadApiPath` yang selesai, `loading` tidak pernah kembali false dan yang
|
|
6
|
+
// terender hanya spinner — uji akan "lulus" karena stepper-nya memang belum
|
|
7
|
+
// sempat dirender, bukan karena perbaikannya bekerja. Itu lulus palsu.
|
|
8
|
+
vi.mock("./workflow/crud-client", () => ({
|
|
9
|
+
fetchDetailData: vi.fn(async () => ({ f1: "satu", f2: "dua", f3: "tiga" })),
|
|
10
|
+
submitWorkflowForm: vi.fn(async () => "wf-1"),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import { CrudSchemaPage } from "./crud-components";
|
|
14
|
+
import type { CrudForm } from "./form-types";
|
|
15
|
+
|
|
16
|
+
type D = { f1: string; f2: string; f3: string };
|
|
17
|
+
|
|
18
|
+
const schema: CrudForm<D> = {
|
|
19
|
+
title: { create: "Baru", edit: "Ubah", view: "Lihat" },
|
|
20
|
+
emptyData: { f1: "", f2: "", f3: "" },
|
|
21
|
+
createApiPath: "/api/x/create",
|
|
22
|
+
updateApiPath: () => "/api/x/1",
|
|
23
|
+
loadApiPath: () => "/api/x/1",
|
|
24
|
+
layout: {
|
|
25
|
+
type: "wizard",
|
|
26
|
+
pages: [
|
|
27
|
+
{ label: "Langkah Satu", sublabel: "Sub Satu", sections: [{ title: "S1", fields: [{ key: "f1", label: "Field Satu", type: "text", span: 4 }] }] },
|
|
28
|
+
{ label: "Langkah Dua", sublabel: "Sub Dua", sections: [{ title: "S2", fields: [{ key: "f2", label: "Field Dua", type: "text", span: 4 }] }] },
|
|
29
|
+
{ label: "Langkah Tiga", sublabel: "Sub Tiga", sections: [{ title: "S3", fields: [{ key: "f3", label: "Field Tiga", type: "text", span: 4 }] }] },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Stepper dirender sebagai <nav aria-label=…> — satu-satunya landmark navigasi
|
|
35
|
+
// di halaman ini, jadi role "navigation" cukup untuk membedakannya.
|
|
36
|
+
const stepper = () => screen.queryByRole("navigation");
|
|
37
|
+
|
|
38
|
+
// Pada `view` field dirender sebagai nilai baca-saja, BUKAN input berlabel —
|
|
39
|
+
// jadi getByLabelText hanya bekerja di create/edit. Query teks label bekerja di
|
|
40
|
+
// ketiga mode, sehingga uji ini membandingkan hal yang sama di semua mode.
|
|
41
|
+
const fieldTampil = (nama: string) => screen.queryByText(nama) !== null;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("navigasi wizard hanya pada mode create", () => {
|
|
48
|
+
it("create — stepper TAMPIL dan hanya halaman aktif yang dirender", async () => {
|
|
49
|
+
render(<CrudSchemaPage schema={schema} mode="create" />);
|
|
50
|
+
|
|
51
|
+
expect(stepper(), "stepper harus ada saat membuat data baru").not.toBeNull();
|
|
52
|
+
expect(screen.getByText("Langkah Tiga")).toBeTruthy(); // ada di stepper…
|
|
53
|
+
expect(fieldTampil("Field Satu")).toBe(true);
|
|
54
|
+
// …tapi field-nya belum, karena halaman 3 belum dibuka.
|
|
55
|
+
expect(fieldTampil("Field Tiga")).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("view — stepper TIDAK ada, dan SELURUH halaman terbaca sekaligus", async () => {
|
|
59
|
+
render(<CrudSchemaPage schema={schema} mode="view" />);
|
|
60
|
+
await waitFor(() => expect(fieldTampil("Field Satu")).toBe(true));
|
|
61
|
+
|
|
62
|
+
expect(stepper(), "view tidak boleh punya navigasi langkah").toBeNull();
|
|
63
|
+
// Inti perbaikannya: nol navigasi TIDAK boleh berarti isi yang hilang.
|
|
64
|
+
// Sebelum ini, `view` merender satu halaman saja sementara footernya nol
|
|
65
|
+
// tombol Lanjut — jadi langkah 2-5 tidak bisa dibaca sama sekali.
|
|
66
|
+
for (const label of ["Field Satu", "Field Dua", "Field Tiga"]) {
|
|
67
|
+
expect(fieldTampil(label), `${label} harus terbaca di view`).toBe(true);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Ditambahkan setelah verifikasi adversarial membantah klaim "nol isi yang
|
|
72
|
+
// hilang": versi pertama perbaikan ini me-`flatMap` seluruh section dan
|
|
73
|
+
// membuang `pg.label`/`pg.sublabel` — 10 string konkret lenyap dari layar
|
|
74
|
+
// perorangan, dan uji versi pertama nol memeriksanya karena ia hanya
|
|
75
|
+
// menanyakan field dan stepper.
|
|
76
|
+
it.each(["view", "edit"] as const)(
|
|
77
|
+
"%s — label & sublabel halaman TETAP terbaca sebagai judul kelompok",
|
|
78
|
+
async (mode) => {
|
|
79
|
+
render(<CrudSchemaPage schema={schema} mode={mode} />);
|
|
80
|
+
await waitFor(() => expect(fieldTampil("Field Satu")).toBe(true));
|
|
81
|
+
|
|
82
|
+
expect(stepper(), "judul kelompok bukan navigasi").toBeNull();
|
|
83
|
+
for (const l of ["Langkah Satu", "Langkah Dua", "Langkah Tiga"]) {
|
|
84
|
+
expect(fieldTampil(l), `${l} hilang dari ${mode}`).toBe(true);
|
|
85
|
+
}
|
|
86
|
+
for (const s of ["Sub Satu", "Sub Dua", "Sub Tiga"]) {
|
|
87
|
+
expect(fieldTampil(s), `sublabel ${s} hilang dari ${mode}`).toBe(true);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
it("edit — stepper TIDAK ada, seluruh halaman terbaca, nol tombol Lanjut", async () => {
|
|
93
|
+
render(<CrudSchemaPage schema={schema} mode="edit" />);
|
|
94
|
+
await waitFor(() => expect(fieldTampil("Field Satu")).toBe(true));
|
|
95
|
+
|
|
96
|
+
expect(stepper(), "edit tidak boleh punya navigasi langkah").toBeNull();
|
|
97
|
+
expect(fieldTampil("Field Tiga")).toBe(true);
|
|
98
|
+
|
|
99
|
+
const tombol = screen.getAllByRole("button").map((b) => b.textContent ?? "");
|
|
100
|
+
expect(tombol.some((t) => /lanjut|next/i.test(t)), `masih ada tombol Lanjut: ${tombol.join("|")}`).toBe(false);
|
|
101
|
+
expect(tombol.some((t) => /kembali|prev/i.test(t)), `masih ada tombol Kembali: ${tombol.join("|")}`).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Kontrol kedua, dan ini yang penting: `tabs` juga punya `pages`, jadi ia
|
|
105
|
+
// kandidat paling mungkin ikut terseret kalau urutan cabang render bergeser.
|
|
106
|
+
// Tanpa kontrol ini, memindahkan cabang datar ke atas cabang `tabs` akan
|
|
107
|
+
// meratakan 4 layar bos7-enterprise/bos7-financing tanpa satu pun galat.
|
|
108
|
+
it("kontrol negatif — layout tabs tetap bertab, tidak ikut diratakan", async () => {
|
|
109
|
+
const tabs: CrudForm<D> = {
|
|
110
|
+
...schema,
|
|
111
|
+
layout: {
|
|
112
|
+
type: "tabs",
|
|
113
|
+
pages: [
|
|
114
|
+
{ label: "Tab Satu", sections: [{ title: "T1", fields: [{ key: "f1", label: "Field Satu", type: "text", span: 4 }] }] },
|
|
115
|
+
{ label: "Tab Dua", sections: [{ title: "T2", fields: [{ key: "f2", label: "Field Dua", type: "text", span: 4 }] }] },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
render(<CrudSchemaPage schema={tabs} mode="view" />);
|
|
120
|
+
await waitFor(() => expect(fieldTampil("Field Satu")).toBe(true));
|
|
121
|
+
|
|
122
|
+
// Tab masih tab: tombol role=tab ada, dan hanya tab aktif yang terlihat.
|
|
123
|
+
expect(screen.getAllByRole("tab").length, "tab hilang — cabang datar menelan layout tabs").toBe(2);
|
|
124
|
+
expect(stepper()).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("kontrol negatif — layout single-page tidak ikut berubah", async () => {
|
|
128
|
+
const single: CrudForm<D> = {
|
|
129
|
+
...schema,
|
|
130
|
+
layout: { type: "single-page", sections: [{ title: "S", fields: [{ key: "f1", label: "Field Satu", type: "text", span: 4 }] }] },
|
|
131
|
+
};
|
|
132
|
+
render(<CrudSchemaPage schema={single} mode="view" />);
|
|
133
|
+
await waitFor(() => expect(fieldTampil("Field Satu")).toBe(true));
|
|
134
|
+
expect(stepper()).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
});
|