import shutil import subprocess from pathlib import Path import pytest @pytest.mark.skipif(not shutil.which('node'), reason='Node required for V5 behavior') def test_composer_new_automatic_body_does_not_mutate_frozen_unknown_outcome(): source = Path('static/js/case-command-composer.js').read_text() body = source.split('function body()', 1)[1].split(' function label(', 1)[0] script = r''' const assert=require('node:assert/strict'); let frozenBody=null,parsed={comment:'Tid',commands:[{type:'time',minutes:15}]}; function body() BODY assert.equal(body().billing_mode,'automatic'); const legacy={comment:'Older',commands:[{type:'time',settlement_choice:'card_8'}],internal:true};frozenBody=legacy; assert.equal(body(),legacy);assert.equal(body().commands[0].settlement_choice,'card_8');assert.equal(body().billing_mode,undefined); '''.replace('BODY', body) result = subprocess.run(['node', '-e', script], text=True, capture_output=True) assert result.returncode == 0, result.stderr @pytest.mark.skipif(not shutil.which('node'), reason='Node required for V5 behavior') def test_active_billing_choice_permission_and_selected_method(): source = Path('static/js/time-billing-review.js').read_text() script = r''' const assert=require('node:assert/strict'),vm=require('node:vm'),fs=require('node:fs'),window={}; vm.runInNewContext(fs.readFileSync('static/js/time-billing-review.js','utf8'),{window});const review=window.TimeBillingReview; assert.equal(review.canChoose({workflow_active:true,workflow_can_override:false,status:'pending'}),false); assert.equal(review.canChoose({workflow_active:true,workflow_can_override:true,status:'pending'}),true); assert.equal(review.canChoose({workflow_active:true,workflow_can_override:true,status:'billed'}),false); assert.match(review.displayBadge({billing_method:'draft',selected_billing_method:'invoice'}),/Faktura/); assert.doesNotMatch(review.displayBadge({billing_method:'draft',selected_billing_method:'invoice'}),/Kladde/); ''' result = subprocess.run(['node', '-e', script], text=True, capture_output=True) assert result.returncode == 0, result.stderr def test_shared_v4_payload_markers_are_conditional_and_manual_only_in_time_tab(): source = Path('app/modules/sag/templates/detail_v4.html').read_text() manual = source.split('async function createManualTimeV1(event)', 1)[1].split(' async function ', 1)[0] assert "document.getElementById('caseV5Data') ? {billing_mode:'manual'" in manual quick = source.split('async function registerQuickTimeFromComment(content)', 1)[1].split(' function resolveCommentAwait', 1)[0] assert "document.getElementById('caseV5Data') ? {billing_mode: 'automatic'} : {}" in quick assert 'settlement_choice' not in quick assert "document.getElementById('caseV5Data')?'&billing_display=true':''" in source @pytest.mark.skipif(not shutil.which('node'), reason='Node required for V5 behavior') def test_active_manual_selection_only_saves_audited_decision(): script = r''' const assert=require('node:assert/strict'),vm=require('node:vm'),fs=require('node:fs'),window={},calls=[]; let host; const select={value:'invoice',add(){}},textarea={value:''},submit={disabled:false},status={},form={}; const document={createElement:()=>host={listeners:{},querySelector:s=>({select,textarea,form,'[type=submit]':submit,'[role=status]':status}[s]),addEventListener:(name,fn)=>host.listeners[name]=fn,remove(){}},body:{append(){}}}; class Modal{show(){}hide(){host.listeners['hidden.bs.modal']();}dispose(){}} const fetch=async(url,options={})=>{calls.push({url,options});return{ok:true,json:async()=>url.endsWith('settlement-options')?{prepaid_cards:[{id:8}],agreements:[]}:url.endsWith('workflow-decision')?{current:{method:'invoice',status:'decided',details:{}}}:{id:1}};}; function Option(label,value){this.value=value;} vm.runInNewContext(fs.readFileSync('static/js/time-billing-review.js','utf8'),{window,document,fetch,Option,bootstrap:{Modal}}); (async()=>{ const promise=window.TimeBillingReview.choose({id:1,workflow_active:true,workflow_can_override:true,status:'pending'}); await new Promise(resolve=>setImmediate(resolve)); select.value='card_8';await form.onsubmit({preventDefault(){}});assert.equal(calls.length,2); textarea.value='Gyldig kundeaftale';await form.onsubmit({preventDefault(){}});assert.equal(await promise,true); assert.equal(calls.length,3);assert.match(calls[2].url,/workflow-override$/);assert.equal(calls[2].options.method,'POST'); assert.deepEqual(JSON.parse(calls[2].options.body),{method:'prepaid',reason:'Gyldig kundeaftale',card_id:8}); assert.equal(calls.some(call=>call.url.endsWith('/approve')),false); })().catch(error=>{console.error(error);process.exitCode=1;}); ''' result = subprocess.run(['node', '-e', script], text=True, capture_output=True) assert result.returncode == 0, result.stderr