diff --git a/doc/newsfragments/2404_new.run_all_button.rst b/doc/newsfragments/2404_new.run_all_button.rst new file mode 100644 index 000000000..bc91e9051 --- /dev/null +++ b/doc/newsfragments/2404_new.run_all_button.rst @@ -0,0 +1 @@ +Introduced a new plan-level Run button to the toolbar on the interactive GUI. \ No newline at end of file diff --git a/testplan/runnable/interactive/base.py b/testplan/runnable/interactive/base.py index 69a63af2b..829391d92 100644 --- a/testplan/runnable/interactive/base.py +++ b/testplan/runnable/interactive/base.py @@ -227,11 +227,14 @@ def reset_test(self, test_uid, await_results=True): self._update_reports([(self.test(test_uid).dry_run().report, [])]) def run_all_tests( - self, await_results: bool = True + self, + shallow_report: Optional[Dict] = None, + await_results: bool = True, ) -> Union[TestReport, Awaitable]: """ Runs all tests. + :param shallow_report: shallow report entry, optional :param await_results: Whether to block until tests are finished, defaults to True. :return: If await_results is True, returns a testplan report. @@ -239,12 +242,20 @@ def run_all_tests( ready. """ if not await_results: - return self._run_async(self.run_all_tests) - - self.logger.debug("Interactive mode: Run all tests") + return self._run_async( + self.run_all_tests, shallow_report=shallow_report + ) - for test_uid in self.all_tests(): - self.run_test(test_uid) + if shallow_report: + self.logger.debug("Interactive mode: Run filtered tests") + for multitest in shallow_report["entries"]: + self.run_test( + test_uid=multitest["name"], shallow_report=multitest + ) + else: + self.logger.debug("Interactive mode: Run all tests") + for test_uid in self.all_tests(): + self.run_test(test_uid=test_uid) def run_test( self, diff --git a/testplan/runnable/interactive/http.py b/testplan/runnable/interactive/http.py index 7e36a17dc..affa83385 100644 --- a/testplan/runnable/interactive/http.py +++ b/testplan/runnable/interactive/http.py @@ -153,8 +153,19 @@ def put(self): new_runtime_status, ): _check_execution_order(ihandler.report) - ihandler.report.runtime_status = RuntimeStatus.WAITING - ihandler.run_all_tests(await_results=False) + filtered = "entries" in shallow_report + if filtered: + entries = _extract_entries(shallow_report) + ihandler.report.set_runtime_status_filtered( + RuntimeStatus.WAITING, + entries, + ) + else: + ihandler.report.runtime_status = RuntimeStatus.WAITING + ihandler.run_all_tests( + shallow_report=shallow_report if filtered else None, + await_results=False, + ) return _serialize_report_entry(ihandler.report) diff --git a/testplan/web_ui/testing/src/Report/InteractiveReport.js b/testplan/web_ui/testing/src/Report/InteractiveReport.js index 8f339c309..c0441d606 100644 --- a/testplan/web_ui/testing/src/Report/InteractiveReport.js +++ b/testplan/web_ui/testing/src/Report/InteractiveReport.js @@ -17,6 +17,7 @@ import { ReloadButton, ResetButton, AbortButton, + RunAllButton, SaveButton, } from "../Toolbar/InteractiveButtons"; import NavBreadcrumbs from "../Nav/NavBreadcrumbs"; @@ -63,6 +64,7 @@ class InteractiveReportComponent extends BaseReport { this.resetAssertionStatus = this.resetAssertionStatus.bind(this); this.resetReport = this.resetReport.bind(this); this.abortTestplan = this.abortTestplan.bind(this); + this.runAll = this.runAll.bind(this); this.reloadCode = this.reloadCode.bind(this); this.envCtrlCallback = this.envCtrlCallback.bind(this); this.handleClick = this.handleClick.bind(this); @@ -72,6 +74,7 @@ class InteractiveReportComponent extends BaseReport { navWidth: `${INTERACTIVE_COL_WIDTH}em`, resetting: false, reloading: false, + running: false, aborting: false, assertionStatus: defaultAssertionStatus, }; @@ -122,9 +125,12 @@ class InteractiveReportComponent extends BaseReport { response.data.runtime_status === "finished" || response.data.runtime_status === "not_run" ) { - if (this.state.resetting){ + if (this.state.resetting) { this.setState({ resetting: false }); } + if (this.state.running) { + this.setState({ running: false }); + } } if ( !this.state.report || @@ -503,11 +509,33 @@ class InteractiveReportComponent extends BaseReport { return shallowEntry; } + /** + * Send request of start all tests to server. + */ + runAll() { + if ( + this.state.resetting || this.state.reloading || + this.state.aborting || this.state.running + ) { + return; + } else { + const updatedReportEntry = { + ...this.shallowReportEntry(this.state.filteredReport.report), + runtime_status: "running", + }; + this.putUpdatedReportEntry(updatedReportEntry); + this.setState({ running: true }); + } + } + /** * Reset the report state to "resetting" and request the change to server. */ resetReport() { - if (this.state.resetting || this.state.reloading || this.state.aborting) { + if ( + this.state.resetting || this.state.reloading || + this.state.aborting || this.state.running + ) { return; } else { const updatedReportEntry = { @@ -523,7 +551,10 @@ class InteractiveReportComponent extends BaseReport { * Send request of reloading report to server. */ reloadCode() { - if (this.state.resetting || this.state.reloading || this.state.aborting) { + if ( + this.state.resetting || this.state.reloading || + this.state.aborting || this.state.running + ) { return; } let currentTime = new Date(); @@ -670,6 +701,12 @@ class InteractiveReportComponent extends BaseReport { updateEmptyDisplayFunc={noop} updateTagsDisplayFunc={noop} extraButtons={[ + , { }); }); }); + + it("Run all tests", (done) => { + const interactiveReport = renderInteractiveReport(); + + const report = initialReport(); + const multitest = report.entries[0]; + expect(multitest.category).toBe("multitest"); + multitest.env_status = "STARTED"; + + const testcase = report.entries[0].entries[0].entries[0]; + expect(testcase.category).toBe("testcase"); + + interactiveReport.setState({ + filteredReport: { + report: report, + filter: {text: null} + }, + }); + interactiveReport.update(); + interactiveReport.instance().runAll(); + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + expect(request.url).toBe("/api/v1/interactive/report"); + expect(request.config.method).toBe("put"); + const putData = JSON.parse(request.config.data); + + request + .respondWith({ + status: 200, + response: putData, + }) + .then(() => { + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + expect(request.url).toBe("/api/v1/interactive/report"); + expect(request.config.method).toBe("put"); + const putData = JSON.parse(request.config.data); + expect(putData.runtime_status).toBe("running"); + expect(putData.entries).not.toBeDefined(); + done(); + }); + }); + }); + }); + + it("Run filtered tests", (done) => { + const interactiveReport = renderInteractiveReport(); + + const report = initialReport(); + const multitest = report.entries[0]; + expect(multitest.category).toBe("multitest"); + multitest.env_status = "STARTED"; + + const testcase = report.entries[0].entries[0].entries[0]; + expect(testcase.category).toBe("testcase"); + + interactiveReport.setState({ + filteredReport: { + report: report, + filter: {text: "something"} + }, + }); + interactiveReport.update(); + interactiveReport.instance().runAll(); + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + expect(request.url).toBe("/api/v1/interactive/report"); + expect(request.config.method).toBe("put"); + const putData = JSON.parse(request.config.data); + + request + .respondWith({ + status: 200, + response: putData, + }) + .then(() => { + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + expect(request.url).toBe("/api/v1/interactive/report"); + expect(request.config.method).toBe("put"); + const putData = JSON.parse(request.config.data); + expect(putData.runtime_status).toBe("running"); + expect(putData.entries).toHaveLength(1); + expect(putData.entries[0].name).toBe("MultiTestName"); + done(); + }); + }); + }); + }); }); 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 4f96cc0af..fa46c7814 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 @@ -8,6 +8,11 @@ exports[`InteractiveReport Handles environment being started 1`] = ` expandStatus="default" extraButtons={ Array [ + , , , , , , , { } }; +/** + * Render a button to Run all Multitests. + * + * If the RunAll action is currently in progress, display an inactive icon + * instead. + */ +export const RunAllButton = (props) => { + if (props.running) { + return ( + +
+ +
+
+ ); + } else { + let title; + if (props.filter) { + title="Run filtered tests"; + } else { + title="Run all tests"; + } + return ( + +
+ +
+
+ ); + } +}; + const getHistoryTable = (historyExporters) => { if (Array.isArray(historyExporters) && historyExporters.length > 0) { const resultList = historyExporters.map((item, i) => { diff --git a/testplan/web_ui/testing/src/Toolbar/__tests__/InteractiveButtons.test.js b/testplan/web_ui/testing/src/Toolbar/__tests__/InteractiveButtons.test.js index a5b784966..24240fb2f 100644 --- a/testplan/web_ui/testing/src/Toolbar/__tests__/InteractiveButtons.test.js +++ b/testplan/web_ui/testing/src/Toolbar/__tests__/InteractiveButtons.test.js @@ -5,7 +5,7 @@ import React from "react"; import { shallow } from "enzyme"; import { StyleSheetTestUtils } from "aphrodite"; -import { ReloadButton, ResetButton, AbortButton } from "../InteractiveButtons"; +import { ReloadButton, ResetButton, AbortButton, RunAllButton } from "../InteractiveButtons"; describe("ReloadButton", () => { beforeEach(() => { @@ -94,3 +94,33 @@ describe("AbortButton", () => { expect(button).toMatchSnapshot(); }); }); + +describe("RunAllButton", () => { + beforeEach(() => { + // Stop Aphrodite from injecting styles, this crashes the tests. + StyleSheetTestUtils.suppressStyleInjection(); + }); + + afterEach(() => { + // Resume style injection once test is finished. + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + }); + + it("Renders a clickable button", () => { + const runAllCbk = jest.fn(); + const button = shallow( + + ); + expect(button).toMatchSnapshot(); + button.find({ title: "Run all tests" }).simulate("click"); + expect(runAllCbk.mock.calls.length).toBe(1); + }); + + it("Renders an inactive icon when running is in-progress", () => { + const runAllCbk = jest.fn(); + const button = shallow( + + ); + expect(button).toMatchSnapshot(); + }); +}); diff --git a/testplan/web_ui/testing/src/Toolbar/__tests__/Toolbar.test.js b/testplan/web_ui/testing/src/Toolbar/__tests__/Toolbar.test.js index 2036f4ca2..55414057c 100644 --- a/testplan/web_ui/testing/src/Toolbar/__tests__/Toolbar.test.js +++ b/testplan/web_ui/testing/src/Toolbar/__tests__/Toolbar.test.js @@ -8,7 +8,7 @@ import { TOOLBAR_BUTTONS_BATCH, TOOLBAR_BUTTONS_INTERACTIVE, } from "../../Common/defaults"; -import { ReloadButton, ResetButton, AbortButton } from "../InteractiveButtons"; +import { ReloadButton, ResetButton, AbortButton, RunAllButton } from "../InteractiveButtons"; function defaultProps() { return { @@ -74,6 +74,7 @@ describe("Toolbar", () => { it("inserts extra buttons into the toolbar", () => { const resetCbk = jest.fn(); props.extraButtons = [ + RunAllButton({ running: false, runAllCbk: jest.fn() }), ReloadButton({ reloading: false, reloadCbk: jest.fn() }), ResetButton({ resetting: false, resetStateCbk: jest.fn() }), AbortButton({ aborting: false, abortCbk: jest.fn() }), diff --git a/testplan/web_ui/testing/src/Toolbar/__tests__/__snapshots__/InteractiveButtons.test.js.snap b/testplan/web_ui/testing/src/Toolbar/__tests__/__snapshots__/InteractiveButtons.test.js.snap index b702ef556..979a01006 100644 --- a/testplan/web_ui/testing/src/Toolbar/__tests__/__snapshots__/InteractiveButtons.test.js.snap +++ b/testplan/web_ui/testing/src/Toolbar/__tests__/__snapshots__/InteractiveButtons.test.js.snap @@ -320,3 +320,110 @@ exports[`ResetButton Renders a spinning icon when reset is in-progress 1`] = ` `; + +exports[`RunAllButton Renders a clickable button 1`] = ` + +
+ +
+
+`; + +exports[`RunAllButton Renders an inactive icon when running is in-progress 1`] = ` + +
+ +
+
+`; diff --git a/testplan/web_ui/testing/src/Toolbar/__tests__/__snapshots__/Toolbar.test.js.snap b/testplan/web_ui/testing/src/Toolbar/__tests__/__snapshots__/Toolbar.test.js.snap index af7303dc8..f35c0f977 100644 --- a/testplan/web_ui/testing/src/Toolbar/__tests__/__snapshots__/Toolbar.test.js.snap +++ b/testplan/web_ui/testing/src/Toolbar/__tests__/__snapshots__/Toolbar.test.js.snap @@ -39,6 +39,57 @@ exports[`Toolbar inserts extra buttons into the toolbar 1`] = ` + +
+ +
+