Modernize Test Plan Navigator planNav - sebiboga/testlink-upgraded GitHub Wiki
The legacy Test Plan Navigator frames β lib/plan/planTCNavigator.php +
lib/plan/planAddTCNavigator.php (with gui/templates/dashio/plan/planTCNavigator.tpl
-
planAddTCNavigator.tpl) β are modernized as a single Dashio hub backed by a REST BFF. The legacy frames were opened from the Test Plan screens' workframe buttons and showed a navigable tree of the plan's test cases (or its requirement coverage) together with the plan-action buttons that loaded the workframe controllers. The modern hub reproduces all of that: plan picker, group-by (Test suites / Requirement coverage), suite tree with deep linked/total quantities, requirement coverage tree, build selector, and the five planning-action deep links (incl. an Event Viewer action, legacyshow_veparity), forwardingtestproject_id/tplan_idexactly like the legacy workframe did.
-
Screen:
gui/templates/plans/planNav.htmlβ Dashio hub: teal header ("Test Plan Navigator", TLi18n locale switcher), context line (Test Project: <name>,Test Planselect incl. inactive plans,Group byselect,Refreshbutton), a Plan actions panel with four cards (Add/Remove Test Cases, Update linked TC versions, Test urgency, TC execution assignment, Event Viewer), the navigator tree panel and the Item details panel. -
Test suites mode: one row per suite (deep
linked/totalbadge), built from/suitesβ includes only suites that carry decorated test cases anywhere in their subtree (upstream containers stay closed, so the top level lists real suites, not the project id), with per-suitetotal_qty(test case nodes) andlinked_qty(distinct test-case versions linked to the plan) aggregated bottom-up so a parent shows the sums of its children. -
Requirement coverage mode: one row per req spec (
covered/totalbadge) with the requirements as children; each requirement shows aβwhen at least one of its linked tcversions is linked to the selected plan, plus the total number of linked tcversions after it. -
Item details panel: localizes and formats the row of the clicked node β
suite β Linked / Total, spec β Requirements / Covered / Total, requirement β
Linked test cases / Covered Yes|No (green
Good/ redBad). -
Plan switch re-boots the hub for the newly selected plan: the tree AND the
four action links both refresh with the new
testplan_id(regression fixed this run β the links kept the original plan id). -
No-rights degrade: a user with no accessible test plan gets an empty hub
(project
-, empty plan combo) plus the localizednoAccesstoast β the BFF answers 403 and the screen degrades gracefully instead of crashing. -
Build selector: shown only when the user has
exec_assign_testcasesand the plan has active+open builds (fromget_builds_for_html_options(ACTIVE, OPEN)) β legacytcExecAssignmenttarget parity.
-
Endpoint:
api/plannav/index.php(session auth +bffSameOriginGuard), routes:-
GET /init?tproject_id=N[&tplan_id=M]β context:tproject_id/name,plans[](id/name/active,getAccessibleTestPlans(active=null)so inactive plans stay selectable),builds{}+default_build_id,rights(canPlan,canUpdateTC,canUrgencyβtestplan_planning;canAssignβexec_assign_testcases;canViewEventsβmgt_view_events), andactions[]β the five deep links withparams=testproject_id=<tproject>&testplan_id=<tplan>, nulled (locked card) when the right is missing. -
GET /suites?tproject_id=&tplan_id=β the suite subtree WITH RECURSIVE (testsuite-typed descendants of the project; anchored at the project root β the legacycontainer_iddrill param is ignored, which also closes a cross-project IDOR), per-suite direct totals + plan-linked distinct tcversions, aggregated bottom-up intodeep_total_qty/deep_linked_qty. -
GET /reqs?tproject_id=&tplan_id=βreq_specsof the project, theirrequirements, and per requirementlinked_qty(distinct linked tcversions) +covered_qty(of those, the ones whose tcversion is linked to the target plan, viatestplan_tcversionsLEFT JOIN); spec nodes carrycovered_qty/total_qty(count of covered requirements / count in spec).
-
-
Rights: no page-level legacy right β any authenticated user with β₯ 1
accessible test plan may load the hub (the legacy navigators only ran
testlinkInitPage); per-feature flags mirror the modern plan screens' own gates. 401 anon / 403 no accessible plans / 404 bad test plan (cross-project or nonexistent) / 400 missing params; unknown route β 404 JSON. - Error contract: all errors return JSON; error paths write no event rows.
-
$actions->planNavadded inlib/functions/common.php(afterusersAssign): both legacy navigator controllers are now session-guarded 302 shims βgui/templates/plans/planNav.html?tproject_id=&tplan_id=(forwarding the deep-link params). The modern hub is reached from the plan screens' workframe buttons exactly like the legacy frames;frmWorkArea.phpforwarding was pinned to the modern hub.
pnav.* keys (30: title, subtitle, group-by labels, tree headers, detail-row
labels, action cards, open/no-right buttons, noAccess toast, empty states) plus
the reused helpers pv.testProject, paddtc.testPlan, paddtc.build,
common.refresh, common.error and footers.planNav β in all 10 locale
bundles (validated with python3 -m json.tool). No hardcoded strings.
-
Plan switch left the action links on the original plan β after switching
the test plan the tree refreshed but all four action-card links still carried
the first plan's
testplan_id, so the buttons would have acted on the wrong plan. The change handler now re-boots the hub with the selected plan id (boot(parseInt(...))), which refreshesINITand therefore the actions too. -
Code-review fixes (this run):
/suitesaccepted an arbitrarycontainer_idas the CTE root β cross-project suite-tree disclosure (now anchored at the owning project);/reqscovered_qtycould double count a tcversion linked under tworeq_coveragereq-versions (COUNT(DISTINCT CASEβ¦)applied); response charset/nosniff headers added; the legacyshow_ve(plan Event Viewer) navigator action is now a fifth hub action card (eventviewer.html?tproject_id=&tplan_id=, gated onmgt_view_events); action hrefs HTML-escape&; Refresh resets the selection.
- Fresh DB + fixture
tmp/fixtures_1572.php(project NAV1572/NA7, Suite A with child Suite A1, TCs A1/A2/A1x/B1/B2 with revisions, plan Plan NAV1572 linked A1+B1+B2 + second unlinked plan Plan NAV Alt, req spec Navigator Spec + req1/req2 +req_coveragereq1βA1, req2βB1; no-rights usernorights/norightsviatmp/mkuser_norights.php). - BFF contract:
/init200 (rights all true, all 4 actions with params, plans [72,73], builds{}),/suites+/reqs200 with the expected counts, 400 missing params, 404 bad plan, 404 unknown route, anon 401. - Browser (EN + RO): suite tree deep counts (Suite A 1/3, Suite A1 0/1, Suite B
2/2), coverage mode (spec 2/2 covered; both reqs
β 1), plan switch refreshes tree + action links both directions (72 β 73; unlinked plan β 0/N + 0/2 + no β), detail panel variants, no-rights 403 degrade, legacy-shim 302s, ENβRO renders. - Event Viewer: 0 new ERROR/WARNING (audit-only rows). Console: 0 errors.




