false", $source); } public function testResponsiveShellHasDesktopAndMobileRules(): void { $css = file_get_contents(dirname(APPPATH) . '/assets/css/app-final.css'); self::assertStringContainsString('@media', $css); self::assertMatchesRegularExpression('/\.mobile-action-bar\s*\{display\s*:\s*none!important\}/i', $css, 'Menu bawah wajib disembunyikan pada desktop.'); self::assertMatchesRegularExpression('/max-width\s*:\s*991(?:\.98)?px[\s\S]*\.mobile-action-bar\s*\{[\s\S]*display\s*:\s*grid!important/i', $css, 'Menu bawah wajib tersedia pada HP.'); self::assertMatchesRegularExpression('/max-width\s*:\s*(991|768|767)(?:\.98)?px/i', $css, 'Breakpoint HP belum tersedia.'); } public function testUiPreventsRepeatedSubmitAndHandlesAjaxFailure(): void { $js = file_get_contents(dirname(APPPATH) . '/assets/js/app-ui.min.js'); $network = file_get_contents(dirname(APPPATH) . '/assets/js/app-network.js'); self::assertMatchesRegularExpression('/submitting|is-submitting|disabled/i', $js); self::assertMatchesRegularExpression('/ajaxError|status\s*===\s*401|\.fail\s*\(/i', $network); } public function testMoneyInputsUseCentralParserBeforeAjax(): void { $ui = file_get_contents(dirname(APPPATH) . '/assets/js/app-ui.min.js'); $journal = file_get_contents(APPPATH . 'views/jurnal/layout.php'); self::assertStringContainsString('window.parseMoney=', $ui); self::assertStringContainsString("replace(/,/g,'')", $ui); self::assertStringContainsString("+(hasDecimal?','+decimal:'')", $ui); self::assertStringNotContainsString("parseFloat($(this).find('.debit').val())", $journal); self::assertStringContainsString("window.parseMoney($(this).find('.debit').val())", $journal); } public function testMasterAdminHasFullAccessAndApprovalOverride(): void { $base = file_get_contents(APPPATH . 'core/MY_Controller.php'); $approval = file_get_contents(APPPATH . 'libraries/ApprovalService.php'); $roles = file_get_contents(APPPATH . 'controllers/Roles.php'); self::assertStringContainsString("userdata('role') === 'Admin'", $base); self::assertStringContainsString('$masterAdmin', $approval); self::assertStringContainsString('is_super_admin', $approval); self::assertStringContainsString('Role wajib mempunyai minimal satu permission.', $roles); self::assertStringContainsString('Role bawaan aplikasi tidak dapat dihapus.', $roles); } public function testProfessionalReportsHaveIndonesianFinancialStatements(): void { $navigation = file_get_contents(APPPATH . 'libraries/NavigationService.php'); $view = file_get_contents(APPPATH . 'views/professional_reports/index.php'); self::assertStringContainsString("'label'=>'Laporan Profesional'", $navigation); self::assertStringContainsString("'balance_sheet'=>'Laporan Neraca'", $view); self::assertStringContainsString("'profit_loss'=>'Laporan Laba Rugi'", $view); self::assertStringContainsString('TOTAL AKTIVA', $view); self::assertStringContainsString('TOTAL PASIVA', $view); self::assertStringContainsString('Laba Bersih Berjalan', $view); self::assertStringContainsString('TOTAL PENDAPATAN', $view); self::assertStringContainsString('TOTAL BEBAN', $view); self::assertStringContainsString('$accountCode', $view); } public function testPurchasePaymentsSeparatePrincipalAndBankCharge(): void { $service = file_get_contents(APPPATH . 'libraries/PurchaseService.php'); $purchases = file_get_contents(APPPATH . 'controllers/Purchases.php'); $payables = file_get_contents(APPPATH . 'controllers/Payables.php'); $migration = file_get_contents(APPPATH . 'migrations/20260905000700_add_purchase_payment_bank_charge.php'); self::assertStringContainsString('bank_charge_amount', $migration); self::assertStringContainsString('total_cash_out', $migration); self::assertStringContainsString("accountmappingservice->get('bank_charge_expense')", $service); self::assertStringContainsString('$principal=round($gross-$charge,2)', $service); self::assertStringContainsString('prepayOrderWithBankCharge', $purchases); self::assertStringContainsString('settleSupplierInvoiceMultiWithBankCharge', $purchases); self::assertStringContainsString('paymentMultiWithBankCharge', $payables); } public function testPurchaseReturnsAreSeparatedAndIntegrated(): void { $navigation=file_get_contents(APPPATH.'libraries/NavigationService.php'); $workflow=file_get_contents(APPPATH.'views/purchases/workflow.php'); $view=file_get_contents(APPPATH.'views/purchase_returns/index.php'); $service=file_get_contents(APPPATH.'libraries/PurchaseAdjustmentService.php'); $controller=file_get_contents(APPPATH.'controllers/Purchasereturns.php'); self::assertStringContainsString("'label'=>'Retur & Refund'",$navigation); self::assertStringNotContainsString("'adjustment'=>'6",$workflow); self::assertStringContainsString('purchase-workflow-stepper',$workflow); self::assertStringContainsString('adjustment-stepper',$view); self::assertStringContainsString('purchase_return_status_history',$service); self::assertStringContainsString("movement_type'=>'purchase_return'",$service); self::assertStringContainsString("writeLedger('purchase_return'",$service); self::assertStringContainsString("accountmappingservice->get('supplier_refund_receivable')",$service); self::assertStringContainsString('FOR UPDATE',$service); self::assertStringContainsString('idempotency_key',$service); self::assertStringContainsString("requirePermission('purchase_returns'",$controller); self::assertStringContainsString("requirePermission('purchase_refunds'",$controller); self::assertStringContainsString("requirePermission('supplier_debit_notes'",$controller); self::assertStringContainsString('attachment-input',$view); self::assertStringContainsString('DataTransfer',$view); self::assertStringContainsString('\$posted",$items,'Aktivasi barang berserial harus dapat diposting bertahap.'); self::assertStringContainsString("\$doc->document_type==='transfer'",$service,'Transfer gudang harus melalui dua sisi ledger.'); self::assertStringContainsString('reservationAction',$service); self::assertStringContainsString("array('release','consume','cancel')",$service); self::assertStringContainsString('stok negatif ditolak',$service); self::assertStringContainsString('idempotency_key',$service); self::assertStringContainsString("\$doc->document_type==='opname'",$service); self::assertStringContainsString('reversal_of_id',$service); self::assertStringContainsString("writeLedger('purchase_return'",$returns); self::assertStringContainsString('purchase_replacements',$returns); self::assertStringContainsString("ib.status = 'available'",$technician); self::assertStringContainsString('FOR UPDATE',$technician); self::assertStringContainsString("where('i.company_id'",$technician); self::assertStringContainsString("'document_type'=>'invoice_details'",$sales); self::assertStringContainsString("'document_type'=>'asset_capitalization'",$asset); self::assertStringContainsString("'reconciliation'",$reports); self::assertStringContainsString("'stock_card'",$reports); self::assertStringContainsString('limit($length, $start)',$items,'Daftar barcode wajib dipaginasi di server.'); } public function testInventoryWriteEndpointsAndButtonsArePermissionAware(): void { $controller=file_get_contents(APPPATH.'controllers/Inventoryprofessional.php'); $items=file_get_contents(APPPATH.'controllers/Items.php'); $operations=file_get_contents(APPPATH.'views/inventory_professional/operations.php'); $pending=file_get_contents(APPPATH.'views/items/draft_items.php'); self::assertStringContainsString("input->method())!=='POST'",$controller); self::assertStringContainsString("input->method()) !== 'POST'",$items); self::assertStringContainsString("requirePermission('items','can_approve')",$controller); self::assertStringContainsString('permissions.approve',$operations); self::assertStringContainsString('$canPost',$items); self::assertStringNotContainsString('btn-adjust',$pending); self::assertStringNotContainsString('btn-delete',$pending); } public function testFixedAssetsUseSingleProfessionalWorkflow(): void { $controller=file_get_contents(APPPATH.'controllers/Fixedassets.php'); $legacy=file_get_contents(APPPATH.'controllers/Asset.php'); $service=file_get_contents(APPPATH.'libraries/FixedAssetService.php'); $model=file_get_contents(APPPATH.'models/FixedAssetModel.php'); $view=file_get_contents(APPPATH.'views/fixed_assets/index.php'); self::assertStringContainsString('FixedAssetService',$controller); self::assertStringContainsString("redirect('fixedassets'",$legacy); self::assertStringNotContainsString("insert('assets'",$legacy); self::assertStringContainsString('FOR UPDATE',$service); self::assertStringContainsString("'document_type'=>'asset_capitalization'",$service); self::assertStringContainsString("'movement_type'=>'installation'",$service); self::assertStringContainsString('idempotency_key',$service); self::assertStringContainsString("event_type==='depreciation'",$service); self::assertStringContainsString("lifecycle_status NOT IN('disposed','lost','damaged')",$model); self::assertStringContainsString('serverSide:true',file_get_contents(dirname(APPPATH).'/assets/js/fixed-assets.js')); self::assertStringContainsString('Laporan & Rekonsiliasi',$view); self::assertStringNotContainsString('window.alert(',$view); self::assertStringNotContainsString('window.confirm(',$view); } }