Test Case Editor Modernized - sebiboga/testlink-upgraded GitHub Wiki
Modernization of the legacy "test case version editor" screen
(lib/testcases/tcEdit.php, gui/templates/dashio/testcases/tcEdit.tpl) —
GitHub issue #812.
The legacy Smarty editor is replaced by a standalone Dashio page
(gui/templates/testcases/tcEdit.html) backed by a plain-PHP REST BFF API
(api/testcasesedit/index.php). The test case viewer toolbar Edit button
(openTcEdit) now opens the modern screen instead of the legacy tcEdit.php
redirect — this was the last remaining HIGH-priority legacy redirect from
issue #753 (tcExport.php, tcCompareVersions.php, tcAssign2Tplan.php were
already converted in #803/#809/#810).
Path: Test Spec → Test Case Viewer → toolbar Edit
URL: gui/templates/testcases/tcEdit.html?tcase_id=..&tcversion_id=..&tproject_id=..
BFF API: GET /api/testcasesedit/?action=edit + POST /api/testcasesedit/?action=update + POST /api/testcasesedit/?action=create_version
Rights: mgt_modify_tc for edit/update; editing an executed version is
gated by testproject_edit_executed_testcases (parity with the legacy
controller's edit-permission logic).
Tracking issue: #812

Issue #1317 — the
legacy editor (tcEdit.tpl:93-98) wires checkmodified.js when
$tlCfg->gui->checkNotSaved is enabled, so leaving the page with unsaved edits
triggers a "You have unsaved changes" prompt. The modern screen mirrors that:
-
ctx.content_modified/ctx.show_modified_warningflags plusmarkModified()/resetModified()helpers (tcEdit.html). -
doBeforeUnload()installed onwindow.onbeforeunload, returning the localisedtcedit.unsavedWarningmessage only when the form is dirty and the warning is armed. - Dirty tracking is event-delegated on the editor card:
input/changeon.form-control, input[type=text], select, textarea, plusclickon keyword pills and step-delete buttons; Add step marks the form modified too. -
renderEditor()callsresetModified()so prefilling never flags a clean form; a successful Save resets the flag; Cancel clearsshow_modified_warning(mirrors the legacy "suppress on Save/Cancel"). - i18n key
tcedit.unsavedWarningadded to all 10 locale bundles.
Because the modern editor uses plain inputs/textareas (no CKEditor), the legacy
editorChanged()/IsDirty() detection is replaced by the delegated event
tracking described above.

Issue #1316 — the
initial modernization built the Status select (tcEdit.html) from hardcoded
i18n keys tcedit.statusN, whose labels diverged from the legacy domain: 4
"Reviewed" (legacy Rework), 5 "Rework" (legacy Obsolete), 6 "Obsolete"
(legacy Future), 7 "Future" (legacy Final). Every stored
tcversions.status >= 4 was displayed with the wrong meaning, and saving e.g.
"Future" silently converted a Future into a Final.
Fix (config-sourced, same helper as the already-aligned tcBulkOp/suiteView):
-
api/testcasesedit/index.phpaddsstatusLabelsto theeditpayload —getConfigAndLabels('testCaseStatus','code')(legacycfg/const.inc.phptestCaseStatus+ localetestCaseStatus_*strings), keyed by numeric code. -
tcEdit.htmlbuilds the dropdown fromstatusLabels(codes sorted ascending) with a defensive fallback map; the hardcoded1..7loop and thetcedit.statusNi18n lookup are gone. - The obsolete
tcedit.status1..7keys were removed from all 10 locale bundles; the labels are now a single server-side source of truth.
Legacy domain served: 1 Draft, 2 Ready for review, 3 Review in progress, 4 Rework, 5 Obsolete, 6 Future, 7 Final.

The BFF mirrors lib/testcases/tcEdit.php + testcaseCommands:
-
Load (
action=edit) resolves the test case version through its position in the version chain, validates it belongs to the project, and returns its full content: name, summary, preconditions, ordered steps (actions + expected results), importance, status, execution type, estimated execution duration, and the project's keywords. It also computeskeywords(assigned) andall_keywords. -
Rights (
mgt_modify_tc) are enforced on every route; a user lacking the right receives HTTP 403No permission. -
Executed-version gate: when the current version has already been
executed, editing is only permitted for users carrying
testproject_edit_executed_testcases; otherwise the BFF refuses the edit. -
Save (
action=update) persists viatestcase::update()(name on thenodes_hierarchynode, summary/preconditions/steps/expected_results on thetcversionsrow) plus$attrforstatusandestimated_exec_duration, and syncs the keywords. Empty titles are rejected (HTTP 400Title must not be empty); bad durations rejected with a client-side guard. -
Create New Version (
action=create_version) clones the current version as a new Draft version (version = max+1) with steps copied, then returns the new version identity so the screen can switch to editing it.
| Section | Description |
|---|---|
| Heading | "Edit Test Case" + test-case identity (PREFIX-<id>:<name> (#<id> v<version>)) |
| Toolbar | Name/title input, Importance / Status / Execution Type / Estimated duration selects, locale switcher |
| Description | Summary textarea |
| Preconditions | Preconditions textarea |
| Steps | Data-list of (actions, expected results) rows with Add step / remove buttons |
| Keywords | Assigned-project-keyword pills |
| Actions | Save, Cancel, Create New Version |
| Overlay/toast | Loading overlay + Bootstrap-toast messages for save/errors/confirms |
Issue #1315 — the
initial modernization called
create_new_version($tcaseId, $userId) without the submitted tcversion_id,
so testcase::create_new_version() (testcase.class.php:2445-2448) fell back to
the latest version as clone source, and the BFF never froze the source
version. Parity with the legacy controller (lib/testcases/tcEdit.php
createNewVersion():838-852) restored:
-
Source of the clone = the version being edited. The front-end already
POSTed
tcversion_id: ctx.tcversionId(doNewVersion():512-517); the BFF now passes that$tcverIdas the third$source_version_idargument ofcreate_new_version(). A v1 → "Create New Version" now clones v1, not the latest v2. -
Source freeze (legacy switch). After a successful creation the BFF reads
config_get('testcase_cfg')->freezeTCVersionOnNewTCVersion(default TRUE,config.inc.php:1352) and appliessetIsOpen($tcaseId, $sourceTCVID, !freeze)— the edited version is closed (is_open=0) so it can no longer be modified; the new draft version is open. -
Configurable like legacy. With
freezeTCVersionOnNewTCVersion=FALSEthe source version stays open after the clone; the clone source is still the edited version. -
Fallback path. When
tcversion_idis omitted,resolveContext()defaults to the latest active version — matching the legacydo_create_new_version_from_latest(tcEdit.php:280-282), which also goes through the same freeze logic. - Rights check unchanged:
mgt_modify_tcon the owning project (403).

Regression Suite 1315 in tmp/TLU_Test_Cases.md — 6/6 PASS (default-config
clone/freeze, keyword no-leak, configurable freeze=FALSE, from-latest fallback,
editor reload, clean Event Viewer).
- On load the page calls
GET /api/testcasesedit/?action=editwithtcase_id,tcversion_id,tproject_id. The BFF resolves and validates the version, checks rights, and returns the version content. - The screen renders the form with all fields prefilled.
-
Save POSTs
?action=updatewith the full payload (name, summary, preconditions, steps, importance, status, exec type, duration, keyword ids). On success a toast shows "Test case saved." and the form reloads the saved version. - Add step appends a new (actions, expected results) row client-side; remove deletes a row; at least one step may be kept.
-
Create New Version confirms, then POSTs
?action=create_version; on success the screen switches to editing the freshly created Draft version. - Cancel closes the editor.
-
GET ?action=edit&tcase_id=..&tcversion_id=..&tproject_id=..→200with the version content (tcase.name,summary,preconditions,steps[],importance,status,exec_type,estimated_duration,keywords,all_keywords,grants,has_been_executed, identity).403when the user lacksmgt_modify_tc;404when context invalid. -
POST ?action=update(JSON) →200 {status:ok};400on empty title;403no right. -
POST ?action=create_version(JSON{tcase_id, tcversion_id, tproject_id}) →200with new version identity; clones the editedtcversion_id(the POSTed one; falls back to the latest active version when absent) and, whentestcase_cfg.freezeTCVersionOnNewTCVersionis on (default), freezes the source version (is_open=0);403no right. - Any other action →
404; no session →401.
All strings use the TLi18n module (gui/templates/i18n/i18n.js) with the
tcedit.* key prefix, present in all 10 locale bundles
(en/ro/de/fr/es/it/pt/ja/ru/zh.json).
Executed as regression Suite 812 — 17/17 PASS
(tmp/TLU_Test_Cases.md), covering load, render, save (fields/name/attrs),
add-step, create-version, empty-title/bad-duration guards, the three rights-deny
paths (edit/update/create_version HTTP 403), unknown-action 404,
unauthenticated 401, bad-context 400, i18n in all bundles, the tcView link
switch, and a clean Event Viewer (0 new Error/Warning rows).