Execute Tests Modernized - sebiboga/testlink-upgraded GitHub Wiki
Modernization of Execute Tests (lib/execute/execNavigator.php +
lib/execute/execSetResults.php) β GitHub issue
#662.
The legacy tree-navigator + execution-form pair is replaced by a standalone
Dashio two-pane page (gui/templates/execute/execTest.html) backed by a
plain-PHP REST BFF (api/execute/index.php, extended with four new actions).
The ASIDE Execute β Execute Tests entry now points to the new HTML screen
when a test plan is selected (switched in getActions() /
initUserEnv() in lib/functions/common.php).
Path: Execute β Execute Tests
URL: gui/templates/execute/execTest.html?tproject_id=<id>&tplan_id=<id>
BFF API: api/execute/index.php (same file also serves the Execution
History screen via ?action=history)
Rights: testplan_execute to record results, exec_ro_access for the
read-only variant β enforced server-side on every BFF route.

- Toolbar: build selector (only active+open builds are executable; closed ones are listed with a "(closed)" hint), platform selector (hidden when the plan has no platforms), result filter and live text search over external id, name and suite path.
- Left pane: all test cases linked to the plan (latest linked version per case) with suite path, version, last-execution timestamp on the selected build/platform and a colored status badge.
- Right pane: the execution form β summary, preconditions, importance, manual/automated marker, prior execution recap for this build/platform, result-status buttons (Not Run / Passed / Failed / Blocked), execution notes and step-level results (per-step status select + notes) with the recorded partial-step results pre-filled from the prior execution.
-
Save records one execution row through the legacy
exec.inc.php:write_execution(), so every side effect keeps working: requirement-link freezing, relation closing, step partial-execution rows. -
Read-only mode: users with only
exec_ro_accesssee everything but cannot save (inputs disabled; API rejects writes with 403).


