diff --git a/bin/jmeter.properties b/bin/jmeter.properties index bac04e85bc9..0a1e8a5be1e 100644 --- a/bin/jmeter.properties +++ b/bin/jmeter.properties @@ -1217,6 +1217,15 @@ view.results.tree.renderers_order=.RenderAsText,.RenderAsRegexp,.RenderAsBoundar # Set to 0 to store all results (might consume a lot of memory) #view.results.tree.max_results=500 +# Set to true to enable by default the checkbox for auto scrolling. +#view.results.tree.autoscroll=false + +# Set to true to enable by default the checkbox for stop auto scrolling on 1st error. +#view.results.tree.autostop=true + +# Default width of the scroll view +#view.results.tree.width=250 + # Maximum size of Document that can be parsed by Tika engine; default=10 * 1024 * 1024 (10 MB) # Set to 0 to disable the size check #document.max_size=0 diff --git a/src/components/src/main/java/org/apache/jmeter/visualizers/SamplerResultTab.java b/src/components/src/main/java/org/apache/jmeter/visualizers/SamplerResultTab.java index 90819ac6c45..a83706812b6 100644 --- a/src/components/src/main/java/org/apache/jmeter/visualizers/SamplerResultTab.java +++ b/src/components/src/main/java/org/apache/jmeter/visualizers/SamplerResultTab.java @@ -87,7 +87,7 @@ public abstract class SamplerResultTab implements ResultRenderer { private static final Logger LOGGER = LoggerFactory.getLogger(SamplerResultTab.class); // N.B. these are not multi-threaded, so don't make it static private final DateTimeFormatter dateFormat = DateTimeFormatter - .ofPattern("yyyy-MM-dd HH:mm:ss z") // ISO format $NON-NLS-1$ + .ofPattern("yyyy-MM-dd HH:mm:ss.SSS O") // ISO format $NON-NLS-1$ .withZone(ZoneId.systemDefault()); private static final String NL = "\n"; // $NON-NLS-1$ @@ -265,6 +265,10 @@ public void setupTabPane() { .append(JMeterUtils .getResString("view_results_thread_name")).append(SPACE) //$NON-NLS-1$ .append(sampleResult.getThreadName()).append(NL); + statsBuff + .append(JMeterUtils + .getResString("view_results_sample_name")) //$NON-NLS-1$ + .append(sampleResult.getSampleLabel()).append(NL); String startTime = dateFormat .format(Instant.ofEpochMilli(sampleResult.getStartTime())); statsBuff @@ -371,6 +375,10 @@ public void setupTabPane() { resultModel.addRow(new RowResult( JMeterUtils.getParsedLabel("view_results_thread_name"), //$NON-NLS-1$ sampleResult.getThreadName())); + resultModel.addRow(new RowResult( + JMeterUtils.getParsedLabel( + "view_results_sample_name"), //$NON-NLS-1$ + sampleResult.getSampleLabel())); resultModel.addRow(new RowResult( JMeterUtils.getParsedLabel("view_results_sample_start"), //$NON-NLS-1$ startTime)); diff --git a/src/components/src/main/java/org/apache/jmeter/visualizers/ViewResultsFullVisualizer.java b/src/components/src/main/java/org/apache/jmeter/visualizers/ViewResultsFullVisualizer.java index 1c9470b45c4..b3c97201893 100644 --- a/src/components/src/main/java/org/apache/jmeter/visualizers/ViewResultsFullVisualizer.java +++ b/src/components/src/main/java/org/apache/jmeter/visualizers/ViewResultsFullVisualizer.java @@ -21,6 +21,8 @@ import java.awt.Color; import java.awt.Component; import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; @@ -43,8 +45,10 @@ import javax.swing.ComboBoxModel; import javax.swing.DefaultComboBoxModel; import javax.swing.ImageIcon; +import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; +import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; @@ -117,6 +121,15 @@ public class ViewResultsFullVisualizer extends AbstractVisualizer private static final String VIEWERS_ORDER = JMeterUtils.getPropDefault("view.results.tree.renderers_order", ""); // $NON-NLS-1$ //$NON-NLS-2$ + //default scroll checkbox status + private static final boolean SCROLL_CHECKBOX = JMeterUtils.getPropDefault("view.results.tree.autoscroll", false); + + //default uncheck if failed scroll checkbox status + private static final boolean SCROLL_STOP_CHECKBOX = JMeterUtils.getPropDefault("view.results.tree.autostop", true); + + // default tree scroll width + private static final int SCROLL_WIDTH = JMeterUtils.getPropDefault("view.results.tree.width", 250); // $NON-NLS-1$ + private static final int REFRESH_PERIOD = JMeterUtils.getPropDefault("jmeter.gui.refresh_period", 500); private static final ImageIcon imageSuccess = JMeterUtils.getImage( @@ -138,9 +151,10 @@ public class ViewResultsFullVisualizer extends AbstractVisualizer private ResultRenderer resultsRender = null; private Object resultsObject = null; private TreeSelectionEvent lastSelectionEvent; - private JCheckBox autoScrollCB; + private JCheckBox autoScrollCB, scrollStopCB; private final Queue buffer; private boolean dataChanged; + private SampleResult failedSampler; /** * Constructor @@ -163,6 +177,8 @@ public void add(final SampleResult sample) { synchronized (buffer) { buffer.add(sample); dataChanged = true; + if (!sample.isSuccessful() && failedSampler == null) + failedSampler = sample; } } @@ -170,6 +186,7 @@ public void add(final SampleResult sample) { * Update the visualizer with new data. */ private void updateGui() { + int failedSamplerPosition = 0; TreePath selectedPath = null; Object oldSelectedElement; Set oldExpandedElements; @@ -184,20 +201,19 @@ private void updateGui() { oldSelectedElement = getSelectedObject(); root.removeAllChildren(); for (SampleResult sampler: buffer) { - SampleResult res = sampler; // Add sample - DefaultMutableTreeNode currNode = new SearchableTreeNode(res, treeModel); + DefaultMutableTreeNode currNode = new SearchableTreeNode(sampler, treeModel); treeModel.insertNodeInto(currNode, root, root.getChildCount()); List path = new ArrayList<>(Arrays.asList(root, currNode)); selectedPath = checkExpandedOrSelected(path, - res, oldSelectedElement, + sampler, oldSelectedElement, oldExpandedElements, newExpandedPaths, selectedPath); - TreePath potentialSelection = addSubResults(currNode, res, path, oldSelectedElement, oldExpandedElements, newExpandedPaths); + TreePath potentialSelection = addSubResults(currNode, sampler, path, oldSelectedElement, oldExpandedElements, newExpandedPaths); if (potentialSelection != null) { selectedPath = potentialSelection; } // Add any assertion that failed as children of the sample node - AssertionResult[] assertionResults = res.getAssertionResults(); + AssertionResult[] assertionResults = sampler.getAssertionResults(); int assertionIndex = currNode.getChildCount(); for (AssertionResult assertionResult : assertionResults) { if (assertionResult.isFailure() || assertionResult.isError()) { @@ -209,6 +225,9 @@ private void updateGui() { assertionNode); } } + + if (sampler == failedSampler) + failedSamplerPosition = root.getChildCount() - 1; } treeModel.nodeStructureChanged(root); dataChanged = false; @@ -221,10 +240,14 @@ private void updateGui() { if (selectedPath != null) { jTree.setSelectionPath(selectedPath); } + if (autoScrollCB.isSelected() && root.getChildCount() > 1) { - jTree.scrollPathToVisible(new TreePath(new Object[] { root, - treeModel.getChild(root, root.getChildCount() - 1) })); + int pos = (scrollStopCB.isSelected() && failedSampler != null) ? failedSamplerPosition : root.getChildCount() - 1; + jTree.scrollPathToVisible(new TreePath(new Object[] { root, treeModel.getChild(root, pos) })); } + //if session not running, reset the error reference + if (!JMeterUtils.isTestRunning()) + failedSampler = null; } private Object getSelectedObject() { @@ -330,6 +353,7 @@ public void clearData() { } resultsRender.clearData(); resultsObject = null; + failedSampler = null; } /** {@inheritDoc} */ @@ -359,7 +383,7 @@ private void init() { // WARNING: called from ctor so must not be overridden (i searchAndMainSP.setOneTouchExpandable(true); JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, makeTitlePanel(), searchAndMainSP); splitPane.setOneTouchExpandable(true); - splitPane.setBorder(BorderFactory.createEmptyBorder()); + splitPane.setBorder(null); add(splitPane); // init right side with first render @@ -392,6 +416,44 @@ private void valueChanged(TreeSelectionEvent e, boolean forceRendering) { } Object userObject = node.getUserObject(); resultsRender.setSamplerResult(userObject); + + // Add Show Current Node button to the tab pane + if (rightSide.getTabCount() > 0) { + Component firstTab = rightSide.getComponentAt(0); + if (firstTab instanceof JPanel) { + JPanel samplerResultTab = (JPanel) firstTab; + // Add button at the top of the sampler result panel + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JButton nodeInfoButton = new JButton(JMeterUtils.getResString("show_current_node")); // $NON-NLS-1$ + nodeInfoButton.addActionListener(e2 -> { + // Get the currently selected node when button is clicked + DefaultMutableTreeNode currentNode = (DefaultMutableTreeNode) jTree.getLastSelectedPathComponent(); + if (currentNode != null) { + TreePath path = new TreePath(currentNode.getPath()); + // First expand the path to make sure all parent nodes are visible + jTree.expandPath(path); + // Get the bounds before scrolling + Rectangle bounds = jTree.getPathBounds(path); + if (bounds != null) { + // Get the visible rectangle + Rectangle visible = jTree.getVisibleRect(); + // Calculate the new viewport position to center the node + int centerY = Math.max(0, bounds.y - (visible.height - bounds.height) / 2); + // Create rectangle that will center the node + Rectangle scrollTo = new Rectangle(0, centerY, 1, visible.height); + jTree.scrollRectToVisible(scrollTo); + // Ensure the node is selected and visible + jTree.setSelectionPath(path); + jTree.repaint(); + } + } + }); + buttonPanel.add(nodeInfoButton); + samplerResultTab.add(buttonPanel, BorderLayout.NORTH); + samplerResultTab.revalidate(); + } + } + resultsRender.setupTabPane(); // Processes Assertions // display a SampleResult if (userObject instanceof SampleResult) { @@ -431,15 +493,42 @@ private synchronized Component createLeftPanel() { jTree.setRootVisible(false); jTree.setShowsRootHandles(true); JScrollPane treePane = new JScrollPane(jTree); - treePane.setPreferredSize(new Dimension(200, 300)); + treePane.setPreferredSize(new Dimension(SCROLL_WIDTH, 300)); VerticalPanel leftPane = new VerticalPanel(); - leftPane.add(treePane, BorderLayout.CENTER); - leftPane.add(createComboRender(), BorderLayout.NORTH); + + // Add controls panel at the top + JPanel controlsPane = new JPanel(new FlowLayout(FlowLayout.LEFT)); + controlsPane.add(createComboRender()); + leftPane.add(controlsPane, BorderLayout.NORTH); + + // Add tree panel in the center with proper sizing + JPanel treePanel = new JPanel(new BorderLayout()); + treePanel.add(treePane, BorderLayout.CENTER); + leftPane.add(treePanel, BorderLayout.CENTER); + + // Create bottom panel with checkbox + JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); + + // Add autoscroll checkbox autoScrollCB = new JCheckBox(JMeterUtils.getResString("view_results_autoscroll")); // $NON-NLS-1$ - autoScrollCB.setSelected(false); + autoScrollCB.setSelected(SCROLL_CHECKBOX); autoScrollCB.addItemListener(this); - leftPane.add(autoScrollCB, BorderLayout.SOUTH); + bottomPanel.add(autoScrollCB); + + // Add autoscroll checkbox + scrollStopCB = new JCheckBox(JMeterUtils.getResString("view_results_autostop")); // $NON-NLS-1$ + scrollStopCB.setSelected(SCROLL_STOP_CHECKBOX); + scrollStopCB.addItemListener(this); + bottomPanel.add(scrollStopCB); + autoScrollCB.doClick(); // to set the initial state of scrollStopCB + + leftPane.add(bottomPanel, BorderLayout.SOUTH); + + // Ensure the left pane maintains its size during window resizing + leftPane.setMinimumSize(new Dimension(SCROLL_WIDTH, 0)); + leftPane.setPreferredSize(new Dimension(SCROLL_WIDTH, 0)); + return leftPane; } @@ -616,6 +705,14 @@ public Component getTreeCellRendererComponent(JTree tree, Object value, */ @Override public void itemStateChanged(ItemEvent e) { - // NOOP state is held by component + Object source = e.getItemSelectable(); + if (source == autoScrollCB) { + scrollStopCB.setEnabled(autoScrollCB.isSelected()); + } + } + + @Override + public String[] getFileExts() { + return new String[] { ".jtl" }; } } diff --git a/src/core/src/main/java/org/apache/jmeter/visualizers/gui/AbstractVisualizer.java b/src/core/src/main/java/org/apache/jmeter/visualizers/gui/AbstractVisualizer.java index 68802c6723d..0b463cd48e0 100644 --- a/src/core/src/main/java/org/apache/jmeter/visualizers/gui/AbstractVisualizer.java +++ b/src/core/src/main/java/org/apache/jmeter/visualizers/gui/AbstractVisualizer.java @@ -104,7 +104,7 @@ public abstract class AbstractVisualizer /** Logging. */ private static final Logger log = LoggerFactory.getLogger(AbstractVisualizer.class); - /** File Extensions */ + /** Default File Extensions */ private static final String[] EXTS = { ".xml", ".jtl", ".csv" }; // $NON-NLS-1$ $NON-NLS-2$ $NON-NLS-3$ /** A panel allowing results to be saved. */ @@ -150,7 +150,7 @@ public void actionPerformed(ActionEvent e) { d.setVisible(true); }); - filePanel = new FilePanel(JMeterUtils.getResString("file_visualizer_output_file"), EXTS); // $NON-NLS-1$ + filePanel = new FilePanel(JMeterUtils.getResString("file_visualizer_output_file"), getFileExts()); // $NON-NLS-1$ filePanel.addChangeListener(this); filePanel.add(new JLabel(JMeterUtils.getResString("log_only"))); // $NON-NLS-1$ filePanel.add(errorLogging); @@ -346,4 +346,8 @@ public void clearGui(){ super.clearGui(); filePanel.clearGui(); } + + public String[] getFileExts() { + return EXTS; + } } diff --git a/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties b/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties index 23721c4b4c9..8d146e06914 100644 --- a/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties +++ b/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties @@ -1401,7 +1401,8 @@ view_graph_tree_title=View Graph Tree view_results_assertion_error=Assertion error: view_results_assertion_failure=Assertion failure: view_results_assertion_failure_message=Assertion failure message: -view_results_autoscroll=Scroll automatically? +view_results_autoscroll=Auto scroll +view_results_autostop=Stop scroll on error view_results_childsamples=Child samples? view_results_datatype=Data type ("text"|"bin"|""): view_results_desc=Shows the text results of sampling in tree form @@ -1461,6 +1462,7 @@ view_results_table_request_tab_raw=Raw view_results_table_result_tab_parsed=Parsed view_results_table_result_tab_raw=Raw view_results_thread_name=Thread Name: +view_results_sample_name=Sample label: view_results_title=View Results view_results_tree_title=View Results Tree warning=Warning! @@ -1545,3 +1547,4 @@ xpath2_extractor_match_number_failure=MatchNumber out of bonds \: you_must_enter_a_valid_number=You must enter a valid number zh_cn=Chinese (Simplified) zh_tw=Chinese (Traditional) +show_current_node=Focus on selected node diff --git a/xdocs/usermanual/properties_reference.xml b/xdocs/usermanual/properties_reference.xml index d74ece0613d..9a7e7e14133 100644 --- a/xdocs/usermanual/properties_reference.xml +++ b/xdocs/usermanual/properties_reference.xml @@ -1556,6 +1556,18 @@ JMETER-SERVER Can be switched off by setting the value to -1.
Defaults to: 10000 + + Set to true to enable by default the checkbox for auto scrolling.
+ Defaults to: false +
+ + Set to true to enable by default the checkbox for stop auto scrolling on 1st error.
+ Defaults to: true +
+ + Default width of the scroll view.
+ Defaults to: 250 +
Maximum size (in bytes) of Document that can be parsed by Tika engine
Set to zero to disable the size check.