null, 'master_kabupaten_kota' => 'provinsi_id', 'master_kecamatan' => 'kabupaten_kota_id', 'master_desa_kelurahan' => 'kecamatan_id', 'master_komoditas' => null, ] as $name => $parent) { Schema::create($name, function (Blueprint $table) use ($parent) { $table->id(); $table->string('nama'); $table->string('kode')->nullable(); $table->boolean('is_utama')->default(true); if ($parent) $table->integer($parent); $table->softDeletes(); }); } Schema::create('survey_kelompok_tani', function (Blueprint $table) { $table->id(); foreach ([ 'uid', 'nama_kelompok', 'nama_responden', 'jabatan_responden', 'kontak_responden', 'dusun', 'jenis_kelompok', 'nama_gapoktan', 'cakupan_wilayah', 'nama_wilayah_cakupan', 'status_legalitas', 'nomor_sk', 'tanggal_tahun_penerbitan_sk', 'frekuensi_pertemuan', 'tanggal_pertemuan_terakhir', 'pembukuan_kas', 'pengelola_kas', 'masalah_pengelolaan_kas_simpan_pinjam', 'deskripsi_masalah_pengelolaan_kas_simpan_pinjam', 'potensi_kelompok', 'alasan_penilaian_potensi', 'konflik_kelompok', 'topik_konflik', 'penyelesaian_konflik', 'partisipasi_perempuan', ] as $field) { $table->string($field)->nullable(); } foreach ([ 'desa_kelurahan_id', 'tahun_berdiri', 'jumlah_pengurus', 'jumlah_pengurus_aktif', 'jumlah_anggota_terdaftar', 'jumlah_anggota_aktif', 'jumlah_pengurus_laki_laki', 'jumlah_pengurus_perempuan', 'jumlah_anggota_laki_laki', 'jumlah_anggota_perempuan', 'jumlah_anggota_pemuda_laki_laki', 'jumlah_anggota_pemuda_perempuan', 'created_by', 'updated_by', 'deleted_by', ] as $field) { $table->integer($field)->nullable(); } $table->json('komoditas_id')->nullable(); $table->json('kegiatan_terlaksana')->nullable(); $table->json('fungsi_kelompok_bagi_anggota')->nullable(); $table->boolean('pernah_mendapatkan_bantuan')->nullable(); $table->timestamps(); $table->softDeletes(); }); Schema::create('survey_pengurus_kelompok_tani', function (Blueprint $table) { $table->id(); $table->string('uid'); $table->integer('survey_kelompok_tani_id'); $table->string('nama_pengurus'); $table->string('jabatan'); $table->string('kontak'); $table->boolean('status'); $table->integer('created_by')->nullable(); $table->integer('updated_by')->nullable(); $table->integer('deleted_by')->nullable(); $table->timestamps(); $table->softDeletes(); }); Schema::create('survey_aset_kelompok_tani', function (Blueprint $table) { $table->id(); $table->string('uid'); $table->integer('survey_kelompok_tani_id'); $table->string('jenis_aset'); $table->boolean('status_aset'); $table->string('status_pengelolaan_aset'); $table->string('kondisi_aset'); $table->integer('created_by')->nullable(); $table->integer('updated_by')->nullable(); $table->integer('deleted_by')->nullable(); $table->timestamps(); $table->softDeletes(); }); Schema::create('survey_bantuan_kelompok_tani', function (Blueprint $table) { $table->id(); $table->string('uid'); $table->integer('survey_kelompok_tani_id'); $table->string('nama_program'); $table->string('penyelenggara'); $table->string('bentuk_bantuan'); $table->integer('tahun')->nullable(); $table->integer('created_by')->nullable(); $table->integer('updated_by')->nullable(); $table->integer('deleted_by')->nullable(); $table->timestamps(); $table->softDeletes(); }); DB::table('master_provinsi')->insert(['id' => 1, 'nama' => 'NTT']); DB::table('master_kabupaten_kota')->insert(['id' => 1, 'nama' => 'Ngada', 'provinsi_id' => 1]); DB::table('master_kecamatan')->insert(['id' => 1, 'nama' => 'Kecamatan A', 'kabupaten_kota_id' => 1]); DB::table('master_desa_kelurahan')->insert(['id' => 1, 'nama' => 'Desa A', 'kecamatan_id' => 1]); $user = new User(['name' => 'Admin Test', 'email' => 'admin@example.test', 'role' => 'admin']); $user->id = 7; $this->be($user); Event::forget('composing: *'); view()->share('errors', new ViewErrorBag()); $this->kelompok = fn (array $attributes = []) => KelompokTani::create([ 'nama_kelompok' => 'Kelompok A', 'nama_responden' => 'Responden A', 'desa_kelurahan_id' => 1, 'jenis_kelompok' => 'Kelompok Tani (Poktan)', ...$attributes, ]); $this->profileRequest = function (array $input = [], string $method = 'PUT') { $request = Request::create('/survey/kelompok-tani/1/edit', $method, $input); $request->setLaravelSession(app('session')->driver()); $this->app->instance('request', $request); return $request; }; $this->profileDom = function ($view) { $dom = new DOMDocument(); $previous = libxml_use_internal_errors(true); $dom->loadHTML($view->render()); libxml_clear_errors(); libxml_use_internal_errors($previous); return new DOMXPath($dom); }; }); test('profile saves identity and membership together on creation and update', function () { $controller = app(SurveyKelompokTaniController::class); $input = [ 'nama_kelompok' => 'Kelompok Baru', 'nama_responden' => 'Budi', 'desa_kelurahan_id' => 1, 'jenis_kelompok' => 'Kelompok Tani (Poktan)', 'jumlah_pengurus' => 4, 'jumlah_pengurus_aktif' => 3, 'jumlah_anggota_terdaftar' => 20, 'jumlah_anggota_aktif' => 18, 'jumlah_pengurus_laki_laki' => 2, 'jumlah_pengurus_perempuan' => 2, 'jumlah_anggota_laki_laki' => 12, 'jumlah_anggota_perempuan' => 8, 'jumlah_anggota_pemuda_laki_laki' => 5, 'jumlah_anggota_pemuda_perempuan' => 3, ]; $locations = ['provinsi_id' => 1, 'kabupaten_id' => 1, 'kecamatan_id' => 1]; $controller->store(($this->profileRequest)([...$input, ...$locations], 'POST')); $group = KelompokTani::firstOrFail(); foreach ($input as $field => $value) expect($group->{$field})->toBe($value); $response = $controller->update(($this->profileRequest)([ ...$input, ...$locations, 'nama_kelompok' => 'Kelompok Diubah', 'jumlah_anggota_terdaftar' => 25, ]), $group); expect($group->refresh()->nama_kelompok)->toBe('Kelompok Diubah') ->and($group->jumlah_anggota_terdaftar)->toBe(25); expect($response->getTargetUrl())->toBe(route('kelompok-tani.edit', $group->id) . '#section-pengurus'); }); test('invalid membership prevents partial profile saves', function () { $group = ($this->kelompok)(['jumlah_anggota_terdaftar' => 20]); $request = ($this->profileRequest)([ 'nama_kelompok' => 'Tidak boleh tersimpan', 'nama_responden' => 'Budi', 'provinsi_id' => 1, 'kabupaten_id' => 1, 'kecamatan_id' => 1, 'desa_kelurahan_id' => 1, 'jenis_kelompok' => 'Kelompok Tani (Poktan)', 'jumlah_anggota_terdaftar' => -1, ]); expect(fn () => app(SurveyKelompokTaniController::class)->update($request, $group)) ->toThrow(ValidationException::class); expect($group->refresh()->nama_kelompok)->toBe('Kelompok A') ->and($group->jumlah_anggota_terdaftar)->toBe(20); }); test('membership fields belong to the profile form on create and edit', function () { $controller = app(SurveyKelompokTaniController::class); ($this->profileRequest)([], 'GET'); $create = ($this->profileDom)($controller->create()); expect($create->query('//form[@id="addKelompokTaniForm"]//input[@name="jumlah_anggota_terdaftar"]')->length)->toBe(1); $group = ($this->kelompok)(['jumlah_anggota_terdaftar' => 20]); $group->pengurus()->create([ 'uid' => 'PKT-TEST', 'nama_pengurus' => 'Ketua A', 'jabatan' => 'Ketua', 'kontak' => '08123456789', 'status' => true, ]); $edit = ($this->profileDom)($controller->edit(($this->profileRequest)([], 'GET'), $group)); expect($edit->query('//form[@id="editKelompokTaniForm"]//input[@name="jumlah_anggota_terdaftar" and @value="20"]')->length)->toBe(1); expect($edit->query('//div[@id="section-pengurus"]//input[@value="Ketua A"]')->length)->toBe(1); expect($edit->query('//form[contains(@class,"form-repeater-pengurus")]')->length)->toBe(1); expect($edit->query('//ul[contains(@class,"nav-pills")]//a[normalize-space()="Keanggotaan"]')->length)->toBe(0); }); test('SK fields render enabled only for statuses with an existing SK', function (?string $status, bool $enabled) { $group = ($this->kelompok)(['status_legalitas' => $status]); $view = app(SurveyKelompokTaniController::class)->edit( ($this->profileRequest)(['section' => 'kelembagaan'], 'GET'), $group ); $dom = ($this->profileDom)($view); foreach (['klb-nomor-sk', 'klb-tanggal-penerbitan-sk'] as $id) { expect($dom->query('//input[@id="' . $id . '"]')->item(0)->hasAttribute('disabled'))->toBe(!$enabled); } expect($dom->query('//label[@for="klb-nomor-sk"]')->item(0)->textContent)->toBe('Nomor SK'); })->with([ [null, false], ['Belum ada SK/dokumen apa pun', false], ['Sedang proses pengajuan', false], ['Tidak tahu', false], ['Sudah ada SK dari desa/kecamatan', true], ['Sudah ada SK dan terdaftar di Dinas/Kabupaten', true], ]); test('inactive SK input is ignored and existing values survive status changes', function () { $group = ($this->kelompok)(['nomor_sk' => 'SK-LAMA', 'tanggal_tahun_penerbitan_sk' => '2020']); $controller = app(SurveyKelompokTaniController::class); $controller->update(($this->profileRequest)([ 'section' => 'kelembagaan', 'status_legalitas' => 'Tidak tahu', 'nomor_sk' => ['invalid'], 'tanggal_tahun_penerbitan_sk' => str_repeat('x', 100), ]), $group); expect($group->refresh()->nomor_sk)->toBe('SK-LAMA') ->and($group->tanggal_tahun_penerbitan_sk)->toBe('2020'); $controller->update(($this->profileRequest)([ 'section' => 'kelembagaan', 'status_legalitas' => 'Sudah ada SK dari desa/kecamatan', 'nomor_sk' => 'SK-BARU', 'tanggal_tahun_penerbitan_sk' => '2026', ]), $group); expect($group->refresh()->nomor_sk)->toBe('SK-BARU') ->and($group->tanggal_tahun_penerbitan_sk)->toBe('2026'); }); test('pengurus actions and the former membership URL return to profile', function () { $group = ($this->kelompok)(); $controller = app(PengurusKelompokTaniController::class); $response = $controller->index($group); expect($response->getTargetUrl())->toBe(route('kelompok-tani.edit', $group->id) . '#section-ringkasan'); $response = $controller->store(($this->profileRequest)(['pengurus' => [[ 'nama_pengurus' => 'Ketua A', 'jabatan' => 'Ketua', 'kontak' => '08123456789', 'status' => 1, ]]], 'POST'), $group); expect($response->getTargetUrl())->toBe(route('kelompok-tani.edit', $group->id) . '#section-pengurus'); $pengurus = $group->pengurus()->firstOrFail(); $response = $controller->destroy($group, $pengurus); expect($response->getTargetUrl())->toBe(route('kelompok-tani.edit', $group->id) . '#section-pengurus'); expect($group->pengurus()->count())->toBe(0); }); test('meeting fields are shown in Kegiatan with existing values and removed from Kelembagaan', function () { $group = ($this->kelompok)([ 'frekuensi_pertemuan' => '1x per bulan', 'tanggal_pertemuan_terakhir' => '2026-10-20', ]); $controller = app(SurveyKelompokTaniController::class); $kelembagaan = ($this->profileDom)($controller->edit( ($this->profileRequest)(['section' => 'kelembagaan'], 'GET'), $group )); expect($kelembagaan->query('//select[@name="frekuensi_pertemuan"] | //input[@name="tanggal_pertemuan_terakhir"]')->length)->toBe(0); $aktivitas = ($this->profileDom)($controller->edit( ($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET'), $group )); expect($aktivitas->query('//form//select[@name="frekuensi_pertemuan"]/option[@selected]')->item(0)->getAttribute('value'))->toBe('1x per bulan'); expect($aktivitas->query('//form//input[@name="tanggal_pertemuan_terakhir"]')->item(0)->getAttribute('value'))->toBe('20-10-2026'); expect($aktivitas->query('//ul[contains(@class,"nav-pills")]//a[normalize-space()="Operasional"]')->length)->toBe(1); expect($aktivitas->query('//input[@name="tanggal_pertemuan_terakhir"]')->item(0)->hasAttribute('placeholder'))->toBeFalse(); }); test('Kegiatan saves meeting details and rejects invalid dates without partial updates', function () { $group = ($this->kelompok)(); $controller = app(SurveyKelompokTaniController::class); $response = $controller->update(($this->profileRequest)([ 'section' => 'aktivitas-fungsi', 'frekuensi_pertemuan' => '1x per bulan', 'tanggal_pertemuan_terakhir' => '20-10-2026', 'kegiatan_terlaksana' => ['Pertemuan/diskusi rutin'], ]), $group); expect($group->refresh()->frekuensi_pertemuan)->toBe('1x per bulan') ->and($group->tanggal_pertemuan_terakhir->format('Y-m-d'))->toBe('2026-10-20') ->and($group->kegiatan_terlaksana)->toBe(['Pertemuan/diskusi rutin']); expect($response->getTargetUrl())->toBe(route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi#section-aset-baku'); $invalid = ($this->profileRequest)([ 'section' => 'aktivitas-fungsi', 'frekuensi_pertemuan' => 'Tidak rutin/tidak terjadwal', 'tanggal_pertemuan_terakhir' => 'invalid-date', ]); expect(fn () => $controller->update($invalid, $group))->toThrow(ValidationException::class); expect($group->refresh()->frekuensi_pertemuan)->toBe('1x per bulan') ->and($group->tanggal_pertemuan_terakhir->format('Y-m-d'))->toBe('2026-10-20'); $controller->update(($this->profileRequest)([ 'section' => 'kelembagaan', 'cakupan_wilayah' => 'Desa', 'frekuensi_pertemuan' => 'Diabaikan', 'tanggal_pertemuan_terakhir' => 'invalid-date', ]), $group); expect($group->refresh()->cakupan_wilayah)->toBe('Desa') ->and($group->frekuensi_pertemuan)->toBe('1x per bulan'); }); test('all group tabs show Simpan with a check and Kembali to the group list', function () { ($this->profileRequest)([], 'GET'); $group = ($this->kelompok)(); foreach (['pengurus', 'aset', 'bantuan'] as $relation) $group->setRelation($relation, collect()); foreach (['add', 'edit', 'kelembagaan', 'aktivitas-fungsi', 'sosial-konflik'] as $template) { $dom = ($this->profileDom)(view('produktivitas.survey-kelompok-tani.' . $template, [ 'kelompokTani' => $template === 'add' ? null : $group, 'provinsi' => collect(), 'komoditas' => collect(), ])); $buttons = $dom->query('//div[contains(@class,"kelompok-tani-page")]//button[@type="submit" and contains(@class,"btn-success")]'); expect($buttons->length)->toBeGreaterThan(0); foreach ($buttons as $button) { $form = $dom->query('ancestor::form', $button)->item(0); expect($form->hasAttribute('novalidate'))->toBeTrue(); expect(trim($button->textContent))->toBe('Simpan'); expect($dom->query('.//i[contains(@class,"tabler-check")]', $button)->length)->toBe(1); $back = $dom->query('./a[normalize-space()="Kembali"]', $button->parentNode); expect($back->length)->toBe(1); expect($back->item(0)->getAttribute('href'))->toBe(route('kelompok-tani.index')); } } }); test('flash notifications appear once above the group summary regardless of section', function (string $key) { ($this->profileRequest)([], 'GET'); session()->flash($key, 'Pesan pemeriksaan'); session()->flash($key . '_section', 'kas'); $group = ($this->kelompok)(); $group->setRelation('pengurus', collect()); $dom = ($this->profileDom)(view('produktivitas.survey-kelompok-tani.edit', [ 'kelompokTani' => $group, 'provinsi' => collect(), 'komoditas' => collect(), ])); expect($dom->query('//div[@id="kelompok-tani-notifications"]')->length)->toBe(1); expect($dom->query('//div[@id="kelompok-tani-summary"]/preceding-sibling::div[@id="kelompok-tani-notifications"]')->length)->toBe(1); expect($dom->query('//div[@id="kelompok-tani-notifications"]//div[@role="alert"]')->length)->toBe(1); expect($dom->query('//div[@id="kelompok-tani-notifications"]')->item(0)->textContent)->toContain('Pesan pemeriksaan'); })->with(['success', 'error', 'warning', 'info', 'status', 'message']); test('validation errors are summarized above the group summary', function () { ($this->profileRequest)([], 'GET'); $errors = new ViewErrorBag(); $errors->put('default', new Illuminate\Support\MessageBag(['nomor_sk' => 'Nomor SK tidak valid.'])); view()->share('errors', $errors); $dom = ($this->profileDom)(view('produktivitas.survey-kelompok-tani.kelembagaan', [ 'kelompokTani' => ($this->kelompok)(), ])); expect($dom->query('//div[@id="kelompok-tani-summary"]/preceding-sibling::div[@id="kelompok-tani-notifications"]')->length)->toBe(1); expect($dom->query('//div[@id="kelompok-tani-notifications"]//li')->item(0)->textContent)->toBe('Nomor SK tidak valid.'); }); test('one asset submission saves standard and additional assets and skips empty rows', function () { $group = ($this->kelompok)(); $controller = app(AsetKelompokTaniController::class); $response = $controller->update(($this->profileRequest)([ 'aset' => [[ 'jenis_aset' => 'Timbangan', 'status_aset' => 1, 'status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => 'Berfungsi', ]], 'aset_lainnya' => [ ['jenis_aset' => 'Mesin pengering', 'status_pengelolaan_aset' => 'Swadaya', 'kondisi_aset' => 'Berfungsi'], ['jenis_aset' => '', 'status_pengelolaan_aset' => '', 'kondisi_aset' => ''], ], ]), $group); expect($group->aset()->count())->toBe(2); expect($group->aset()->where('jenis_aset', 'Timbangan')->firstOrFail()->status_pengelolaan_aset)->toBe('Hibah'); expect($group->aset()->where('jenis_aset', 'Mesin pengering')->firstOrFail()->kondisi_aset)->toBe('Berfungsi'); expect($response->getTargetUrl())->toBe(route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi#section-status-bantuan'); }); test('invalid additional assets prevent standard assets from being changed', function () { $group = ($this->kelompok)(); $existing = $group->aset()->create([ 'uid' => 'AKT-TEST', 'jenis_aset' => 'Timbangan', 'status_aset' => true, 'status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => 'Berfungsi', ]); $request = ($this->profileRequest)([ 'aset' => [['jenis_aset' => 'Timbangan', 'status_aset' => 0]], 'aset_lainnya' => [['jenis_aset' => 'Timbangan']], ]); expect(fn () => app(AsetKelompokTaniController::class)->update($request, $group))->toThrow(ValidationException::class); expect($existing->fresh()->trashed())->toBeFalse(); expect($group->aset()->count())->toBe(1); }); test('combined asset save restores and removes standard assets while keeping additional assets', function () { $group = ($this->kelompok)(); $controller = app(AsetKelompokTaniController::class); $existing = $group->aset()->create([ 'uid' => 'AKT-TEST', 'jenis_aset' => 'Timbangan', 'status_aset' => true, 'status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => 'Berfungsi', ]); $existing->delete(); $controller->update(($this->profileRequest)([ 'aset' => [[ 'jenis_aset' => 'Timbangan', 'status_aset' => 1, 'status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => 'Berfungsi', ]], 'aset_lainnya' => [['jenis_aset' => 'Mesin pengering']], ]), $group); expect($existing->fresh()->trashed())->toBeFalse(); expect($group->aset()->withTrashed()->where('jenis_aset', 'Timbangan')->count())->toBe(1); $controller->update(($this->profileRequest)([ 'aset' => [['jenis_aset' => 'Timbangan', 'status_aset' => 0]], ]), $group); expect($existing->fresh()->trashed())->toBeTrue(); expect($group->aset()->pluck('jenis_aset')->all())->toBe(['Mesin pengering']); }); test('asset save rolls back standard changes if additional asset creation fails', function () { $group = ($this->kelompok)(); $existing = $group->aset()->create([ 'uid' => 'AKT-TEST', 'jenis_aset' => 'Timbangan', 'status_aset' => true, 'status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => 'Berfungsi', ]); Event::listen('eloquent.creating: ' . AsetKelompokTani::class, function ($asset) { if ($asset->jenis_aset === 'Mesin gagal') throw new RuntimeException('Simulated failure'); }); $request = ($this->profileRequest)([ 'aset' => [['jenis_aset' => 'Timbangan', 'status_aset' => 0]], 'aset_lainnya' => [['jenis_aset' => 'Mesin gagal']], ]); expect(fn () => app(AsetKelompokTaniController::class)->update($request, $group))->toThrow(RuntimeException::class); expect($existing->fresh()->trashed())->toBeFalse(); expect($group->aset()->count())->toBe(1); }); test('asset card has one shared form and preserves inputs after validation errors', function () { $group = ($this->kelompok)(); $existing = $group->aset()->create([ 'uid' => 'AKT-TEST', 'jenis_aset' => 'Mesin lama', 'status_aset' => true, 'status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => 'Berfungsi', ]); ($this->profileRequest)([], 'GET'); session()->flashInput([ 'aset' => [['status_aset' => '1', 'status_pengelolaan_aset' => 'Pinjaman', 'kondisi_aset' => 'Mangkrak']], 'aset_lainnya' => [['jenis_aset' => 'Mesin baru', 'status_pengelolaan_aset' => 'Sewa', 'kondisi_aset' => 'Berfungsi']], ]); $dom = ($this->profileDom)(app(SurveyKelompokTaniController::class)->edit(($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET'), $group)); expect($dom->query('//div[@id="section-aset-baku"]//form[@id="aset-produktif-form"]')->length)->toBe(1); expect($dom->query('//form[@id="aset-produktif-form"]//div[@id="section-aset-lainnya"]')->length)->toBe(1); expect($dom->query('//form[@id="aset-produktif-form"]//button[contains(@class,"btn-success")]')->length)->toBe(1); expect($dom->query('//form[@id="aset-produktif-form"]//form')->length)->toBe(0); expect($dom->query('//form[@id="aset-produktif-form"]//input[@value="Mesin baru"]')->length)->toBe(1); expect($dom->query('//form[@id="aset-produktif-form"]//select[@name="aset[0][status_pengelolaan_aset]"]/option[@selected]')->item(0)->getAttribute('value'))->toBe('Pinjaman'); expect($dom->query('//form[@id="aset-produktif-form"]//select[@name="status_pengelolaan_aset"]/option[@selected]')->item(0)->getAttribute('value'))->toBe('Sewa'); expect($dom->query('//button[@form="delete-aset-' . $existing->id . '"]')->length)->toBe(1); expect($dom->query('//form[@id="delete-aset-' . $existing->id . '"]')->length)->toBe(1); }); test('cash management is visible only when the cash asset is present or selected in old input', function (?bool $savedStatus, bool $deleted, ?string $oldStatus, bool $visible) { $group = ($this->kelompok)(); if ($savedStatus !== null) { $asset = $group->aset()->create([ 'uid' => 'AKT-KAS', 'jenis_aset' => 'Kas/simpanan kelompok', 'status_aset' => $savedStatus, 'status_pengelolaan_aset' => 'Swadaya', 'kondisi_aset' => 'Berfungsi', ]); if ($deleted) $asset->delete(); } ($this->profileRequest)([], 'GET'); if ($oldStatus !== null) { $cashIndex = array_search('Kas/simpanan kelompok', AsetKelompokTaniController::JENIS_ASET_BAKU, true); session()->flashInput(['aset' => [$cashIndex => ['status_aset' => $oldStatus]]]); } $dom = ($this->profileDom)(app(SurveyKelompokTaniController::class)->edit(($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET'), $group)); expect($dom->query('//div[@id="section-kas"]')->item(0)->hasAttribute('hidden'))->toBe(!$visible); expect($dom->query('//tr[@data-aset-kas]//input[@value="1"]')->item(0)->hasAttribute('checked'))->toBe($visible); })->with([ [null, false, null, false], [true, false, null, true], [false, false, null, false], [true, true, null, false], [true, false, '0', false], [null, false, '1', true], ]); test('the combined tab contains activities assets and cash management with one header', function () { $group = ($this->kelompok)(); $group->aset()->create([ 'uid' => 'AKT-KAS', 'jenis_aset' => 'Kas/simpanan kelompok', 'status_aset' => true, 'status_pengelolaan_aset' => 'Swadaya', 'kondisi_aset' => 'Berfungsi', ]); $request = ($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET'); $route = (new Illuminate\Routing\Route('GET', '/survey/kelompok-tani/{kelompokTani}/edit', fn () => null))->name('kelompok-tani.edit'); $request->setRouteResolver(fn () => $route); $dom = ($this->profileDom)(app(SurveyKelompokTaniController::class)->edit($request, $group)); expect($dom->query('//div[@id="kelompok-tani-summary"]')->length)->toBe(1); expect($dom->query('//div[@id="section-aktivitas-fungsi"]')->length)->toBe(1); expect($dom->query('//div[@id="section-aset-baku"]')->length)->toBe(1); expect($dom->query('//div[@id="section-kas"]')->item(0)->hasAttribute('hidden'))->toBeFalse(); expect($dom->query('//div[@id="section-status-bantuan"]')->length)->toBe(1); expect($dom->query('//div[@id="section-penilaian"]')->length)->toBe(0); expect($dom->query('//ul[contains(@class,"nav-pills")]//a[normalize-space()="Bantuan & Penilaian"]')->length)->toBe(0); expect($dom->query('//ul[contains(@class,"nav-pills")]//a[contains(@class,"active") and normalize-space()="Operasional"]')->length)->toBe(1); expect($dom->query('//ul[contains(@class,"nav-pills")]//a[normalize-space()="Aset & Keuangan"]')->length)->toBe(0); }); test('asset actions and cash saves return to the combined tab', function () { $group = ($this->kelompok)(); $controller = app(AsetKelompokTaniController::class); $url = route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi'; expect($controller->index($group)->getTargetUrl())->toBe($url . '#section-aset-baku'); $response = $controller->store(($this->profileRequest)([ 'aset_lainnya' => [['jenis_aset' => 'Mesin pengering']], ], 'POST'), $group); expect($response->getTargetUrl())->toBe($url . '#section-status-bantuan'); $response = $controller->destroy($group, $group->aset()->firstOrFail()); expect($response->getTargetUrl())->toBe($url . '#section-aset-lainnya'); $response = app(SurveyKelompokTaniController::class)->update(($this->profileRequest)([ 'section' => 'kas', 'pengelola_kas' => 'Bendahara kelompok', ]), $group); expect($response->getTargetUrl())->toBe($url . '#section-status-bantuan'); expect($group->refresh()->pengelola_kas)->toBe('Bendahara kelompok'); }); test('present assets require both management status and condition before saving any assets', function ($status) { $group = ($this->kelompok)(); $request = ($this->profileRequest)([ 'aset' => [['jenis_aset' => 'Timbangan', 'status_aset' => $status]], 'aset_lainnya' => [['jenis_aset' => 'Mesin pengering']], ]); try { app(AsetKelompokTaniController::class)->update($request, $group); $this->fail('An asset marked Ada must require management status and condition.'); } catch (ValidationException $exception) { expect(array_keys($exception->errors()))->toBe([ 'aset.0.status_pengelolaan_aset', 'aset.0.kondisi_aset', ]); expect($exception->errors()['aset.0.kondisi_aset'][0])->toBe('Kondisi wajib diisi jika aset dipilih Ada.'); } expect($group->aset()->count())->toBe(0); })->with(['1', 1, true]); test('asset requirements follow each row and reject blank values', function (array $details, string $missingField) { $group = ($this->kelompok)(); $request = ($this->profileRequest)([ 'aset' => [ ['jenis_aset' => 'Timbangan', 'status_aset' => '0'], ['jenis_aset' => 'Gudang/tempat simpan', 'status_aset' => '1', ...$details], ], ]); try { app(AsetKelompokTaniController::class)->update($request, $group); $this->fail('The selected asset has a missing detail.'); } catch (ValidationException $exception) { expect(array_keys($exception->errors()))->toBe(['aset.1.' . $missingField]); } expect($group->aset()->count())->toBe(0); })->with([ [['status_pengelolaan_aset' => '', 'kondisi_aset' => 'Berfungsi'], 'status_pengelolaan_aset'], [['status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => null], 'kondisi_aset'], [['status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => ' '], 'kondisi_aset'], ]); test('asset dropdowns are required only on rows selected Ada', function (bool $present) { $group = ($this->kelompok)(); $group->aset()->create([ 'uid' => 'AKT-TEST', 'jenis_aset' => 'Timbangan', 'status_aset' => $present, 'status_pengelolaan_aset' => 'Hibah', 'kondisi_aset' => 'Berfungsi', ]); $request = ($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET'); $dom = ($this->profileDom)(app(SurveyKelompokTaniController::class)->edit($request, $group)); $index = array_search('Timbangan', AsetKelompokTaniController::JENIS_ASET_BAKU, true); foreach (['status_pengelolaan_aset', 'kondisi_aset'] as $field) { $select = $dom->query('//select[@name="aset[' . $index . '][' . $field . ']"]')->item(0); expect($select->hasAttribute('required'))->toBe($present); expect($select->hasAttribute('disabled'))->toBe(!$present); } })->with([true, false]); test('asset validation errors appear beside the marked required dropdowns', function () { $group = ($this->kelompok)(); $request = ($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET'); session()->flashInput(['aset' => [['status_aset' => '1']]]); $messages = [ 'aset.0.status_pengelolaan_aset' => 'Status Pengelolaan wajib diisi jika aset dipilih Ada.', 'aset.0.kondisi_aset' => 'Kondisi wajib diisi jika aset dipilih Ada.', ]; $errors = new ViewErrorBag(); $errors->put('default', new Illuminate\Support\MessageBag($messages)); view()->share('errors', $errors); $dom = ($this->profileDom)(app(SurveyKelompokTaniController::class)->edit($request, $group)); expect($dom->query('//form[@id="aset-produktif-form"]//th/span[contains(@class,"text-danger")]')->length)->toBe(2); foreach (['status_pengelolaan_aset' => 'status-pengelolaan', 'kondisi_aset' => 'kondisi'] as $field => $idPart) { $id = 'aset-0-' . $idPart; $select = $dom->query('//select[@id="' . $id . '"]')->item(0); expect($select->getAttribute('class'))->toContain('is-invalid'); expect($select->getAttribute('aria-describedby'))->toBe($id . '-error'); expect(trim($dom->query('//div[@id="' . $id . '-error"]')->item(0)->textContent))->toBe($messages['aset.0.' . $field]); } }); test('saving assets advances to cash only when the cash section is available', function (bool $hasCash) { $group = ($this->kelompok)(); $controller = app(AsetKelompokTaniController::class); $input = ['aset' => [[ 'jenis_aset' => 'Kas/simpanan kelompok', 'status_aset' => $hasCash, 'status_pengelolaan_aset' => 'Swadaya', 'kondisi_aset' => 'Berfungsi', ]]]; $response = $controller->update(($this->profileRequest)($input), $group); $url = route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi'; expect($response->getTargetUrl())->toBe($url . ($hasCash ? '#section-kas' : '#section-status-bantuan')); $input['aset'][0]['status_aset'] = false; $response = $controller->update(($this->profileRequest)($input), $group); expect($response->getTargetUrl())->toBe($url . '#section-status-bantuan'); })->with([true, false]); test('saving aid status stays on the combined aid card in Operasional', function (bool $hasAid) { $group = ($this->kelompok)(); $response = app(SurveyKelompokTaniController::class)->update(($this->profileRequest)([ 'section' => 'status-bantuan', 'pernah_mendapatkan_bantuan' => $hasAid, 'bantuan' => $hasAid ? [['nama_program' => 'Program A', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit', 'tahun' => 2026]] : [], ]), $group); $url = route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi'; expect($response->getTargetUrl())->toBe($url . '#section-status-bantuan'); })->with([true, false]); test('legacy aid saves return to Operasional and failed saves retain the notification target', function () { $group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => true]); $controller = app(BantuanKelompokTaniController::class); $response = $controller->store(($this->profileRequest)(['bantuan' => [[ 'nama_program' => 'Bibit Kakao', 'penyelenggara' => 'Dinas Pertanian', 'bentuk_bantuan' => 'Bibit', 'tahun' => 2026, ]]], 'POST'), $group); $url = route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi'; expect($response->getTargetUrl())->toBe($url . '#section-status-bantuan'); $group->update(['pernah_mendapatkan_bantuan' => false]); $response = $controller->store(($this->profileRequest)([], 'POST'), $group); expect($response->getTargetUrl())->toBe($url . '#kelompok-tani-notifications'); expect($group->bantuan()->count())->toBe(1); }); test('single and final section saves stay on their own box', function () { $group = ($this->kelompok)(); $controller = app(SurveyKelompokTaniController::class); foreach ([ 'kelembagaan' => ['cakupan_wilayah' => 'Desa'], 'sosial-konflik' => ['konflik_kelompok' => 'Tidak pernah'], ] as $section => $input) { $response = $controller->update(($this->profileRequest)(['section' => $section, ...$input]), $group); expect(parse_url($response->getTargetUrl(), PHP_URL_FRAGMENT))->toBe('section-' . $section); } }); test('invalid required fields return Indonesian errors above the summary and beside each field', function (string $section, array $payload, array $messages) { $group = ($this->kelompok)(); $url = route('kelompok-tani.edit', $group->id) . ($section === 'identitas' ? '' : '?section=' . ($section === 'status-bantuan' ? 'aktivitas-fungsi' : $section)); $response = $this->from($url)->put(route('kelompok-tani.update', $group->id), ['section' => $section, ...$payload]); $response->assertRedirect($url)->assertSessionHasErrors(array_keys($messages)); $page = $this->get($url)->assertOk(); expect(strpos($page->getContent(), 'id="kelompok-tani-notifications"'))->toBeLessThan(strpos($page->getContent(), 'id="kelompok-tani-summary"')); $dom = new DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML($page->getContent()); libxml_clear_errors(); $xpath = new DOMXPath($dom); $summary = $xpath->query('//div[@id="kelompok-tani-notifications"]')->item(0)->textContent; $inlineMessages = collect($xpath->query('//div[contains(@class,"invalid-feedback")]'))->map(fn ($node) => trim($node->textContent))->all(); foreach ($messages as $message) { expect($summary)->toContain($message); expect($inlineMessages)->toContain($message); } expect($xpath->query('//input[contains(@class,"is-invalid")] | //select[contains(@class,"is-invalid")]')->length)->toBeGreaterThan(0); expect($group->refresh()->nama_kelompok)->toBe('Kelompok A'); })->with([ ['identitas', [], [ 'nama_responden' => 'Nama responden wajib diisi.', 'nama_kelompok' => 'Nama kelompok wajib diisi.', 'provinsi_id' => 'Provinsi wajib diisi.', 'kabupaten_id' => 'Kabupaten/Kota wajib diisi.', 'kecamatan_id' => 'Kecamatan wajib diisi.', 'desa_kelurahan_id' => 'Desa/Kelurahan wajib dipilih.', 'jenis_kelompok' => 'Terdaftar/Dikenal Sebagai wajib diisi.', ]], ['identitas', ['jenis_kelompok' => 'Lainnya'], ['jenis_kelompok_lainnya' => 'Sebutkan Jenis Kelompok wajib diisi.']], ['status-bantuan', [], ['pernah_mendapatkan_bantuan' => 'Pengalaman Akses Bantuan/Program wajib diisi.']], ['status-bantuan', ['pernah_mendapatkan_bantuan' => '1', 'bantuan' => [[]]], [ 'bantuan.0.nama_program' => 'Nama Program/Bantuan pada baris pertama wajib diisi.', 'bantuan.0.penyelenggara' => 'Penyelenggara pada baris pertama wajib diisi.', 'bantuan.0.bentuk_bantuan' => 'Bentuk Bantuan pada baris pertama wajib diisi.', 'bantuan.0.tahun' => 'Tahun pada baris pertama wajib diisi.', ]], ['sosial-konflik', ['konflik_kelompok' => 'Pernah, sudah selesai'], [ 'topik_konflik' => 'Jika Pernah, Soal Apa? wajib diisi jika pernah terjadi konflik internal.', ]], ]); test('asset errors show together above the summary and beside both conditional dropdowns', function () { $group = ($this->kelompok)(); $url = route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi'; $this->from($url)->put(route('kelompok-tani.aset-keuangan.update', $group->id), [ 'aset' => [['jenis_aset' => AsetKelompokTaniController::JENIS_ASET_BAKU[0], 'status_aset' => '1']], ])->assertRedirect($url)->assertSessionHasErrors(['aset.0.status_pengelolaan_aset', 'aset.0.kondisi_aset']); $page = $this->get($url)->assertOk(); foreach (['Status Pengelolaan wajib diisi jika aset dipilih Ada.', 'Kondisi wajib diisi jika aset dipilih Ada.'] as $message) { expect(substr_count($page->getContent(), $message))->toBeGreaterThanOrEqual(2); } expect($group->aset()->count())->toBe(0); }); test('profile location choices survive a failed save on both create and edit', function (bool $creating) { $group = ($this->kelompok)(); $url = $creating ? route('kelompok-tani.create') : route('kelompok-tani.edit', $group->id); $input = ['provinsi_id' => 1, 'kabupaten_id' => 1, 'kecamatan_id' => 1, 'desa_kelurahan_id' => 1, 'jenis_kelompok' => 'Kelompok Tani (Poktan)']; $response = $creating ? $this->from($url)->post(route('kelompok-tani.store'), $input) : $this->from($url)->put(route('kelompok-tani.update', $group->id), $input); $response->assertRedirect($url)->assertSessionHasErrors(['nama_responden', 'nama_kelompok']); $page = $this->get($url)->assertOk(); $dom = new DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML($page->getContent()); libxml_clear_errors(); $xpath = new DOMXPath($dom); foreach (array_keys($input) as $field) { expect($xpath->query('//select[@name="' . $field . '"]/option[@selected]')->length)->toBe(1); } expect($xpath->query('//select[@name="desa_kelurahan_id" and @disabled]')->length)->toBe(0); })->with([true, false]); test('invalid location input returns feedback without breaking the form reload', function ($invalidId) { $url = route('kelompok-tani.create'); $this->from($url)->post(route('kelompok-tani.store'), [ 'provinsi_id' => $invalidId, 'kabupaten_id' => $invalidId, 'kecamatan_id' => $invalidId, 'desa_kelurahan_id' => $invalidId, ])->assertRedirect($url)->assertSessionHasErrors(['provinsi_id', 'kabupaten_id', 'kecamatan_id', 'desa_kelurahan_id']); $this->get($url)->assertOk()->assertSee('Pilihan Provinsi tidak valid.'); })->with([[['invalid']], ['1.5'], ['invalid']]); test('aid experience and program entries are saved together in Operasional', function () { $group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => false]); $response = app(SurveyKelompokTaniController::class)->update(($this->profileRequest)([ 'section' => 'status-bantuan', 'pernah_mendapatkan_bantuan' => '1', 'bantuan' => [ ['nama_program' => 'Bibit Kakao', 'penyelenggara' => 'Dinas Pertanian', 'bentuk_bantuan' => 'Bibit', 'tahun' => 2026], ['nama_program' => '', 'penyelenggara' => '', 'bentuk_bantuan' => '', 'tahun' => null], ], ]), $group); expect($group->refresh()->pernah_mendapatkan_bantuan)->toBeTrue(); expect($group->bantuan()->pluck('nama_program')->all())->toBe(['Bibit Kakao']); expect($response->getTargetUrl())->toBe(route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi#section-status-bantuan'); expect(session('success'))->toBe('Data Operasional berhasil disimpan.'); }); test('answering Belum Pernah ignores hidden program payloads and preserves existing history', function ($payload) { $group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => true]); $group->bantuan()->create([ 'uid' => 'BKT-TEST', 'nama_program' => 'Program Lama', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit', ]); app(SurveyKelompokTaniController::class)->update(($this->profileRequest)([ 'section' => 'status-bantuan', 'pernah_mendapatkan_bantuan' => '0', 'bantuan' => $payload, ]), $group); expect($group->refresh()->pernah_mendapatkan_bantuan)->toBeFalse(); expect($group->bantuan()->pluck('nama_program')->all())->toBe(['Program Lama']); })->with([ ['malformed'], [[['nama_program' => ['invalid'], 'tahun' => 'invalid']]], ]); test('invalid aid entries cannot partially update the experience answer', function () { $group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => false]); $request = ($this->profileRequest)([ 'section' => 'status-bantuan', 'pernah_mendapatkan_bantuan' => '1', 'bantuan' => [['nama_program' => 'Program A', 'tahun' => date('Y') + 1]], ]); expect(fn () => app(SurveyKelompokTaniController::class)->update($request, $group))->toThrow(ValidationException::class); expect($group->refresh()->pernah_mendapatkan_bantuan)->toBeFalse(); expect($group->bantuan()->count())->toBe(0); }); test('aid creation failures roll back the experience answer', function () { $group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => false]); Event::listen('eloquent.creating: ' . BantuanKelompokTani::class, function () { throw new RuntimeException('Simulated aid failure'); }); $request = ($this->profileRequest)([ 'section' => 'status-bantuan', 'pernah_mendapatkan_bantuan' => '1', 'bantuan' => [['nama_program' => 'Program A', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit', 'tahun' => 2026]], ]); expect(fn () => app(SurveyKelompokTaniController::class)->update($request, $group))->toThrow(RuntimeException::class); expect($group->refresh()->pernah_mendapatkan_bantuan)->toBeFalse(); expect($group->bantuan()->count())->toBe(0); }); test('aid list shares the experience box and is only visible for Pernah', function (?bool $savedAnswer, ?string $oldAnswer, bool $visible) { $group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => $savedAnswer]); $request = ($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET'); if ($oldAnswer !== null) { session()->flashInput([ 'pernah_mendapatkan_bantuan' => $oldAnswer, 'bantuan' => [['nama_program' => 'Program belum disimpan', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit', 'tahun' => 2026]], ]); } $dom = ($this->profileDom)(app(SurveyKelompokTaniController::class)->edit($request, $group)); expect($dom->query('//form[@id="pengalaman-bantuan-form"]//div[@id="section-bantuan"]')->length)->toBe(1); expect($dom->query('//div[@id="section-bantuan"]')->item(0)->hasAttribute('hidden'))->toBe(!$visible); expect($dom->query('//fieldset[@id="bantuan-fields"]')->item(0)->hasAttribute('disabled'))->toBe(!$visible); expect($dom->query('//form[@id="pengalaman-bantuan-form"]//button[contains(@class,"btn-success")]')->length)->toBe(1); expect($dom->query('//form[@id="pengalaman-bantuan-form"]//form')->length)->toBe(0); expect($dom->query('//div[@id="section-penilaian"] | //input[@name="potensi_kelompok"] | //textarea[@name="alasan_penilaian_potensi"]')->length)->toBe(0); if ($oldAnswer !== null) { expect($dom->query('//form[@id="pengalaman-bantuan-form"]//input[@value="Program belum disimpan"]')->length)->toBe(1); } })->with([ [null, null, false], [false, null, false], [true, null, true], [true, '0', false], [false, '1', true], ]); test('old aid URLs redirect to Operasional and removed assessment fields cannot be saved', function () { $group = ($this->kelompok)(['potensi_kelompok' => 'Rendah']); $url = route('kelompok-tani.edit', $group->id) . '?section=aktivitas-fungsi#section-status-bantuan'; expect(app(BantuanKelompokTaniController::class)->index($group)->getTargetUrl())->toBe($url); $response = app(SurveyKelompokTaniController::class)->update(($this->profileRequest)([ 'section' => 'penilaian', 'potensi_kelompok' => 'Tinggi', 'alasan_penilaian_potensi' => 'Diabaikan', ]), $group); expect($response->getTargetUrl())->toBe($url); expect($group->refresh()->potensi_kelompok)->toBe('Rendah'); }); test('Pernah requires every first-row aid field before saving', function (string $missing) { $group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => false]); $row = ['nama_program' => 'Program A', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit', 'tahun' => 2026]; $row[$missing] = ' '; $request = ($this->profileRequest)(['pernah_mendapatkan_bantuan' => '1', 'bantuan' => [$row]]); try { app(BantuanKelompokTaniController::class)->update($request, $group); $this->fail('An incomplete first row must be rejected.'); } catch (ValidationException $exception) { expect($exception->errors())->toHaveKey('bantuan.0.' . $missing); } expect($group->refresh()->pernah_mendapatkan_bantuan)->toBeFalse(); expect($group->bantuan()->count())->toBe(0); })->with(['nama_program', 'penyelenggara', 'bentuk_bantuan', 'tahun']); test('Pernah rejects missing first aid rows including attempts to skip the first index', function ($rows) { $group = ($this->kelompok)(); $request = ($this->profileRequest)(['pernah_mendapatkan_bantuan' => '1', 'bantuan' => $rows]); expect(fn () => app(BantuanKelompokTaniController::class)->update($request, $group))->toThrow(ValidationException::class); expect($group->bantuan()->count())->toBe(0); })->with([ [null], [[]], [[[]]], [[1 => ['nama_program' => 'Program A', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit', 'tahun' => 2026]]], ]); test('recorded aid meets the minimum without requiring a duplicate entry', function () { $group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => true]); $group->bantuan()->create(['uid' => 'BKT-EXISTING', 'nama_program' => 'Program Lama', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit']); app(BantuanKelompokTaniController::class)->update(($this->profileRequest)(['pernah_mendapatkan_bantuan' => '1']), $group); expect($group->bantuan()->count())->toBe(1); app(BantuanKelompokTaniController::class)->update(($this->profileRequest)([ 'pernah_mendapatkan_bantuan' => '1', 'bantuan' => [['nama_program' => 'Program Tambahan'], []], ]), $group); expect($group->bantuan()->count())->toBe(2); }); test('an optional aid row with details still needs a program name', function () { $group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => true]); $group->bantuan()->create(['uid' => 'BKT-EXISTING', 'nama_program' => 'Program Lama', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit']); $request = ($this->profileRequest)(['pernah_mendapatkan_bantuan' => '1', 'bantuan' => [['penyelenggara' => 'Dinas']]]); expect(fn () => app(BantuanKelompokTaniController::class)->update($request, $group))->toThrow(ValidationException::class); expect($group->bantuan()->count())->toBe(1); }); test('required aid markers only apply to the first unsaved row for Pernah', function (bool $pernah, bool $saved) { $group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => $pernah]); if ($saved) { $group->bantuan()->create(['uid' => 'BKT-EXISTING', 'nama_program' => 'Program Lama', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit']); } $request = ($this->profileRequest)(['section' => 'aktivitas-fungsi'], 'GET'); session()->flashInput(['bantuan' => [[], []]]); $dom = ($this->profileDom)(app(SurveyKelompokTaniController::class)->edit($request, $group)); $expected = $pernah && !$saved ? 4 : 0; expect($dom->query('//fieldset[@id="bantuan-fields"]//input[@required]')->length)->toBe($expected); expect($dom->query('//fieldset[@id="bantuan-fields"]//span[contains(@class,"bantuan-required-mark") and not(@hidden)]')->length)->toBe($expected); expect($dom->query('//div[@data-repeater-list="bantuan"]/div[2]//input[@required]')->length)->toBe(0); expect($dom->query('//div[@id="section-bantuan"]/p[normalize-space()="Minimal satu data bantuan wajib diisi."]')->length)->toBe(1); $firstRow = $saved ? '//div[@id="section-bantuan"]/div[1]' : '//div[@data-repeater-list="bantuan"]/div[1]'; foreach (['Nama Program/Bantuan', 'Penyelenggara', 'Bentuk Bantuan', 'Tahun'] as $label) { $marks = $dom->query($firstRow . '//label[starts-with(normalize-space(),"' . $label . '")]/span[contains(@class,"text-danger") and not(@hidden)]'); expect($marks->length)->toBe($saved || $pernah ? 1 : 0); } })->with([[true, false], [false, false], [true, true]]); test('conflict topics are required for both Pernah answers without partial saves', function (string $answer, $topic) { $group = ($this->kelompok)(['konflik_kelompok' => 'Tidak pernah', 'partisipasi_perempuan' => 'Hadir tapi pasif']); $request = ($this->profileRequest)([ 'section' => 'sosial-konflik', 'konflik_kelompok' => $answer, 'topik_konflik' => $topic, 'partisipasi_perempuan' => 'Tidak terlibat', ]); try { app(SurveyKelompokTaniController::class)->update($request, $group); $this->fail('A conflict topic is required.'); } catch (ValidationException $exception) { expect($exception->errors())->toHaveKey('topik_konflik'); } expect($group->refresh()->konflik_kelompok)->toBe('Tidak pernah'); expect($group->partisipasi_perempuan)->toBe('Hadir tapi pasif'); })->with(['Pernah, sudah selesai', 'Pernah, masih berlanjut/belum selesai'])->with([null, '', ' ']); test('social saves accept conflict topics and require none for other answers', function ($answer, bool $required) { $group = ($this->kelompok)(); $request = ($this->profileRequest)([ 'section' => 'sosial-konflik', 'konflik_kelompok' => $answer, ...($required ? ['topik_konflik' => 'Pembagian hasil'] : []), ]); app(SurveyKelompokTaniController::class)->update($request, $group); expect($group->refresh()->topik_konflik)->toBe($required ? 'Pembagian hasil' : null); expect(session('success'))->toBe('Data Sosial berhasil disimpan.'); ($this->profileRequest)(['section' => 'sosial-konflik'], 'GET'); $dom = ($this->profileDom)(view('produktivitas.survey-kelompok-tani.sosial-konflik', ['kelompokTani' => $group])); $input = $dom->query('//input[@id="sk-topik-konflik"]')->item(0); expect($input->hasAttribute('required'))->toBe($required); expect($input->hasAttribute('disabled'))->toBe(!$required); expect($dom->query('//label[@for="sk-topik-konflik"]/span[not(@hidden)]')->length)->toBe($required ? 1 : 0); })->with([ ['Pernah, sudah selesai', true], ['Pernah, masih berlanjut/belum selesai', true], ['Tidak pernah', false], ['Tidak bersedia menjawab', false], [null, false], ]); test('conflict topic requirements and input follow the previous submission after validation', function () { $group = ($this->kelompok)(['konflik_kelompok' => 'Tidak pernah']); ($this->profileRequest)([], 'GET'); session()->flashInput(['konflik_kelompok' => 'Pernah, sudah selesai', 'topik_konflik' => 'Pembagian hasil']); $dom = ($this->profileDom)(view('produktivitas.survey-kelompok-tani.sosial-konflik', ['kelompokTani' => $group])); $input = $dom->query('//input[@id="sk-topik-konflik"]')->item(0); expect($input->hasAttribute('required'))->toBeTrue(); expect($input->getAttribute('value'))->toBe('Pembagian hasil'); expect($dom->query('//div[@id="sk-detail-konflik-wrapper"]')->item(0)->hasAttribute('style'))->toBeFalse(); }); test('the last recorded aid cannot be deleted while the answer remains Pernah', function () { $group = ($this->kelompok)(['pernah_mendapatkan_bantuan' => true]); $aid = $group->bantuan()->create(['uid' => 'BKT-EXISTING', 'nama_program' => 'Program Lama', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit']); $controller = app(BantuanKelompokTaniController::class); $response = $controller->destroy($group, $aid); expect($response->getTargetUrl())->toEndWith('#kelompok-tani-notifications'); expect($group->bantuan()->count())->toBe(1); $other = $group->bantuan()->create(['uid' => 'BKT-OTHER', 'nama_program' => 'Program Lain', 'penyelenggara' => 'Dinas', 'bentuk_bantuan' => 'Bibit']); $controller->destroy($group, $aid); expect($group->bantuan()->count())->toBe(1); $group->update(['pernah_mendapatkan_bantuan' => false]); $controller->destroy($group, $other); expect($group->bantuan()->count())->toBe(0); });