# 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

1. `.prg` de test: `PUBLIC gcUILog, gcSyncDir`; `SET SAFETY OFF` + `SET TALK OFF` ca PRIMA
   linie (vezi capcana a); `SET PATH/CLASSLIB/PROCEDURE` ca in aplicatie; incarca ULTIMELE
   `ui_harness.prg` si (daca ai dialoguri) `mock_amessagebox.prg`, cu `ADDITIVE`.
2. Creeaza dummy-urile minime (`goExecutor` cu metode reale, `goApp` pt. `ReadIni`, `poAct`
   pt. curs valutar) + cursoarele cerute de Init (RecordSource-urile grilelor).
3. Instantiaza clasa, `WindowType=0` (modeless - `Show()` pe modal nu returneaza), `Show()`,
   `WindowState=2`.
4. 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 final `DO HarnessDone WITH 'done'`.
5. `powershell -File vfp_ui_harness.ps1 -TestPrg <test.prg> -Steps @('pas0',...)`.
   PNG-uri in `screenshots\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:

1. `_SCREEN.AddObject('tmrDrv','Timer')`, `Interval = 200`,
   `BINDEVENT(_SCREEN.tmrDrv, 'Timer', <obiect driver>, 'Executa')`, `Enabled = .T.`
2. `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.
3. **Detectia formularului: `_SCREEN.Forms(i)` / `_SCREEN.FormCount`**, potrivit pe `.Class`.
   **`_SCREEN.ActiveForm` NU merge headless** — a ramas gol la toate cele 229 de tick-uri: depinde
   de `WM_ACTIVATE`, care nu se declanseaza fara focus real, pe cand colectia `Forms` contine
   formularul indiferent de activare.
4. **Referinta la controale**: `loForm.<nume>` merge normal; iterarea `loForm.Controls(i)` cu
   potrivire pe `.Name` e 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 cursorului `saft_taxtable`, capcana j, a lasat instanta cu 21 de controale in loc de
   25, iar controlul urmator din ordinea de adaugare "disparea"). Verifica intai `ControlCount` si
   lista de nume.
5. **Inchiderea**: `do_termin()` (gateaza pe `inainte_de_do_termin()`, seteaza `gnButon = 1` si
   elibereaza — `_frm_base.vc2:363-371`) sau `do_renunt()`. **Daca validarea respinge, `do_termin()`
   nu seteaza `gnButon` si NU elibereaza formularul, deci `Show(1)` ramane blocat** — in testele
   care asteapta o respingere de validare, iesi prin `do_renunt()`.
6. **Driverul se instrumenteaza obligatoriu**: log pe fiecare pas, `TRY/CATCH` cu
   `ErrorNo`/`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
  foloseste `COMPOBJ()`.

## 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 loCk` pentru referinta la un checkbox se ciocneste
   cu `LOCK()` — `loCk.Value = 1` da eroarea **1924 "LOCK is not an object"**, iar `VARTYPE(loCk)`
   evalueaza `LOCK()` si intoarce `'L'`, deci verificarile de tipul `VARTYPE(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 ... WITH` paseaza PRIN REFERINTA, iar variabila devine invizibila sub numele ei propriu in
   procedura apelata.** Consecinta urata: orice `?variabila` din 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, dar `TYPE()` intorcea `'U'` (nedefinita) exact la intrarea in procedurile apelate cu
   `DO proc WITH gnAn, gnLuna` — si valida in cele apelate fara `DO ... 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 din `COMUN` poate incarca implicit copia ALTUI PRODUS**, daca
   harnessul de mediu comun (ex. `test_init_env_auto.prg`) face `SET DEFAULT TO`/`SET PATH` pe
   working copy-ul altui produs (ex. `D:\ROA\ROACONT\...`) inainte de `SET CLASSLIB TO <clasa>
   ADDITIVE`: `Createobject` gaseste prima clasa cu acel nume pe `PATH`, care poate fi
   `D:\ROA\ROACONT\COMUN\clase\<clasa>.vcx` in 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 ... ADDITIVE`
   **singur**, fara `RELEASE` intai, da eroarea 24 "Alias name is already in use" si pastreaza
   tacut copia veche). Verificare obligatorie dupa instantiere: `loForm.ClassLibrary` trebuie sa
   arate calea completa asteptata.
s. **Dialoguri native care nu apar in log si nu sunt prinse de `ON ERROR`/mock-uri** (ex. "View
   Parameter", "Open" pe fisier lipsa): nu se depaneaza orbeste, se ruleaza sub
   `COMUN\utile\Teste\watchdog_vfp.ps1 -Script <test.prg> -AutoDismiss`. Lanseaza `vfp9.exe -A -T`,
   polleaza ferestrele top-level ale procesului (identificate prin `Process.MainWindowHandle`, nu
   dupa numele clasei — VFP foloseste clase `vfp9...` 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). `-AutoDismiss` foloseste STRICT mesaje Windows
   tintite pe handle (`BM_CLICK` pe buton Cancel/Anulare, altfel `WM_COMMAND IDCANCEL` ->
   ESCAPE ca mesaj `WM_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.
