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`] = `
+
+
+
+
+