All endpoints are GET unless stated otherwise; session auth required;
tplan_id must be one of the user's accessible test plans.
| Action | Params | Returns |
|---|---|---|
init |
tplan_id |
project/plan context, grants, builds (+executable flag, default build = newest active+open), platforms, localized status vocabulary |
tcList |
tplan_id, build_id, platform_id, optional q
|
latest linked version per case: external id, name, suite path, importance, execution type, last execution (status/ts/tester) |
tcDetails |
tplan_id, tcase_id, tcversion_id, build_id, platform_id
|
version block, ordered steps, prior execution incl. prior step results, and exec_history (all executions of this tcversion in the plan, DESC, with per-row run-mode/version/build/tester/status + can_edit_notes/can_delete grants) |
save (POST JSON) |
tplan_id,tcase_id,tcversion_id,version_number,build_id,platform_id,status(code),notes,steps{stepId:{status,notes}} |
saved:true + execution id/ts, or reason:'not_run'; validates executable build + plan link; delegates to write_execution()
|
deleteExecution (POST JSON) |
tplan_id, execution_id
|
deleted:true; gates: testplan_execute + exec_delete (public-plan-aware), execution must belong to the plan, build must be open; delegates to delete_execution() and logs an AUDIT event only on success
|
updateNotes (POST JSON) |
tplan_id, execution_id, notes
|
updated:true; gates: testplan_execute + exec_edit_notes, execution must belong to the plan, build must be open; delegates to updateExecutionNotes()
|
linkBug (POST JSON) |
tplan_id,execution_id,tcstep_id,bug_id |
validates bug syntax + existence via ITS, writes execution_bugs row, returns {ok:true,id}; audit-logged |
createBug (POST JSON) |
tplan_id,execution_id,tcstep_id,bug_summary,bug_description |
creates a real issue via its->addIssue() (GitHub returns new id), links it to the execution, returns {ok:true,id}; audit-logged |
Status codes travel as codes (n/p/f/b) exactly as served by init
(statuses[].code).
- Only active+open builds can receive results (legacy UI offered the same set; the BFF additionally rejects violations server-side).
-
Not Runresults are never written as executions (identical towrite_execution()behavior since 1.9.20) β the UI explains this via toast. - Plans without platforms store
platform_id = 0on execution rows (the client sends-1, mapped exactly like legacy$executedInPlatform). - Step results reuse the legacy
execution_tcstepsstorage; steps whose status is not_run and whose notes are empty are skipped like legacy. - The aside entry keeps its
menuGrantsgating (testplan_execute,exec_ro_accessfallback label) inaside.tpl.
Deliberate v1 scope cuts (tracked on #662): step-level attachment upload, execution custom fields in the form, bulk mode. Execution-level attachment upload/list + "Copy attachments from latest execution" are implemented (see Bugfix-Issue-792-Execute-Tests-Attachments).
Issue-tracker integration inside the execution form (legacy bugAdd.php
link / create / doCreate flows), enabled when the project has an
issue tracker configured and exec_cfg->features->issue_tracker->enabled.
-
Link existing bug β from the case toolbar or a specific step row, open
the Link bug to execution modal, switch to Link existing bug, type a
bug id and submit. BFF
postBugAction('linkBug')validates syntax + existence against the tracker (GitHub), thenwrite_execution_bug($db,$execId,$bugId,$tcstepId)stores theexecution_bugsrow (execution-level or step-level viatcstep_id). -
Create new bug β switch to Create new bug, fill Title (summary,
required) + optional Description, submit. BFF
postBugAction('createBug')resolves the tracker, requiresaddIssue()support, callsits->addIssue($summary,$description)to create a real GitHub issue, then links it to the execution and audit-logsBug {id} created and linked to execution {execId}. - Static-name calls guarded so CI
fix-bug.ymlnever picks up test issues. - The modal target line shows
Execution <id>(case) orExecution <id> on step N (<id>)(step).

The test-case details panel renders a Linked bugs DataTables card
(Bug ID / Title / Labels / Status / Step), fed real-time from the GitHub
API via the tcDetails linked_bugs array:
- Bug ID is a clickable GitHub link (
bug_url), unless the issue is deleted. -
Deleted / rejected GitHub issues (API returns HTTP
410) setunavailable:trueβ the row shows a grey Deleted status badge, an "Unavailable" title and no dead link (#805/#806demonstrate this; live issues show real title/labels/status). -
Labels render as GitHub-colored badges.
githubrestInterface::getIssue()andlistIssues()exposelabel_colors(nameβhex) alongsidelabels;execTest.html renderLinkedBugsTable()uses the same map; missing color falls back to grey#6c757d. - BFF dedupes by bug id and aggregates the steps each bug is bound to
(
steps) plus the number of executions (exec_count). - The same
unavailable+ GitHub label-color handling is mirrored in the Dashboard (mainPage.htmlbugs + issues tables,dash.bugDeleted/dash.bugUnavailable).
All strings use the exe.* prefix, defined in ALL ten client bundles:
en, de, es, fr, it, ja, pt, ro, ru, zh (46 keys each after the Linked Bugs
table keys exe.bugDeleted / exe.bugUnavailable), consumed through
TLi18n. Server-side labels (statuses) come from lang_get('test_status_*')
like legacy localize_tc_status().
- Session auth + same-origin guard (
bffSameOriginGuard) on every request. -
testplan_resolveContext: plan must be within the user's accessible plan set for the owning project (else 403/404). - Write route requires
testplan_executeon (project, plan);exec_ro_accessalone yields HTTP 403. - All ids cast to int; notes prepared inside legacy
prepare_string()paths.
Suite 63 in tmp/TLU_Test_Cases.md: 14/14 PASS β covers init/list/detail
contracts, full UI save flow, filters/search, not-run guard, closed-build
guard, read-only path (banner + disabled UI + 403), locale switch and Event
Viewer cleanliness after fixes.
Fixes landed during testing:
- options blob read instead of vestigial
option_platformscolumn (E_WARNING in Event Viewer); -
locale/en_GB/custom_strings.txtaddingfile_upload_step_exec_ok/ko(surfaces #621's missing en_GB keys on every step-level save).
write_execution() (lib/functions/exec.inc.php:257-258) evaluates
lang_get('file_upload_step_exec_ok'/'ko') on every step-level save. When the
active locale lacks the keys, lang_get() falls back to en_GB and writes a
string '...' is not localized for locale '<loc>' - using en_GB event
(log_level 32) into the Event Viewer.
Root cause / coverage: the keys were added to 16 locales' custom_strings.txt
and lived in strings.txt for pt_BR/pt_PT, but ro_RO had neither (no
custom_strings.txt, 0 hits in strings.txt) β a 19-locale scan confirmed
ro_RO was the only missing bundle.
Fix (Refs #621): created locale/ro_RO/custom_strings.txt with both keys
(UTF-8 Romanian translation, same Fixes #621 / #662 header convention as the
16 existing bundles).
Verification (regression suite 621, 5/5 PASS):
- ro_RO save pre-fix β 2 L18N events (reproduced, events id 9/10);
- ro_RO save post-fix β 0 new
file_upload_step_execevents; - en_GB + pt_BR saves β unchanged (still 0);
- browser UI save on the modernized Execute Tests screen β "Execution saved." with no console errors and no L18N events.
Side-discoveries logged as new issues (out of this fix's scope):
- #773 β
Undefined property: stdClass::$requirementsEnabledE_WARNING on every execution save whentestprojects.optionsis empty; - #774 β other ro_RO server-side keys (
manual,automated,the_format_tc_xml_import) still missing.

Symptom: opening a test case in the modernized Execute Tests screen showed no inline execution-history table at all. The legacy 1.9.20 screen had, under the prior-execution note line, a full history table with per-row edit-notes / delete / print actions and run-mode icons.
Root cause: the modernized execTest.html simply never rendered that
table β the BFF tcDetails response carried no execution-history payload and
there were no BFF actions for edit-notes / delete, so 1.9.20 functionality was
silently dropped during the rewrite.
Fix approach (restore 1.9.20 parity, following the legacy code paths):
-
BFF (
api/execute/index.php):tcDetailsnow also returnsexec_historyβ the full execution set for this tcversion+plan (all builds, DESC), each row withbuild_name/build_is_open, tester login/name, localized status,execution_ts/execution_duration/run_type,tcversion_number, pluscan_edit_notes+can_deleteper-row grants (fromexec_edit_notes/exec_delete+ open build). Two new write routes:-
deleteExecutionβ mirrors legacyexecSetResults.phpdelete flow (exec_deleteright, open build, execution belongs to the plan); callsdelete_execution()(cleans execution_bugs / cfield_execution_values / execution_tcsteps / attachments) and writes an AUDIT event only on success. -
updateNotesβ mirrors legacyeditExecution.phpdoUpdate (testplan_execute+exec_edit_notes, open build, plan ownership); callsupdateExecutionNotes().
-
-
Frontend (
gui/templates/execute/execTest.html): newrenderHistoryHtml()renders the EXECUTION HISTORY table (Date/time, Build+lock icon, Executed by, Status badge, Run-mode icon, Version, Actions) plus a per-row notes line. Row actions: edit-notes β legacyeditExecution.phppopup; print β legacyexecPrint.phppopup; delete β confirm dialog β BFFdeleteExecutionβ refresh form + list. -
i18n: added
exe.executionHistory / actions / deleteExecution / confirmDeleteExecution / deletedOk / errDeleteandexechist.closedBuildto all ten client bundles; also repaired pre-existing corrupted English/Romanian values for theexechist.*keys the table uses.
Verification (regression suite 796, 7/7 PASS, see tmp/TLU_Test_Cases.md):
init/list/detail contracts unchanged; history table renders all runtime rows
with per-row action buttons; edit-notes popup preloads the execution notes;
print popup renders the full execution; delete removes the execution + child
rows and logs an AUDIT delete event; rights/state gates confirmed by code
review (testplan_execute + fine-grained right, plan ownership, open build);
Event Viewer clean (only AUDIT events, no new Error/Warning).

Symptom: the modernized Execute Tests screen had no way to save partial
step results for later resumption. Legacy 1.9.20 rendered a
"Save Steps Work In Progress Execution" button under the steps table
(inc_exec_test_spec.tpl β TLS_saveStepsForPartialExec) that wrote
execution_tcsteps_wip rows; the rewrite dropped the button, the BFF action
and the WIP read-back, so mid-run progress could be lost.
Root cause: three missing pieces during the #662 rewrite, all present in legacy:
- no button in the form;
- no BFF route (legacy:
execSetResults.phpβtestcase::saveStepsPartialExec()); -
tcDetailsdid not mergeexecution_tcsteps_wipintoprior_step_results, so saved WIP rows never pre-filled the form on reopen.
Fix approach (restore 1.9.20 parity):
-
BFF (
api/execute/index.php):- new
POST ?action=savePartialStepsβ gatestestplan_execute(403), active+open build (400), validtcversion_id(400); validates every step id against the version (tcsteps+nodes_hierarchyjoin, forged ids dropped) and every status viaresults.code_status(fallbacknot_run); delegates totestcase::saveStepsPartialExec()withtestplan_id,platform_id(normalized>0 ? id : 0),build_id,tester_id= session user. Legacy delete-then-insert semantics preserved (a resave replaces the tester's WIP rows for that plan/build/platform). -
tcDetailsnow merges WIP rows over prior executed steps for the current (plan, build, platform, tester) context β the resume pre-fill.
- new
-
Frontend (
gui/templates/execute/execTest.html): non-read-only forms with steps render theSave Steps Work In Progress Executionbutton + a hint;savePartialSteps()replicates legacycheckStepsHaveContentclient-side ("nothing to save" toast, no request), POSTs the same step payload shape assaveExecution(), then toasts on success and re-opens the case so resumed values show. -
i18n:
exe.partialExecInfo / partialExecNothingToSave / partialExecSaved / savePartialStepsadded to all ten bundles; button label reuses the legacy English string, translations reuse legacylocale/*/strings.txtwhere present.
Verification (regression suite 64, 11/11 PASS, see tmp/TLU_Test_Cases.md):
partial save writes only execution_tcsteps_wip (no executions row); empty
save is blocked client-side (no POST, DB untouched); resume pre-fill in-session
and across full reloads; full "Save execution" after a partial save clears the
WIP rows (legacy exec.inc.php flow) and writes the execution + step rows;
zero-right user β 403; inactive build β 400; forged step id dropped; Event
Viewer clean (only INFO-level AUDIT rows).

Before: issue-795-execTest-no-partial-save.png (no button).
Symptom: the modernized Execute Tests screen offered only Save execution
and Reset in the save row. The 1.9.20 toolbar buttons
btn_save_exec_and_movetonext ("Save and move to next") and btn_next
("Next" β move to the next test case WITHOUT saving) were dropped in the #662
rewrite, and the BFF init never surfaced exec_mode->save_and_move, so the
configured navigation mode could not be honored.
Root cause: three pieces missing during the rewrite, all present in legacy
(lib/execute/execSetResults.php doExecSave()/move2next window at lines
140-345 + 617-632, gui/templates/tl-classic/execute/inc_exec_controls.tpl lines
89-97):
- no "Save and move to next" button / BFF save-then-navigate flow;
- no "Next" (navigate, no save) button / BFF
move2nextequivalent; - no read of
$tlCfg->exec_cfg->exec_mode->save_and_move(config.inc.php:1119, default'unlimited') in the BFFinitresponse.
Fix (restore 1.9.20 parity):
-
BFF (
api/execute/index.php):initnow returnssave_and_movefromconfig_get('exec_cfg')->exec_mode->save_and_move(fallback'unlimited'). -
Frontend (
gui/templates/execute/execTest.html): extractedvisibleItems()(the exactrenderList()filter predicate β navigation always matches what the user sees);saveAndMoveMode();execNextTarget()β cyclical successor in the visible set,'unlimited'= whole list,'limited'= members of the SAME test suite (grouped by displayedtsuite_path, the flat-list equivalent of legacygetTestCaseNextSiblingβ note the BFFtsuite_idfield actually carries the test-case node id, not the suite id, and is unused by the frontend);moveToNext()= legacymove2next(no save);saveExecution('next')saves thenloadList()+openCase(next), successor computed pre-save like legacy. Save row: Save execution Β· Save and move to next Β· Next Β· Reset, all sharing the existingroModedisabled guard. -
i18n:
exe.saveAndMoveNext/exe.moveToNext/exe.noNextCasein all ten bundles (legacylocale/*/strings.txttranslations reused).
Note β bulk results mode: the legacy bulk results grid is a testsuite-level
feature ($args->level == 'testsuite'); the modern flat-list screen has no
testsuite view, so bulk execution is out of scope here.
Verification (regression suite 69, 12/12 PASS, see tmp/TLU_Test_Cases.md):
BFF init returns save_and_move:"unlimited"; four-button save row; save-and-move
writes the execution and advances to the successor; move-to-next navigates WITHOUT
writing; unlimited cycle wraps; limited mode scoped per suite; search-filtered
navigation honors the visible subset; plain Save unchanged; i18n bundles valid
10/10; Event Viewer clean (no new Error/Warning rows).

Before: issue-793-save-and-next-before.png (only Save execution + Reset).
The Execute Tests toolbar now shows a collapsible Test Plan Notes, Build
Notes and Platform Notes panel fed by the BFF init response
(tplan.notes, builds[].notes, platforms[].notes). Each section toggles
closed/open on click (mirroring legacy execSetResults.tpl
show_hide('tplan_notes'|'build_notes'|'platform_notes')). Build and Platform
boxes auto-hide when their note is empty; the whole panel disappears when there
is nothing to display. Notes re-render whenever the selected build or platform
changes. i18n keys exe.tplanNotes/exe.buildNotes/exe.platformNotes added
to all 10 bundles.

Verification (suite 1163, 7/7 PASS): all three panels render; plan notes toggle collapse/expand; empty build/platform boxes hide; empty-all hides the panel; notes follow build selection; console clean; Event Viewer clean.