ofacturare_editare.prg (nou): helpere comune - garda eFactura, cursoarele notei si ale rulajelor, randul de vanzare corespunzator notei si liniile de articole citite din view-ul VVANZARI_ARTICOLE. omodificari.vc2 (frm_modific2024): pagina noua de articole ale facturii, deocamdata doar afisare. Apare numai cand ofacturare_editare.prg e inregistrat si documentul are rand in VANZARI; in ROACONT si ROAGEST, unde fisierul nu e incarcat, pagina lipseste si registrul jurnal ramane neatins. Randul de vanzare al notei se cauta pe toate tripletele distincte (nract, serie_act, dataact) din nota, pentru ca primul rand poate fi o incasare. ofacturare_comun.vc2 (frm_facturi): do_editare_factura si butonul aferent, dupa modelul lui do_sterge, cu garzile de luna inchisa, luna curenta, document sters, referinte si eFactura. docs: comentariile se scriu strict necesar, si in cod si in scripturile de migrare - fara referinte la planuri, stories, decizii sau erori, istoricul doar in antetul fisierului. Plus regula zero (predarea contextului), capcanele de la testarea headless si completari pe fluxul text -> binar. utile/Teste: suita de regresie pentru editarea facturii si harness-ul watchdog (nu sunt in SVN, unde utile/Teste e ignorat). .gitignore ignora si capturile PNG si watchdog_out/, ramase din rulari. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SN8snvkk94KuhWwoXUUey3
17 KiB
Testare UI VFP headless cu harness + screenshots
Harness reutilizabil pentru teste care afiseaza un formular REAL (cu dependinte dummy, fara
Oracle), executa actiuni si valideaza vizual prin capturi. Valabil pentru toate proiectele VFP
din suita ROA. Alternativa la testare-vfp-mcp.md (care porneste programul principal intreg);
capcanele generale de rulare headless (-A -T, SAFETY, script agatat) sunt in
depanare_testare_vfp.md.
Fisiere in COMUN\utile\Teste\: vfp_ui_harness.ps1 (orchestrator generic, parametrizat),
_precompile.ps1 (precompilare izolata), ui_harness.prg (proceduri de handshake),
mock_amessagebox.prg (mock dialog) — infrastructura sta in radacina Teste\; suitele stau in
subfoldere pe subiect. Exemplu complet: achizitie_import\test_import_nota_ui.prg + .ps1.
REGULA (Marius): testele pe aplicatii frontend exerseaza fluxul REAL al utilizatorului — formularul/butonul/metoda pe care o apeleaza aplicatia, NU introducerea directa a randurilor in cursoare/tabele. Popularea directa sare peste validari/filtre/efecte laterale si da PASS fals (caz real: articol "adaugat" direct in cursor trecea testul, dar prin fluxul real nu se adauga deloc). Mock-urile raman permise doar pe INFRASTRUCTURA (Oracle, dialoguri modale), nu pe pasii fluxului testat.
Test UI nou in 5 pasi
.prgde test:PUBLIC gcUILog, gcSyncDir;SET SAFETY OFF+SET TALK OFFca PRIMA linie (vezi capcana a);SET PATH/CLASSLIB/PROCEDUREca in aplicatie; incarca ULTIMELEui_harness.prgsi (daca ai dialoguri)mock_amessagebox.prg, cuADDITIVE.- Creeaza dummy-urile minime (
goExecutorcu metode reale,goApppt.ReadIni,poActpt. curs valutar) + cursoarele cerute de Init (RecordSource-urile grilelor). - Instantiaza clasa,
WindowType=0(modeless -Show()pe modal nu returneaza),Show(),WindowState=2. - Pe fiecare pas: fa actiunea, verifica efectul in date (PASS/FAIL cu helper local), apoi
DO HarnessStep WITH <n>, '<mesaj>'(scrie ready_n, asteapta cont_n de la orchestrator sau auto-continua dupa ~30s). La finalDO HarnessDone WITH 'done'. powershell -File vfp_ui_harness.ps1 -TestPrg <test.prg> -Steps @('pas0',...). PNG-uri inscreenshots\step_<n>_<eticheta>.png; log in<test>_log.txt.
Dialoguri modale (Show(1))
Doua situatii, cu solutii diferite:
A. Testul instantiaza el dialogul — WindowType = 0 inainte de Show() (pasul 3 de mai sus).
Show() revine imediat si conduci formularul pas cu pas. Prefera intotdeauna varianta asta.
B. Show(1) e hardcodat in metoda aplicatiei (ex. frm_facturare_articole.do_adauga_articol /
do_modifica): testul nu are referinta la dialog, deci nu poate seta WindowType, iar fluxul real
cere sa treaca prin buton. Se conduce dintr-un Timer pe _SCREEN, pregatit INAINTE de actiunea
care deschide dialogul:
_SCREEN.AddObject('tmrDrv','Timer'),Interval = 200,BINDEVENT(_SCREEN.tmrDrv, 'Timer', <obiect driver>, 'Executa'),Enabled = .T.Show(1)chiar pompeaza mesaje Windows cat e modal — timerul ticaie normal (masurat: 229 tick-uri in 46s). Bucla modala nu e oarba la evenimente externe.- Detectia formularului:
_SCREEN.Forms(i)/_SCREEN.FormCount, potrivit pe.Class._SCREEN.ActiveFormNU merge headless — a ramas gol la toate cele 229 de tick-uri: depinde deWM_ACTIVATE, care nu se declanseaza fara focus real, pe cand colectiaFormscontine formularul indiferent de activare. - Referinta la controale:
loForm.<nume>merge normal; iterarealoForm.Controls(i)cu potrivire pe.Namee alternativa robusta (utila si ca diagnostic, pentru ca listeaza ce EXISTA cu adevarat). Daca accesul da 1925 "Unknown member" pe un control care sigur e in clasa, suspecteaza mediul de test, nu colectia: o eroare in constructia unui obiect adaugat mai devreme opreste adaugarea celor de dupa, iar formularul ramane incomplet fara sa se plange (caz real: lipsa cursoruluisaft_taxtable, capcana j, a lasat instanta cu 21 de controale in loc de 25, iar controlul urmator din ordinea de adaugare "disparea"). Verifica intaiControlCountsi lista de nume. - Inchiderea:
do_termin()(gateaza peinainte_de_do_termin(), seteazagnButon = 1si elibereaza —_frm_base.vc2:363-371) saudo_renunt(). Daca validarea respinge,do_termin()nu seteazagnButonsi NU elibereaza formularul, deciShow(1)ramane blocat — in testele care asteapta o respingere de validare, iesi prindo_renunt(). - Driverul se instrumenteaza obligatoriu: log pe fiecare pas,
TRY/CATCHcuErrorNo/Message/LineNo, iar steagul "am tratat dialogul" se seteaza dupa interactiunea reusita, nu inainte. Altfel prima eroare e invizibila si blocheaza orice reincercare — simptomul e tick-uri la nesfarsit pana la timeout, fara nicio urma in log.
Regula de metoda, invatata scump: mediul unei suite noi se mosteneste din suita care TRECE,
nu din una care esueaza. Un mediu copiat dintr-un test nevalidat a produs trei defecte succesive
(cursor saft_taxtable lipsa, mock poArticol incomplet, nume de variabila coliziv), fiecare
descoperit dupa cate o rulare, si toate aratau ca defecte de aplicatie sau ca "test agatat". Cand
doua suite au nevoie de acelasi mediu, extrage-l intr-un singur loc (cu grija la capcana l
pentru constructia obiectelor).
Doua non-dovezi, ca sa nu se piarda timp pe ele:
AMEMBERS(a, o)fara flagul 2 intoarce proprietati/metode, nu obiectele continute — absenta unui control din acea lista nu inseamna ca lipseste din clasa.==pe doua obiecte da eroare 107 "Operator/operand type mismatch"; pentru identitate se folosesteCOMPOBJ().
Capcane / deblocari
a. SET SAFETY OFF + SET TALK OFF inaintea primului STRTOFILE. Cu SAFETY ON, scrierea
log-ului existent scoate un dialog modal care blocheaza rularea (proces viu, "instanta
moarta"). Cu TALK ON, SUM/CALCULATE isi echo-eaza rezultatele peste formular in
screenshots.
b. Lansarea orchestratorului: prin tool cu timeout implicit (~120s) e omorat inainte de
final. Ruleaza-l in background + asteapta done.txt, sau timeout 300-600s. NU porni VFP
separat de orchestrator - si-l lanseaza singur.
c. Asteptarea ready_0: cu .fxp cald START apare in ~2s, la rece mult mai mult (zeci de
classlib-uri). -ReadyTimeoutSec generos (implicit 180s); retry NUMAI la "start ratat",
nu la "start lent". Detectia "a pornit" se face pe mtime-ul log-ului > momentul lansarii
(un log stale tinut de un vfp9 zombi pacaleste verificarea "log ne-gol") - omoara zombii
PROPRII (filtrati pe linia de comanda, vezi punctul e) inainte de fiecare lansare.
d. vfp9 -A test.prg deschide INTERMITENT editorul in loc sa ruleze (log gol). Lanseaza
.fxp-ul PRECOMPILAT. Precompilarea (COMPILE+QUIT) atarna dupa COMPILE si, rulata in
ACEEASI sesiune powershell, otraveste lansarile ulterioare - ruleaza-o intr-un proces copil
izolat (Start-Process -Wait _precompile.ps1).
e. Suitele NU se ruleaza in paralel: harness-ul omoara instantele vfp9 ramase inainte de
fiecare lansare, deci un al doilea test pornit peste primul il ucide. Omorarea e filtrata pe
linia de comanda (Win32_Process.CommandLine contine folderul de teste): instantele vfp9
straine (sesiunea IDE a utilizatorului, teste dintr-un ALT proiect VFP) nu sunt atinse. Nu
reintroduce Get-Process vfp9 | Stop-Process -Force fara filtru.
f. FARA FURT DE FOCUS: Graphics.CopyFromScreen fura focus si se corupe daca
utilizatorul lucreaza in paralel - nu se mai foloseste. Capturile: PrintWindow (user32,
P/Invoke) pe Process.MainWindowHandle, flag 2 = PW_RENDERFULLCONTENT (fallback 0 daca
iese goala); merge cu fereastra acoperita, NU minimizata. Fereastra se muta OFF-SCREEN
(SetWindowPos, HWND_BOTTOM, x=-4000, SWP_NOACTIVATE) la aparitia handle-ului si SE
RE-IMPINGE la fiecare pas (VFP se reactiveaza singur la Show()/dialoguri). Consolele
powershell copil: -WindowStyle Hidden (sigur pentru consola, NU pentru GUI VFP ascunsa -
PrintWindow poate reda gol). Implementat in vfp_ui_harness.ps1 (Take-Screenshot,
Push-Offscreen, Wait-MainWindowHandle) si _precompile.ps1.
g. Mock de messagebox: FUNCTION amessagebox care returneaza direct valoarea butonului
(6=Da), incarcat cu SET PROCEDURE ... ADDITIVE PRIMUL, inaintea fisierelor aplicatiei:
la nume duplicat de procedura VFP foloseste fisierul cautat PRIMUL (verificat empiric).
Astfel umbreste amessagebox si in apelurile din metodele de clasa.
h. Cursorii ceruti de Init: pre-populeaza-i INAINTE de instantiere (Init-ul recreeaza din
Oracle cursoarele goale, iar recrearea lasa grid-ul fara coloane, eroare 1925). Pune 1 rand
placeholder inainte, apoi ZAP dupa Show() — NU DELETE ALL, care lasa un rand fizic
sters si pointerul la EOF (cod care face GO <recno> da eroare 5, vezi
conventie_go_recno.md).
i. Cursorii trebuie sa aiba EXACT indecsii creati de fluxul real inainte de instantiere
(fluxul real ii creeaza in metoda apelanta, ex. un INDEX ON ... TAG ord_doc +
SET ORDER TO inainte de CREATEOBJECT). Fara ei cursorul ramane pe ordinea FIZICA, iar
logica de tip GO TOP + LOCATE FOR ... alege alt rand decat in productie - rezultat fals,
uneori in cascada la apeluri repetate. Verifica in codul apelant ce indecsi se creeaza si
replica-i identic.
j. Mock-urile de date trebuie sa reproduca structura si semantica REALA a sursei (view
Oracle: toate coloanele; relatii intre randuri: toate randurile implicate, nu una singura cu
valori "compuse" manual). Un mock incomplet lasa campuri NULL sau ascunde pasul testat. Cand
un flag global activeaza cod suplimentar (ex. gl406), acel cod cere mock-uri in plus -
verifica ce apeleaza si adauga metodele lipsa din dummy. Cu gl406=.T. (SAFT),
GetTaxCodeIdPart/GetTaxCode cer: goApp.ReadIni/WriteIni (clasa dummyapp are nevoie
de METODE, nu doar proprietate), goExecutor.oReset (no-op e suficient), si cursor
saft_taxtable real (nu doar mock pe goExecutor - update_jtva_coloane face
USE saft_taxtable direct pe alias). Fara ele, simptomul e dialog nativ Windows "Open" (cauta
saft_taxtable.dbf) care blocheaza headless la nesfarsit, fara linie noua in log si CPU 0% -
vezi depanare_testare_vfp.md pentru diagnostic cu EnumWindows/PrintWindow. Acelasi
simptom apare si FARA gl406: un control cu RowSourceType=3 legat direct in clasa de un SQL
pe saft_taxtable (ex. combo pe explicatie SAFT) isi evalueaza RowSource la constructia
formularului indiferent de gl406 sau de Visible=.F. - cursorul mock trebuie creat oricum.
k. LOCATE FOR camp == 'literal' pe camp C(n) padded nu gaseste nimic (== e exact) -
foloseste ALLTRIM(camp) == 'literal'.
l. CREATEOBJECT/ADDPROPERTY se comporta gresit apelate DINTR-O PROCEDURA in acest runtime
headless (obiect returnat ca string de 30 spatii; ADDPROPERTY da eroare 11). Din programul
PRINCIPAL merg. Construieste obiectele-parametru INLINE in main, nu intr-un helper. Cauza
confirmata intr-un caz concret: linie de cod prea lunga in ACEEASI metoda de clasa - vezi
depanare_testare_vfp.md sectiunea 6; remediul (constructie inline) ramane valabil oricum.
m. Fara SELECT-SQL pe cursorul legat de grid: SELECT ... FROM <cursor> INTO CURSOR cat
timp cursorul e RecordSource-ul unui grid viu poate omori procesul vfp9 silentios (fara
eroare catchabila, fara semafor). Foloseste xBase nativ (COUNT FOR ... TO, CALCULATE,
LOCATE), care nu comuta zona de lucru a grid-ului.
n. Wrapper-ul .ps1 trebuie sa paseze -SyncDir daca .prg-ul isi seteaza propriul
gcSyncDir - altfel testul scrie semafoarele intr-un folder si vfp_ui_harness.ps1 le
asteapta in cel implicit (uisync\), deadlock pana la timeout. Simptom identic cu "test
agatat", dar FARA .ERR si cu CPU ~0 la procesul vfp9 (nu un dialog modal - procesul chiar
asteapta un fisier care nu vine). Timeout minim: ~30s x numarul de pasi din -Steps
(auto-continue per checkpoint din HarnessWaitContinue), plus marja de pornire.
o. Formularul resincronizeaza singur dupa o alegere din grid (lSyncPending + tmrSync,
ex. do_modifica_explicatie_tva din ointroduceri.vcx): starea imediat dupa actiune NU e
observabila - DOEVENTS FORCE lasa timer-ul sa porneasca, iar resincronizarea rescrie ce
tocmai s-a aplicat (la explicatia TVA: randul S revine la familia documentului, randurile T se
realiniaza dupa el). Scrie assert-urile pe starea de DUPA resincronizare, pe un efect pe care
resincronizarea NU il repara (acolo: scc-ul randului T, atins doar la schimbarea explicatiei)
- altfel testul pica fara sa fie ceva gresit in cod.
p. Nu numi variabilele ca functii built-in VFP. VFP e insensibil la litere mari/mici, iar la
coliziune functia castiga. Caz real:
LOCAL loCkpentru referinta la un checkbox se ciocneste cuLOCK()—loCk.Value = 1da eroarea 1924 "LOCK is not an object", iarVARTYPE(loCk)evalueazaLOCK()si intoarce'L', deci verificarile de tipulVARTYPE(lo...) == 'O'ies tacut false si par sa spuna ca obiectul nu a fost gasit — masuratoarea e stricata, nu rezultatul. Simptom de recunoscut: mesajul de eroare contine numele cu majuscule al unei functii VFP, nu numele variabilei asa cum ai scris-o. Prefixeaza distinctiv (loBifa,loCtrl). q.DO ... WITHpaseaza PRIN REFERINTA, iar variabila devine invizibila sub numele ei propriu in procedura apelata. Consecinta urata: orice?variabiladin SQL passthrough de pe lantul apelurilor respective nu se mai poate lega, si VFP deschide dialogul nativ "View Parameter — Enter the value for<var>", care blocheaza testul headless la infinit (fara log, fara eroare). Dovedit pe date: o variabila globala era valida in programul principal la fiecare checkpoint, darTYPE()intorcea'U'(nedefinita) exact la intrarea in procedurile apelate cuDO proc WITH gnAn, gnLuna— si valida in cele apelate faraDO ... WITH— corelatie exacta cu sintaxa apelului, nu cu ce face procedura. Remediu: paranteze pe fiecare argument forteaza pasarea prin valoare —DO proc WITH (gnAn), (gnLuna). r. O suita care testeaza o clasa dinCOMUNpoate incarca implicit copia ALTUI PRODUS, daca harnessul de mediu comun (ex.test_init_env_auto.prg) faceSET DEFAULT TO/SET PATHpe working copy-ul altui produs (ex.D:\ROA\ROACONT\...) inainte deSET CLASSLIB TO <clasa> ADDITIVE:Createobjectgaseste prima clasa cu acel nume pePATH, care poate fiD:\ROA\ROACONT\COMUN\clase\<clasa>.vcxin loc de fisierul editat sub test — silentios, fara eroare. Simptom: testul trece curat, dar valideaza cod vechi/alt working copy; orice concluzie trasa asa e nula. Remediu, dupa initializarea mediului comun:RELEASE CLASSLIB <nume>+SET CLASSLIB TO <cale completa a fisierului sub test> ADDITIVE(SET CLASSLIB ... ADDITIVEsingur, faraRELEASEintai, da eroarea 24 "Alias name is already in use" si pastreaza tacut copia veche). Verificare obligatorie dupa instantiere:loForm.ClassLibrarytrebuie sa arate calea completa asteptata. s. Dialoguri native care nu apar in log si nu sunt prinse deON ERROR/mock-uri (ex. "View Parameter", "Open" pe fisier lipsa): nu se depaneaza orbeste, se ruleaza subCOMUN\utile\Teste\watchdog_vfp.ps1 -Script <test.prg> -AutoDismiss. Lanseazavfp9.exe -A -T, polleaza ferestrele top-level ale procesului (identificate prinProcess.MainWindowHandle, nu dupa numele clasei — VFP foloseste clasevfp9...si pentru shell-ul principal si pentru dialogurile proprii), captureaza PNG + textul fiecarui control copil (WM_GETTEXT) la orice fereastra noua, si omoara procesul la iesire (finally, niciodata ramas viu). Interzis strict: input real (keybd_event/SetForegroundWindow/SendInput/mouse_event) — masina e partajata, un input injectat la nivel de sistem ajunge in orice fereastra are focus in acel moment, nu neaparat in dialogul tintit (patit: un ESCAPE injectat a intrerupt sesiunea IDE a utilizatorului, aflat in lucru in paralel).-AutoDismissfoloseste STRICT mesaje Windows tintite pe handle (BM_CLICKpe buton Cancel/Anulare, altfelWM_COMMAND IDCANCEL-> ESCAPE ca mesajWM_KEYDOWN/WM_KEYUP->WM_CLOSE); pe dialogurile proprii VFP owner-drawn (butoane desenate, nu HWND-uri reale) dismiss-ul poate esua cinstit — captura PNG + dump text raman oricum de incredere, indiferent de rezultatul dismiss-ului. Sterge intotdeauna.fxp-ul vechi inainte de FIECARE rulare (watchdog-ul o face singur la lansare) — altfel VFP ruleaza tacut codul compilat vechi si rulari "identice" dau rezultate diferite, fara nicio urma in log.