Follow-up hardening of the planNav REST BFF (api/plannav/index.php) and the
screen JS, resolving the six items raised during the #1572 code review.
-
DB-error JSON contract: every route is wrapped in
try { β¦ } catch (Throwable $e); on a DB failure the API now exitsHTTP 500with{"status":"error","message":"Internal error"}instead of leaking the legacy HTML error page. -
405 for known non-GET routes, 404 for unknowns:
/init,/suites,/reqsunder POST/PUT/DELETE return405 "Method not allowed"JSON before the 404 fallback. -
keyword_id filter parity: the keyword join is applied to the deep
linked-count aggregation too β a filtered suite reports zero linked counts
instead of
total=0 deep_linked=2. -
Session default plan: a param-less load re-opens
$_SESSION['testplanID'](bare?testproject_id=1shows the last-used plan 20, not newest). -
O(N) deep aggregation: single bottom-up post-order pass replaces nested
per-branch queries β 4000-suite chain
/suites~0.367 s β ~0.028 s.
- Request-token guards
initSeq/treeSeq: every boot/tree fetch increments its token and stale deferred handlers early-return, so a slow response for an old plan/keyword/group-by can never overwrite the new tree;boot()also bumpstreeSeqon entry (a plan switch invalidates an in-flight tree immediately) and restores the plan select to the last good plan on failure. Item-detail rendering is synchronous from the fetched tree, so it needs no separate guard.
- Suite 1573 (6/6 PASS) appended to
tmp/TLU_Test_Cases.md; Event Viewer clean. -
curl: 405/404/500 all JSON; kw parity; session default plan 20; 4000-chain 0.028 s. - Browser: tree 1/3Β·2/2Β·0/1, reqcov 2/2 ββ, plan+group-by switching, param-less reload β plan 20, all [200], 0 JS errors, race bursts settle correctly.


Suite 1573 (6/6 PASS) appended to tmp/TLU_Test_Cases.md.