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
215 lines
17 KiB
Markdown
215 lines
17 KiB
Markdown
# 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.
|