diff --git a/doc/newsfragments/15721_new.testcase_parameter_filter.rst b/doc/newsfragments/15721_new.testcase_parameter_filter.rst new file mode 100644 index 000000000..0a46044d4 --- /dev/null +++ b/doc/newsfragments/15721_new.testcase_parameter_filter.rst @@ -0,0 +1 @@ +Added an Extended Search button for filtering and navigating parametrized testcase permutations in reports. diff --git a/testplan/common/serialization/fields.py b/testplan/common/serialization/fields.py index d80560b3c..2b0e83f87 100644 --- a/testplan/common/serialization/fields.py +++ b/testplan/common/serialization/fields.py @@ -7,7 +7,17 @@ import pprint from datetime import timezone, datetime -from typing import Any, Dict, List, Optional, Tuple, Type, Union, cast +from typing import ( + Any, + Callable, + Dict, + List, + Optional, + Tuple, + Type, + Union, + cast, +) from boltons.iterutils import is_scalar, remap from lxml import etree @@ -24,6 +34,7 @@ # orjson's native int range; outside this it raises _INT64_MIN = -(2**63) _UINT64_MAX = 2**64 - 1 +_MAX_TEXT_LENGTH = 1000 # pylint: disable=unused-argument @@ -140,7 +151,9 @@ def _repr_obj(obj: object) -> str: return object.__repr__(obj) -def native_or_pformat(value: Any) -> Any: +def native_or_pformat( + value: Any, formatter: Callable[[Any], Any] = pprint.pformat +) -> Any: """Generic serialization compatible value formatter.""" if comparison.is_regex(value): value = "REGEX({})".format(value.pattern) @@ -159,11 +172,24 @@ def native_or_pformat(value: Any) -> Any: elif type(value) in JSON_SAFE_SCALARS: result = value else: - result = pprint.pformat(value) + result = formatter(value) return result +def native_or_text(value: Any) -> Any: + """Preserve primitive values and render other values as bounded text.""" + try: + if isinstance(value, (set, frozenset)): + value = sorted(value, key=repr) + result = native_or_pformat(value, formatter=str) + except Exception: # pylint: disable=broad-except + return "<{}>".format(type(value).__name__) + if isinstance(result, str) and len(result) > _MAX_TEXT_LENGTH: + return result[:_MAX_TEXT_LENGTH] + "..." + return result + + def native_or_pformat_dict(value: Dict[str, Any]) -> Dict[str, Any]: """ Converter utility for dictionaries, diff --git a/testplan/report/testing/base.py b/testplan/report/testing/base.py index a4d167680..67242e93c 100644 --- a/testplan/report/testing/base.py +++ b/testplan/report/testing/base.py @@ -41,7 +41,6 @@ import copy import getpass -import hashlib import itertools import platform import re @@ -61,6 +60,7 @@ Status, ) from testplan.common.report.base import ExceptionLoggerBase +from testplan.common.serialization.fields import native_or_text from testplan.common.utils.timing import iana_tz from testplan.testing import tagging from testplan.testing.common import TEST_PART_PATTERN_FORMAT_STRING @@ -524,6 +524,7 @@ def __init__( name: str, tags: Optional[Union[Dict[str, Any], str]] = None, category: str = ReportCategories.TESTCASE, + parametrization_kwargs: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> None: super(TestCaseReport, self).__init__(name=name, **kwargs) @@ -533,6 +534,14 @@ def __init__( self.attachments: List[Any] = [] self.category = category self.covered_lines: Optional[dict] = None + self.parametrization_kwargs = ( + { + key: native_or_text(value) + for key, value in parametrization_kwargs.items() + } + if parametrization_kwargs + else None + ) def _get_comparison_attrs(self) -> List[str]: return super(TestCaseReport, self)._get_comparison_attrs() + [ diff --git a/testplan/report/testing/schemas.py b/testplan/report/testing/schemas.py index cbdaf21eb..fcae8aae3 100644 --- a/testplan/report/testing/schemas.py +++ b/testplan/report/testing/schemas.py @@ -63,6 +63,16 @@ class TestCaseReportSchema(ReportSchema): category = fields.String() counter = fields.Dict(dump_only=True) tags = TagField() + parametrization_kwargs = fields.Dict(allow_none=True) + + @post_dump + def strip_empty_parametrization_kwargs( + self, data: Dict[str, Any], **kwargs: Any + ) -> Dict[str, Any]: + """Omit parametrization data from ordinary testcase reports.""" + if data.get("parametrization_kwargs") is None: + data.pop("parametrization_kwargs", None) + return data @post_load def make_report( diff --git a/testplan/testing/multitest/base.py b/testplan/testing/multitest/base.py index 0fbcb04fe..853983c50 100644 --- a/testplan/testing/multitest/base.py +++ b/testplan/testing/multitest/base.py @@ -771,6 +771,9 @@ def _new_testcase_report(self, testcase: Any) -> TestCaseReport: definition_name=testcase.name, uid=testcase.__name__, tags=testcase.__tags__, + parametrization_kwargs=getattr( + testcase, "_parametrization_kwargs", None + ), ) def _new_parametrized_group_report( diff --git a/testplan/web_ui/testing/src/Report/BatchReport.js b/testplan/web_ui/testing/src/Report/BatchReport.js index d61a64335..eb6900b0d 100644 --- a/testplan/web_ui/testing/src/Report/BatchReport.js +++ b/testplan/web_ui/testing/src/Report/BatchReport.js @@ -65,6 +65,9 @@ class BatchReportComponent extends BaseReport { this.updateTagsDisplay = this.updateTagsDisplay.bind(this); this.updateFilter = this.updateFilter.bind(this); this.updatePanelView = this.updatePanelView.bind(this); + this.handleExtendedSearchNavigate = this.handleExtendedSearchNavigate.bind( + this + ); this.state = { ...this.state, @@ -243,6 +246,15 @@ class BatchReportComponent extends BaseReport { this.props.history.push(newUrl); } + /** + * Handle navigation from extended search. + * @param {Array} uids - Array of UIDs from report root to target entry + */ + handleExtendedSearchNavigate(uids) { + if (!uids || uids.length === 0) return; + const newPath = generateSelectionPath(this.props.match.path, uids); + this.props.history.push(newPath); + } getSelectedUIDsFromPath() { const { uid, selection } = this.props.match.params; @@ -310,6 +322,7 @@ class BatchReportComponent extends BaseReport { updateTagsDisplayFunc={this.updateTagsDisplay} current_pannel={this.state.currentPanelView} switchPanelViewFunc={this.updatePanelView} + onExtendedSearchNavigate={this.handleExtendedSearchNavigate} />
entry.uid); - // the filter text is either "null" or an empty string, use truthy-falsy - if (this.state.filteredReport.filter.text) { + const filter = this.state.filteredReport.filter; + if (filter.text || filter.filters?.length) { shallowEntry.entries = entries .map((entry) => this.pruneReportEntry(entry)) .filter((entry) => entry !== null); @@ -698,6 +701,17 @@ class InteractiveReportComponent extends BaseReport { ); } + /** + * Handle navigation from extended search. + * @param {Array} uids - Array of UIDs from report root to target entry + */ + handleExtendedSearchNavigate(uids) { + if (!uids || uids.length === 0) return; + const [uid, ...selection] = uids.map((entryUid) => base64url(entryUid)); + const newPath = generatePath(this.props.match.path, { uid, selection }); + this.props.history.push(newPath); + } + /** * Render the InteractiveReport component based on its current state. */ @@ -748,6 +762,7 @@ class InteractiveReportComponent extends BaseReport { updateFilterFunc={noop} updateEmptyDisplayFunc={noop} updateTagsDisplayFunc={noop} + onExtendedSearchNavigate={this.handleExtendedSearchNavigate} extraButtons={[ { moxios.uninstall(); }); + it("navigates to encoded extended search results", () => { + const interactiveReport = renderInteractiveReport(); + const historyPush = jest.spyOn(interactiveReport.instance().props.history, "push"); + + interactiveReport.instance().handleExtendedSearchNavigate([ + "TestplanUID", + "MultiTestUID", + "SuiteUID", + "testcaseUID", + ]); + + expect(historyPush).toHaveBeenCalledWith( + "/interactive/VGVzdHBsYW5VSUQ/TXVsdGlUZXN0VUlE/U3VpdGVVSUQ/dGVzdGNhc2VVSUQ" + ); + }); + it("Handles 'full' parameter for report serialization", (done) => { const interactiveReport = renderInteractiveReport(true); const report = initialReport(); @@ -905,7 +921,7 @@ describe("InteractiveReport", () => { }); }); - it("Run filtered tests", (done) => { + it("Run filtered tests with an empty display string", (done) => { const interactiveReport = renderInteractiveReport(); const report = initialReport(); @@ -919,7 +935,7 @@ describe("InteractiveReport", () => { interactiveReport.setState({ filteredReport: { report: report, - filter: { text: "something" }, + filter: { text: "", filters: [{ type: "regexp", search: "case" }] }, }, }); interactiveReport.update(); diff --git a/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/BatchReport.test.js.snap b/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/BatchReport.test.js.snap index 173d0ea3a..c4c3eb715 100644 --- a/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/BatchReport.test.js.snap +++ b/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/BatchReport.test.js.snap @@ -13,58 +13,33 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge style="width: 22em;" >
- - - - - Search - - - - - - - - Click for help - - - - + + + + Search + + +
+ + + + Click for help + + + + + + + + Extended Search - Search by parameters + + + +
Switch Panel View @@ -115,7 +136,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge class="buttonsBar_6zsntc" > Expand all assertions @@ -142,7 +163,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge class="buttonsBar_6zsntc" > Collapse all assertions @@ -175,7 +196,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge href="#" > Preferences @@ -340,7 +361,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge href="#" > Choose filter @@ -590,7 +611,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge class="buttonsBar_6zsntc" > Display tags @@ -617,7 +638,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge class="buttonsBar_6zsntc" > Info @@ -644,7 +665,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge class="buttonsBar_6zsntc" > Print page @@ -674,7 +695,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge target="_blank" > Documentation @@ -714,7 +735,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge class="MuiButton-label" > Home @@ -1311,58 +1332,33 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge style="width: 22em;" >
- - - - - Search - - - - - - - - Click for help - - - - + + + + Search + + +
+ + + + Click for help + + + + + + + + Extended Search - Search by parameters + + + +
Switch Panel View @@ -1413,7 +1455,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge class="buttonsBar_6zsntc" > Expand all assertions @@ -1440,7 +1482,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge class="buttonsBar_6zsntc" > Collapse all assertions @@ -1473,7 +1515,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge href="#" > Preferences @@ -1638,7 +1680,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge href="#" > Choose filter @@ -1888,7 +1930,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge class="buttonsBar_6zsntc" > Display tags @@ -1915,7 +1957,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge class="buttonsBar_6zsntc" > Info @@ -1942,7 +1984,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge class="buttonsBar_6zsntc" > Print page @@ -1972,7 +2014,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge target="_blank" > Documentation @@ -2012,7 +2054,7 @@ exports[`BatchReport Merge multitest merges multitest parts when clicking merge class="MuiButton-label" > Home @@ -2552,58 +2594,33 @@ exports[`BatchReport loads a failed simple report and navigates to first failure style="width: 22em;" >
- - - - - Search - - - - - - - - Click for help - - - - + + + + Search + + +
+ + + + Click for help + + + + + + + + Extended Search - Search by parameters + + + +
Switch Panel View @@ -2654,7 +2717,7 @@ exports[`BatchReport loads a failed simple report and navigates to first failure class="buttonsBar_6zsntc" > Expand all assertions @@ -2681,7 +2744,7 @@ exports[`BatchReport loads a failed simple report and navigates to first failure class="buttonsBar_6zsntc" > Collapse all assertions @@ -2714,7 +2777,7 @@ exports[`BatchReport loads a failed simple report and navigates to first failure href="#" > Preferences @@ -2879,7 +2942,7 @@ exports[`BatchReport loads a failed simple report and navigates to first failure href="#" > Choose filter @@ -3129,7 +3192,7 @@ exports[`BatchReport loads a failed simple report and navigates to first failure class="buttonsBar_6zsntc" > Display tags @@ -3156,7 +3219,7 @@ exports[`BatchReport loads a failed simple report and navigates to first failure class="buttonsBar_6zsntc" > Info @@ -3183,7 +3246,7 @@ exports[`BatchReport loads a failed simple report and navigates to first failure class="buttonsBar_6zsntc" > Print page @@ -3213,7 +3276,7 @@ exports[`BatchReport loads a failed simple report and navigates to first failure target="_blank" > Documentation @@ -3253,7 +3316,7 @@ exports[`BatchReport loads a failed simple report and navigates to first failure class="MuiButton-label" > Home @@ -3933,58 +3996,33 @@ exports[`BatchReport loads a more complex error report 1`] = ` style="width: 22em;" >
- - - - - Search - - - - - - - - Click for help - - - - + + + + Search + + +
+ + + + Click for help + + + + + + + + Extended Search - Search by parameters + + + +
Switch Panel View @@ -4035,7 +4119,7 @@ exports[`BatchReport loads a more complex error report 1`] = ` class="buttonsBar_6zsntc" > Expand all assertions @@ -4062,7 +4146,7 @@ exports[`BatchReport loads a more complex error report 1`] = ` class="buttonsBar_6zsntc" > Collapse all assertions @@ -4095,7 +4179,7 @@ exports[`BatchReport loads a more complex error report 1`] = ` href="#" > Preferences @@ -4260,7 +4344,7 @@ exports[`BatchReport loads a more complex error report 1`] = ` href="#" > Choose filter @@ -4510,7 +4594,7 @@ exports[`BatchReport loads a more complex error report 1`] = ` class="buttonsBar_6zsntc" > Display tags @@ -4537,7 +4621,7 @@ exports[`BatchReport loads a more complex error report 1`] = ` class="buttonsBar_6zsntc" > Info @@ -4564,7 +4648,7 @@ exports[`BatchReport loads a more complex error report 1`] = ` class="buttonsBar_6zsntc" > Print page @@ -4594,7 +4678,7 @@ exports[`BatchReport loads a more complex error report 1`] = ` target="_blank" > Documentation @@ -4634,7 +4718,7 @@ exports[`BatchReport loads a more complex error report 1`] = ` class="MuiButton-label" > Home @@ -5116,58 +5200,33 @@ exports[`BatchReport loads a more complex report 1`] = ` style="width: 22em;" >
- - - - - Search - - - - - - - - Click for help - - - - + + + + Search + + +
+ + + + Click for help + + + + + + + + Extended Search - Search by parameters + + + +
Switch Panel View @@ -5218,7 +5323,7 @@ exports[`BatchReport loads a more complex report 1`] = ` class="buttonsBar_6zsntc" > Expand all assertions @@ -5245,7 +5350,7 @@ exports[`BatchReport loads a more complex report 1`] = ` class="buttonsBar_6zsntc" > Collapse all assertions @@ -5278,7 +5383,7 @@ exports[`BatchReport loads a more complex report 1`] = ` href="#" > Preferences @@ -5443,7 +5548,7 @@ exports[`BatchReport loads a more complex report 1`] = ` href="#" > Choose filter @@ -5693,7 +5798,7 @@ exports[`BatchReport loads a more complex report 1`] = ` class="buttonsBar_6zsntc" > Display tags @@ -5720,7 +5825,7 @@ exports[`BatchReport loads a more complex report 1`] = ` class="buttonsBar_6zsntc" > Info @@ -5747,7 +5852,7 @@ exports[`BatchReport loads a more complex report 1`] = ` class="buttonsBar_6zsntc" > Print page @@ -5777,7 +5882,7 @@ exports[`BatchReport loads a more complex report 1`] = ` target="_blank" > Documentation @@ -5817,7 +5922,7 @@ exports[`BatchReport loads a more complex report 1`] = ` class="MuiButton-label" > Home @@ -6619,58 +6724,33 @@ exports[`BatchReport loads a passed simple report 1`] = ` style="width: 22em;" >
- - - - - Search - - - - - - - - Click for help - - - - + + + + Search + + +
+ + + + Click for help + + + + + + + + Extended Search - Search by parameters + + + +
Switch Panel View @@ -6721,7 +6847,7 @@ exports[`BatchReport loads a passed simple report 1`] = ` class="buttonsBar_6zsntc" > Expand all assertions @@ -6748,7 +6874,7 @@ exports[`BatchReport loads a passed simple report 1`] = ` class="buttonsBar_6zsntc" > Collapse all assertions @@ -6781,7 +6907,7 @@ exports[`BatchReport loads a passed simple report 1`] = ` href="#" > Preferences @@ -6946,7 +7072,7 @@ exports[`BatchReport loads a passed simple report 1`] = ` href="#" > Choose filter @@ -7196,7 +7322,7 @@ exports[`BatchReport loads a passed simple report 1`] = ` class="buttonsBar_6zsntc" > Display tags @@ -7223,7 +7349,7 @@ exports[`BatchReport loads a passed simple report 1`] = ` class="buttonsBar_6zsntc" > Info @@ -7250,7 +7376,7 @@ exports[`BatchReport loads a passed simple report 1`] = ` class="buttonsBar_6zsntc" > Print page @@ -7280,7 +7406,7 @@ exports[`BatchReport loads a passed simple report 1`] = ` target="_blank" > Documentation @@ -7320,7 +7446,7 @@ exports[`BatchReport loads a passed simple report 1`] = ` class="MuiButton-label" > Home @@ -7520,58 +7646,33 @@ exports[`BatchReport loads a report with selection at Multitest level 1`] = ` style="width: 22em;" >
- - - - - Search - - - - - - - - Click for help - - - - + + + + Search + + +
+ + + + Click for help + + + + + + + + Extended Search - Search by parameters + + + +
Switch Panel View @@ -7622,7 +7769,7 @@ exports[`BatchReport loads a report with selection at Multitest level 1`] = ` class="buttonsBar_6zsntc" > Expand all assertions @@ -7649,7 +7796,7 @@ exports[`BatchReport loads a report with selection at Multitest level 1`] = ` class="buttonsBar_6zsntc" > Collapse all assertions @@ -7682,7 +7829,7 @@ exports[`BatchReport loads a report with selection at Multitest level 1`] = ` href="#" > Preferences @@ -7847,7 +7994,7 @@ exports[`BatchReport loads a report with selection at Multitest level 1`] = ` href="#" > Choose filter @@ -8097,7 +8244,7 @@ exports[`BatchReport loads a report with selection at Multitest level 1`] = ` class="buttonsBar_6zsntc" > Display tags @@ -8124,7 +8271,7 @@ exports[`BatchReport loads a report with selection at Multitest level 1`] = ` class="buttonsBar_6zsntc" > Info @@ -8151,7 +8298,7 @@ exports[`BatchReport loads a report with selection at Multitest level 1`] = ` class="buttonsBar_6zsntc" > Print page @@ -8181,7 +8328,7 @@ exports[`BatchReport loads a report with selection at Multitest level 1`] = ` target="_blank" > Documentation @@ -8221,7 +8368,7 @@ exports[`BatchReport loads a report with selection at Multitest level 1`] = ` class="MuiButton-label" > Home @@ -8553,58 +8700,33 @@ exports[`BatchReport loads a report with selection at Testcase level 1`] = ` style="width: 22em;" >
- - - - - Search - - - - - - - - Click for help - - - - + + + + Search + + +
+ + + + Click for help + + + + + + + + Extended Search - Search by parameters + + + +
Switch Panel View @@ -8655,7 +8823,7 @@ exports[`BatchReport loads a report with selection at Testcase level 1`] = ` class="buttonsBar_6zsntc" > Expand all assertions @@ -8682,7 +8850,7 @@ exports[`BatchReport loads a report with selection at Testcase level 1`] = ` class="buttonsBar_6zsntc" > Collapse all assertions @@ -8715,7 +8883,7 @@ exports[`BatchReport loads a report with selection at Testcase level 1`] = ` href="#" > Preferences @@ -8880,7 +9048,7 @@ exports[`BatchReport loads a report with selection at Testcase level 1`] = ` href="#" > Choose filter @@ -9130,7 +9298,7 @@ exports[`BatchReport loads a report with selection at Testcase level 1`] = ` class="buttonsBar_6zsntc" > Display tags @@ -9157,7 +9325,7 @@ exports[`BatchReport loads a report with selection at Testcase level 1`] = ` class="buttonsBar_6zsntc" > Info @@ -9184,7 +9352,7 @@ exports[`BatchReport loads a report with selection at Testcase level 1`] = ` class="buttonsBar_6zsntc" > Print page @@ -9214,7 +9382,7 @@ exports[`BatchReport loads a report with selection at Testcase level 1`] = ` target="_blank" > Documentation @@ -9254,7 +9422,7 @@ exports[`BatchReport loads a report with selection at Testcase level 1`] = ` class="MuiButton-label" > Home @@ -10056,58 +10224,33 @@ exports[`BatchReport loads a report with selection at Testcase level and Time In style="width: 22em;" >
- - - - - Search - - - - - - - - Click for help - - - - + + + + Search + + +
+ + + + Click for help + + + + + + + + Extended Search - Search by parameters + + + +
Switch Panel View @@ -10158,7 +10347,7 @@ exports[`BatchReport loads a report with selection at Testcase level and Time In class="buttonsBar_6zsntc" > Expand all assertions @@ -10185,7 +10374,7 @@ exports[`BatchReport loads a report with selection at Testcase level and Time In class="buttonsBar_6zsntc" > Collapse all assertions @@ -10218,7 +10407,7 @@ exports[`BatchReport loads a report with selection at Testcase level and Time In href="#" > Preferences @@ -10419,7 +10608,7 @@ exports[`BatchReport loads a report with selection at Testcase level and Time In href="#" > Choose filter @@ -10669,7 +10858,7 @@ exports[`BatchReport loads a report with selection at Testcase level and Time In class="buttonsBar_6zsntc" > Display tags @@ -10696,7 +10885,7 @@ exports[`BatchReport loads a report with selection at Testcase level and Time In class="buttonsBar_6zsntc" > Info @@ -10723,7 +10912,7 @@ exports[`BatchReport loads a report with selection at Testcase level and Time In class="buttonsBar_6zsntc" > Print page @@ -10753,7 +10942,7 @@ exports[`BatchReport loads a report with selection at Testcase level and Time In target="_blank" > Documentation @@ -10793,7 +10982,7 @@ exports[`BatchReport loads a report with selection at Testcase level and Time In class="MuiButton-label" > Home @@ -11672,58 +11861,33 @@ exports[`BatchReport loads a report with selection at Testcase level and UTC Tim style="width: 22em;" >
- - - - - Search - - - - - - - - Click for help - - - - + + + + Search + + +
+ + + + Click for help + + + + + + + + Extended Search - Search by parameters + + + +
Switch Panel View @@ -11774,7 +11984,7 @@ exports[`BatchReport loads a report with selection at Testcase level and UTC Tim class="buttonsBar_6zsntc" > Expand all assertions @@ -11801,7 +12011,7 @@ exports[`BatchReport loads a report with selection at Testcase level and UTC Tim class="buttonsBar_6zsntc" > Collapse all assertions @@ -11834,7 +12044,7 @@ exports[`BatchReport loads a report with selection at Testcase level and UTC Tim href="#" > Preferences @@ -12034,7 +12244,7 @@ exports[`BatchReport loads a report with selection at Testcase level and UTC Tim href="#" > Choose filter @@ -12284,7 +12494,7 @@ exports[`BatchReport loads a report with selection at Testcase level and UTC Tim class="buttonsBar_6zsntc" > Display tags @@ -12311,7 +12521,7 @@ exports[`BatchReport loads a report with selection at Testcase level and UTC Tim class="buttonsBar_6zsntc" > Info @@ -12338,7 +12548,7 @@ exports[`BatchReport loads a report with selection at Testcase level and UTC Tim class="buttonsBar_6zsntc" > Print page @@ -12368,7 +12578,7 @@ exports[`BatchReport loads a report with selection at Testcase level and UTC Tim target="_blank" > Documentation @@ -12408,7 +12618,7 @@ exports[`BatchReport loads a report with selection at Testcase level and UTC Tim class="MuiButton-label" > Home @@ -13287,58 +13497,33 @@ exports[`BatchReport loads a report with selection at Testsuite level 1`] = ` style="width: 22em;" >
- - - - - Search - - - - - - - - Click for help - - - - + + + + Search + + +
+ + + + Click for help + + + + + + + + Extended Search - Search by parameters + + + +
Switch Panel View @@ -13389,7 +13620,7 @@ exports[`BatchReport loads a report with selection at Testsuite level 1`] = ` class="buttonsBar_6zsntc" > Expand all assertions @@ -13416,7 +13647,7 @@ exports[`BatchReport loads a report with selection at Testsuite level 1`] = ` class="buttonsBar_6zsntc" > Collapse all assertions @@ -13449,7 +13680,7 @@ exports[`BatchReport loads a report with selection at Testsuite level 1`] = ` href="#" > Preferences @@ -13614,7 +13845,7 @@ exports[`BatchReport loads a report with selection at Testsuite level 1`] = ` href="#" > Choose filter @@ -13864,7 +14095,7 @@ exports[`BatchReport loads a report with selection at Testsuite level 1`] = ` class="buttonsBar_6zsntc" > Display tags @@ -13891,7 +14122,7 @@ exports[`BatchReport loads a report with selection at Testsuite level 1`] = ` class="buttonsBar_6zsntc" > Info @@ -13918,7 +14149,7 @@ exports[`BatchReport loads a report with selection at Testsuite level 1`] = ` class="buttonsBar_6zsntc" > Print page @@ -13948,7 +14179,7 @@ exports[`BatchReport loads a report with selection at Testsuite level 1`] = ` target="_blank" > Documentation @@ -13988,7 +14219,7 @@ exports[`BatchReport loads a report with selection at Testsuite level 1`] = ` class="MuiButton-label" > Home @@ -14546,58 +14777,33 @@ exports[`BatchReport renders the correct HTML structure when report with errors style="width: 22em;" >
- - - - - Search - - - - - - - - Click for help - - - - + + + + Search + + +
+ + + + Click for help + + + + + + + + Extended Search - Search by parameters + + + +
Switch Panel View @@ -14648,7 +14900,7 @@ exports[`BatchReport renders the correct HTML structure when report with errors class="buttonsBar_6zsntc" > Expand all assertions @@ -14675,7 +14927,7 @@ exports[`BatchReport renders the correct HTML structure when report with errors class="buttonsBar_6zsntc" > Collapse all assertions @@ -14708,7 +14960,7 @@ exports[`BatchReport renders the correct HTML structure when report with errors href="#" > Preferences @@ -14873,7 +15125,7 @@ exports[`BatchReport renders the correct HTML structure when report with errors href="#" > Choose filter @@ -15123,7 +15375,7 @@ exports[`BatchReport renders the correct HTML structure when report with errors class="buttonsBar_6zsntc" > Display tags @@ -15150,7 +15402,7 @@ exports[`BatchReport renders the correct HTML structure when report with errors class="buttonsBar_6zsntc" > Info @@ -15177,7 +15429,7 @@ exports[`BatchReport renders the correct HTML structure when report with errors class="buttonsBar_6zsntc" > Print page @@ -15207,7 +15459,7 @@ exports[`BatchReport renders the correct HTML structure when report with errors target="_blank" > Documentation @@ -15247,7 +15499,7 @@ exports[`BatchReport renders the correct HTML structure when report with errors class="MuiButton-label" > Home diff --git a/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/InteractiveReport.test.js.snap b/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/InteractiveReport.test.js.snap index 625e9727c..579dc1cae 100644 --- a/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/InteractiveReport.test.js.snap +++ b/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/InteractiveReport.test.js.snap @@ -31,6 +31,7 @@ exports[`InteractiveReport Handles 'full' parameter for report serialization 1`] filterBoxWidth="28em" filterText={null} handleNavFilter={[Function]} + onExtendedSearchNavigate={[Function]} report={ Object { "attachments": Object {}, @@ -1845,6 +1846,7 @@ exports[`InteractiveReport Handles environment being started 1`] = ` filterBoxWidth="28em" filterText={null} handleNavFilter={[Function]} + onExtendedSearchNavigate={[Function]} report={ Object { "attachments": Object {}, @@ -2447,6 +2449,7 @@ exports[`InteractiveReport Loads report skeleton when mounted 1`] = ` filterBoxWidth="28em" filterText={null} handleNavFilter={[Function]} + onExtendedSearchNavigate={[Function]} report={ Object { "attachments": Object {}, @@ -4030,6 +4033,7 @@ exports[`InteractiveReport Parially refreshes the report on update. 1`] = ` filterBoxWidth="28em" filterText={null} handleNavFilter={[Function]} + onExtendedSearchNavigate={[Function]} report={ Object { "attachments": Object {}, @@ -4438,6 +4442,7 @@ exports[`InteractiveReport Updates testcase state 1`] = ` filterBoxWidth="28em" filterText={null} handleNavFilter={[Function]} + onExtendedSearchNavigate={[Function]} report={ Object { "attachments": Object {}, @@ -5468,6 +5473,7 @@ exports[`InteractiveReport handles individual parametrizations being run 1`] = ` filterBoxWidth="28em" filterText={null} handleNavFilter={[Function]} + onExtendedSearchNavigate={[Function]} report={ Object { "attachments": Object {}, @@ -6074,6 +6080,7 @@ exports[`InteractiveReport handles individual test suites being run 1`] = ` filterBoxWidth="28em" filterText={null} handleNavFilter={[Function]} + onExtendedSearchNavigate={[Function]} report={ Object { "attachments": Object {}, @@ -6676,6 +6683,7 @@ exports[`InteractiveReport handles individual testcases being run 1`] = ` filterBoxWidth="28em" filterText={null} handleNavFilter={[Function]} + onExtendedSearchNavigate={[Function]} report={ Object { "attachments": Object {}, @@ -7278,6 +7286,7 @@ exports[`InteractiveReport handles tests being run 1`] = ` filterBoxWidth="28em" filterText={null} handleNavFilter={[Function]} + onExtendedSearchNavigate={[Function]} report={ Object { "attachments": Object {}, diff --git a/testplan/web_ui/testing/src/Toolbar/ExtendedSearchDropdown.js b/testplan/web_ui/testing/src/Toolbar/ExtendedSearchDropdown.js new file mode 100644 index 000000000..52b69da03 --- /dev/null +++ b/testplan/web_ui/testing/src/Toolbar/ExtendedSearchDropdown.js @@ -0,0 +1,701 @@ +import React, { useState, useMemo, useEffect, useRef } from "react"; +import PropTypes from "prop-types"; +import _ from "lodash"; +import { StyleSheet, css } from "aphrodite"; + +import { isReportLeaf } from "../Report/reportUtils"; + +/** + * Recursively collect testcases with hierarchy + * context from the report tree. + */ +const collectTestcases = ( + entries, + testName = "", + testsuiteName = "", + parametrization = null, + result = [], + reportTestName = "" +) => { + _.forEach(entries || [], (entry) => { + const curTest = + entry.category === "multitest" + ? Array.isArray(entry.part) + ? entry.definition_name || entry.name + : entry.name + : testName; + const curReportTest = + entry.category === "multitest" ? entry.name : reportTestName; + const curSuite = + entry.category === "testsuite" ? entry.name : testsuiteName; + const curParametrization = + entry.category === "parametrization" ? entry : parametrization; + + if (entry.category === "testcase") { + result.push({ + entry, + baseName: curParametrization?.name || entry.name, + parametrizationUid: curParametrization?.uid || "", + testName: curTest, + reportTestName: curReportTest, + testsuiteName: curSuite, + }); + } + if (!isReportLeaf(entry) && !_.isEmpty(entry.entries)) { + collectTestcases( + entry.entries, + curTest, + curSuite, + curParametrization, + result, + curReportTest + ); + } + }); + return result; +}; + +/** + * Extract unique sorted values for a field, + * optionally pre-filtered by other fields. + */ +const getUniqueField = (list, field, filters = {}) => { + const filtered = _.reduce( + Object.entries(filters), + (items, [k, v]) => (v ? _.filter(items, (item) => item[k] === v) : items), + list + ); + return _(filtered).map(field).compact().uniq().sortBy().value(); +}; + +const paramValueToken = (value) => `${typeof value}:${JSON.stringify(value)}`; + +const formatParamValue = (value) => + typeof value === "string" ? value : JSON.stringify(value); + +/** + * Build { paramKey: [uniqueValues] } from + * permutations for dynamic filter dropdowns. + */ +const buildFilterOptions = (perms) => + _(perms) + .flatMap((p) => _.toPairs(p.params)) + .groupBy(0) + .mapValues((pairs) => + _(pairs) + .map(1) + .uniqBy(paramValueToken) + .sort((left, right) => + String(left).localeCompare(String(right), undefined, { + numeric: true, + }) + ) + .value() + ) + .value(); + +/** + * Filter permutations by selected param values. + * A key is present only when that filter is active, so null and empty string + * remain valid parameter values. + */ +const applyParamFilters = (perms, selected) => + _.filter(perms, (p) => + _.every(Object.entries(selected), ([k, v]) => _.isEqual(p.params[k], v)) + ); + +/** + * Extended Search Dropdown Component. + * Allows searching testcases by report hierarchy + * (Test -> Testsuite -> Testcase) and by + * structured parameters. + */ +const ExtendedSearchDropdown = ({ + report, + onNavigate, + onClose, + handleNavFilter, + value, + onStateChange, + triggerRef, +}) => { + const testcaseOptionsId = "ext-search-tc-list"; + const { + selectedTest, + selectedTestsuite, + selectedTestcase, + selectedParams, + searchText, + } = value; + const [showTestcaseOptions, setShowTestcaseOptions] = useState(false); + + const dropdownRef = useRef(null); + const reportUid = report?.uid; + const reportIdentity = report?.hash || report; + + const allTestcases = useMemo(() => { + if (!report || !report.entries) return []; + return collectTestcases(report.entries); + // Interactive polling creates new report objects even when hash is stable. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reportIdentity]); + + const uniqueTestNames = useMemo( + () => getUniqueField(allTestcases, "testName"), + [allTestcases] + ); + + const uniqueTestsuiteNames = useMemo( + () => + getUniqueField(allTestcases, "testsuiteName", { testName: selectedTest }), + [allTestcases, selectedTest] + ); + + const uniqueTestcaseNames = useMemo( + () => + getUniqueField(allTestcases, "baseName", { + testName: selectedTest, + testsuiteName: selectedTestsuite, + }), + [allTestcases, selectedTest, selectedTestsuite] + ); + + const filteredTestcaseNames = searchText.trim() + ? _.filter(uniqueTestcaseNames, (name) => + name.toLowerCase().includes(searchText.toLowerCase()) + ) + : uniqueTestcaseNames; + + const permutations = useMemo(() => { + if (!selectedTestcase) return []; + return _(allTestcases) + .filter( + (testcase) => + (!selectedTest || testcase.testName === selectedTest) && + (!selectedTestsuite || + testcase.testsuiteName === selectedTestsuite) && + testcase.baseName === selectedTestcase + ) + .map((testcase) => ({ + ...testcase, + params: testcase.entry.parametrization_kwargs || {}, + })) + .value(); + }, [allTestcases, selectedTest, selectedTestsuite, selectedTestcase]); + + const filterOptions = useMemo( + () => buildFilterOptions(permutations), + [permutations] + ); + + const filteredPermutations = useMemo( + () => applyParamFilters(permutations, selectedParams), + [permutations, selectedParams] + ); + + useEffect(() => { + const handleClickOutside = (e) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(e.target) && + !triggerRef?.current?.contains(e.target) + ) { + onClose(); + } + }; + const handleEscape = (e) => { + if (e.key === "Escape") { + onClose(); + } + }; + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("keydown", handleEscape); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("keydown", handleEscape); + }; + }, [onClose, triggerRef]); + + const handleTestChange = (e) => { + const value = e.target.value; + setShowTestcaseOptions(false); + onStateChange({ + selectedTest: value, + selectedTestsuite: "", + selectedTestcase: "", + selectedParams: {}, + searchText: "", + }); + }; + + const handleTestsuiteChange = (e) => { + const value = e.target.value; + setShowTestcaseOptions(false); + onStateChange({ + selectedTestsuite: value, + selectedTestcase: "", + selectedParams: {}, + searchText: "", + }); + }; + + const handleTestcaseSearch = (e) => { + const value = e.target.value; + if (_.includes(uniqueTestcaseNames, value)) { + setShowTestcaseOptions(false); + onStateChange({ + selectedTestcase: value, + selectedParams: {}, + searchText: value, + }); + } else { + setShowTestcaseOptions(true); + onStateChange({ + selectedTestcase: "", + selectedParams: {}, + searchText: value, + }); + } + }; + + const handleTestcaseFocus = () => { + setShowTestcaseOptions(true); + }; + + const handleTestcaseSelect = (value) => { + setShowTestcaseOptions(false); + onStateChange({ + selectedTestcase: value, + selectedParams: {}, + searchText: value, + }); + }; + + const handleParamChange = (key, token) => { + const newParams = { ...selectedParams }; + if (token === "") { + delete newParams[key]; + } else { + newParams[key] = _.find( + filterOptions[key], + (value) => paramValueToken(value) === token + ); + } + onStateChange({ selectedParams: newParams }); + }; + + /** + * Navigate to a specific permutation. + * Replaces any existing filter text with a + * targeted suite+case filter for the selected + * permutation, then navigates via UID path. + */ + const handleResultClick = (permutation) => { + if (!reportUid) return; + + if (handleNavFilter) { + const name = permutation.entry.name; + const test = permutation.reportTestName || permutation.testName; + const suite = permutation.testsuiteName; + const filters = []; + const textTerms = []; + if (test) { + filters.push({ + type: "regexp", + search: `^${_.escapeRegExp(test)}$`, + }); + if (!test.includes('"')) { + textTerms.push(`re:"^${_.escapeRegExp(test)}$"`); + } + } + if (suite) { + filters.push({ + type: "regexp", + search: `^${_.escapeRegExp(suite)}$`, + }); + if (!suite.includes('"')) { + textTerms.push(`re:"^${_.escapeRegExp(suite)}$"`); + } + } + filters.push({ + type: "regexp", + search: `^${_.escapeRegExp(name)}$`, + }); + if (!name.includes('"')) { + textTerms.push(`re:"^${_.escapeRegExp(name)}$"`); + } + handleNavFilter({ + text: textTerms.length === filters.length ? textTerms.join(" ") : "", + filters, + }); + } + + onNavigate(permutation.entry.uids); + onClose(); + }; + + const handleResultKeyDown = (e, perm) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleResultClick(perm); + } + }; + + const paramKeys = _.keys(filterOptions); + const hasTestcases = uniqueTestcaseNames.length > 0; + const isParametrized = _.some( + permutations, + (permutation) => permutation.parametrizationUid + ); + + return ( +
+ {/* Test/Multitest (Optional) */} +
+ + +
+ + {/* Testsuite (Optional) */} +
+ + +
+ + {/* Testcase (Mandatory) */} +
+ +
+ + {showTestcaseOptions && ( +
+ {filteredTestcaseNames.length === 0 ? ( +
+ No matching testcases found +
+ ) : ( + filteredTestcaseNames.map((name) => ( + + )) + )} +
+ )} +
+
+ + {/* Parameter Filters */} + {selectedTestcase && paramKeys.length > 0 && ( +
+ +
+ {paramKeys.map((key) => ( +
+ + +
+ ))} +
+
+ )} + + {/* No params message */} + {selectedTestcase && paramKeys.length === 0 && ( +
+
+ {isParametrized + ? "Parameter data is unavailable in this report. " + : "No parameters found. "} + Showing all matching testcases. +
+
+ )} + + {/* Results */} + {selectedTestcase && ( +
+ +
+ {filteredPermutations.length === 0 ? ( +
+ No matching permutations found +
+ ) : ( + filteredPermutations.map((perm, index) => ( +
handleResultClick(perm)} + onKeyDown={(e) => handleResultKeyDown(e, perm)} + title={`Navigate to ${perm.entry.name}`} + > + {perm.entry.name} +
+ )) + )} +
+
+ )} + + {/* Prompt when no testcase selected */} + {!selectedTestcase && hasTestcases && ( +
+ Select a testcase to see permutations +
+ )} + + {/* Loading or empty */} + {!report && ( +
Loading report data...
+ )} + {report && !hasTestcases && ( +
+ No testcases found in report +
+ )} +
+ ); +}; + +const styles = StyleSheet.create({ + dropdown: { + position: "absolute", + top: "100%", + left: 0, + marginTop: "8px", + width: "100%", + minWidth: "min(400px, 100%)", + maxWidth: "500px", + maxHeight: "550px", + overflowY: "auto", + backgroundColor: "#ffffff", + border: "1px solid #d0d0d0", + borderRadius: "6px", + boxShadow: "0 4px 16px rgba(0, 0, 0, 0.15)", + zIndex: 1000, + padding: "16px", + }, + select: { + width: "100%", + padding: "8px 12px", + marginBottom: "8px", + border: "1px solid #c0c0c0", + borderRadius: "4px", + fontSize: "14px", + backgroundColor: "#fff", + cursor: "pointer", + ":focus": { + outline: "none", + borderColor: "#007bff", + boxShadow: "0 0 0 2px rgba(0, 123, 255, 0.25)", + }, + }, + testcaseOptions: { + maxHeight: "220px", + overflowY: "auto", + border: "1px solid #ddd", + borderRadius: "4px", + backgroundColor: "#fff", + }, + testcaseOption: { + display: "block", + width: "100%", + padding: "8px 12px", + border: "none", + borderBottom: "1px solid #e8e8e8", + backgroundColor: "#fff", + color: "#333", + fontSize: "13px", + textAlign: "left", + cursor: "pointer", + ":last-child": { borderBottom: "none" }, + ":hover": { backgroundColor: "#1976d2", color: "#fff" }, + }, + selectedOption: { backgroundColor: "#1976d2", color: "#fff" }, + label: { + display: "block", + marginBottom: "6px", + fontWeight: 600, + fontSize: "13px", + color: "#333", + textTransform: "uppercase", + letterSpacing: "0.5px", + }, + section: { + marginBottom: "16px", + paddingBottom: "12px", + borderBottom: "1px solid #eee", + }, + lastSection: { borderBottom: "none", marginBottom: 0, paddingBottom: 0 }, + results: { + maxHeight: "220px", + overflowY: "auto", + border: "1px solid #ddd", + borderRadius: "4px", + backgroundColor: "#fafafa", + }, + resultItem: { + padding: "10px 12px", + cursor: "pointer", + borderBottom: "1px solid #e8e8e8", + fontSize: "13px", + fontFamily: "monospace", + backgroundColor: "#fff", + color: "#333", + transition: "background-color 0.15s ease, color 0.15s ease", + ":hover": { backgroundColor: "#1976d2", color: "#ffffff" }, + }, + lastResultItem: { borderBottom: "none" }, + noResults: { + padding: "16px", + color: "#888", + fontStyle: "italic", + textAlign: "center", + backgroundColor: "#f9f9f9", + borderRadius: "4px", + }, + paramsGrid: { + display: "grid", + gridTemplateColumns: "1fr 1fr", + gap: "12px", + }, + paramItem: { marginBottom: 0 }, + paramLabel: { + display: "block", + marginBottom: "4px", + fontWeight: 500, + fontSize: "11px", + color: "#666", + textTransform: "capitalize", + }, + paramSelect: { marginBottom: 0 }, +}); + +ExtendedSearchDropdown.propTypes = { + /** The testplan report object */ + report: PropTypes.object, + /** Callback when navigating to a permutation */ + onNavigate: PropTypes.func.isRequired, + /** Callback to close the dropdown */ + onClose: PropTypes.func.isRequired, + /** Callback to filter report (like search) */ + handleNavFilter: PropTypes.func, + /** Current selection state */ + value: PropTypes.shape({ + selectedTest: PropTypes.string, + selectedTestsuite: PropTypes.string, + selectedTestcase: PropTypes.string, + selectedParams: PropTypes.object, + searchText: PropTypes.string, + }).isRequired, + /** Callback to update selection state */ + onStateChange: PropTypes.func.isRequired, + /** Ref to the button that toggles the dropdown */ + triggerRef: PropTypes.shape({ current: PropTypes.any }), +}; + +export default ExtendedSearchDropdown; + +export { + collectTestcases, + paramValueToken, + getUniqueField, + buildFilterOptions, + applyParamFilters, +}; diff --git a/testplan/web_ui/testing/src/Toolbar/FilterBox.js b/testplan/web_ui/testing/src/Toolbar/FilterBox.js index 806ac4702..171665968 100644 --- a/testplan/web_ui/testing/src/Toolbar/FilterBox.js +++ b/testplan/web_ui/testing/src/Toolbar/FilterBox.js @@ -8,8 +8,10 @@ import { faSearch, faExclamationCircle, faQuestionCircle, + faSearchPlus, } from "@fortawesome/free-solid-svg-icons"; import SearchFieldParser from "../Parser/SearchFieldParser"; +import ExtendedSearchDropdown from "./ExtendedSearchDropdown"; import { RED } from "../Common/defaults"; import { Popover, PopoverHeader, PopoverBody, Table } from "reactstrap"; @@ -22,11 +24,24 @@ class FilterBox extends Component { super(props); this.inputField = createRef(); this.helpIcon = createRef(); + this.extendedSearchIcon = createRef(); this.state = { parserError: null, showHelp: false, + showExtendedSearch: false, + // Persist Extended Search selections across open/close + extendedSearchState: { + selectedTest: "", + selectedTestsuite: "", + selectedTestcase: "", + selectedParams: {}, + searchText: "", + }, }; this.toggleHelp = this.toggleHelp.bind(this); + this.toggleExtendedSearch = this.toggleExtendedSearch.bind(this); + this.closeExtendedSearch = this.closeExtendedSearch.bind(this); + this.updateExtendedSearchState = this.updateExtendedSearchState.bind(this); this.helpText = ( <>

@@ -63,6 +78,46 @@ class FilterBox extends Component { this.setState({ showHelp: !this.state.showHelp }); } + toggleExtendedSearch() { + this.setState((prev) => ({ + showExtendedSearch: !prev.showExtendedSearch, + })); + } + + closeExtendedSearch() { + this.setState({ showExtendedSearch: false }); + } + + resetExtendedSearchState() { + this.setState({ + showExtendedSearch: false, + extendedSearchState: { + selectedTest: "", + selectedTestsuite: "", + selectedTestcase: "", + selectedParams: {}, + searchText: "", + }, + }); + } + + componentDidUpdate(prevProps) { + if ( + prevProps.report?.uid !== this.props.report?.uid + ) { + this.resetExtendedSearchState(); + } + } + + updateExtendedSearchState(newState) { + this.setState((prev) => ({ + extendedSearchState: { + ...prev.extendedSearchState, + ...newState, + }, + })); + } + hasError() { return Boolean(this.state.parserError); } @@ -72,39 +127,12 @@ class FilterBox extends Component { } render() { + const supportsExtendedSearch = this.props.onExtendedSearchNavigate; + return (

- - - - - - - - - How to search - - {this.helpText} - - + +
this.onFilterChange(event)} />
+ + + + + How to search + + {this.helpText} + + + {supportsExtendedSearch && ( + + + + )} + {this.state.showExtendedSearch && supportsExtendedSearch && ( + + )}
); } @@ -127,10 +206,9 @@ class FilterBox extends Component { this.setState({ parserError: null }); this.props.handleNavFilter({ text: e.target.value, filters }); } catch (error) { - this.setState({ parserError: error }); - console.log("Could not parse seach string: " + error); - console.log(e.target.value); - this.props.handleNavFilter({ text: e.target.values, filters: [] }); + this.setState({ parserError: error?.message || String(error) }); + console.log("Could not parse search string:", error); + this.props.handleNavFilter({ text: e.target.value, filters: [] }); } } @@ -244,22 +322,34 @@ FilterBox.propTypes = { /** Function to handle expressions entered into the Filter box */ handleNavFilter: PropTypes.func, filterText: PropTypes.string, + /** Report object for extended search */ + report: PropTypes.object, + /** Callback for extended search navigation, receives array of UIDs */ + onExtendedSearchNavigate: PropTypes.func, }; const styles = StyleSheet.create({ searchBox: { height: "100%", padding: "0.4em", + display: "flex", + alignItems: "center", + gap: "8px", + position: "relative", }, searchBoxInner: { - paddingLeft: "20px", - paddingRight: "20px", + flex: 1, }, searchBoxIcon: { - float: "left", + flexShrink: 0, }, searchBoxInfoIcon: { - float: "right", + flexShrink: 0, + cursor: "pointer", + }, + searchBoxExtendedIcon: { + flexShrink: 0, + cursor: "pointer", }, searchBoxInput: { width: "100%", diff --git a/testplan/web_ui/testing/src/Toolbar/Toolbar.js b/testplan/web_ui/testing/src/Toolbar/Toolbar.js index 9db8dde66..739cb0d40 100644 --- a/testplan/web_ui/testing/src/Toolbar/Toolbar.js +++ b/testplan/web_ui/testing/src/Toolbar/Toolbar.js @@ -476,7 +476,13 @@ const ToolbarInfoButton = ({ report }) => { ); }; -const ToolbarFilterBox = ({ filterBoxWidth, filterText, handleNavFilter }) => { +const ToolbarFilterBox = ({ + filterBoxWidth, + filterText, + handleNavFilter, + report, + onExtendedSearchNavigate, +}) => { return (
{ width: filterBoxWidth, }} > - +
); }; @@ -509,6 +520,8 @@ const Toolbar = function (props) { filterBoxWidth={props.filterBoxWidth} handleNavFilter={props.handleNavFilter} filterText={props.filterText} + report={props.report} + onExtendedSearchNavigate={props.onExtendedSearchNavigate} />