Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ An example on how to do this: `((RemoteWebDriver) driver).getSessionId().toStrin

A full example that you can use is available on our GitHub [Jenkins-Demo](https://github.com/testingbot/Jenkins-Demo) page.

On the build page, each JUnit test that used TestingBot is shown with a preview thumbnail of its session (the first one, if a test records several) and expands in place to the full session media (video screencast, screenshots and logs) — no need to leave Jenkins. The embedded media loads lazily, so collapsed sessions stay lightweight.

## Embedded TestingBot Build report

Whenever the plugin injects credentials — through the freestyle **Build Environment** option or the `testingbot { }` / `testingbotTunnel { }` pipeline steps — it exposes a `TESTINGBOT_BUILD` environment variable and adds a **TestingBot Build** page to the build.
Expand Down
24 changes: 21 additions & 3 deletions src/main/java/testingbot/TestingBotBuildObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.testingbot.models.TestingbotTest;

import java.io.Serializable;
import java.util.List;

public class TestingBotBuildObject implements Serializable {
private static final long serialVersionUID = 1L;
Expand All @@ -12,16 +13,33 @@ public class TestingBotBuildObject implements Serializable {
private final boolean isPassed;
private String authHash;
private String environmentName;
private String thumbUrl;

public TestingBotBuildObject(String sessionId, String className, String testName, boolean isPassed, String authHash, TestingbotTest test) {
this.sessionId = sessionId;
this.className = className;
this.testName = testName;
this.isPassed = isPassed;
this.authHash = authHash;
// TestingbotTest is only used here to derive the environment label; it is not stored (it is not
// marshalable under the JEP-200 class filter, and nothing reads it afterwards).
this.environmentName = test == null ? "" : test.getBrowser() + " | " + test.getOs();
// TestingbotTest is only used here to derive display fields; it is not stored (it is not
// marshalable under the JEP-200 class filter, and nothing reads it afterwards). Only the derived
// Strings are kept, which marshal cleanly.
if (test == null) {
this.environmentName = "";
this.thumbUrl = "";
} else {
this.environmentName = test.getBrowser() + " | " + test.getOs();
List<String> thumbs = test.getThumbs();
this.thumbUrl = (test.isAssetsAvailable() && thumbs != null && !thumbs.isEmpty()) ? thumbs.get(0) : "";
}
}

/**
* @return a preview thumbnail URL for the session, or an empty string when no assets are available.
*/
public String getThumbUrl() {
// Null-safe for build.xml records persisted before this field existed (deserialized as null).
return thumbUrl == null ? "" : thumbUrl;
}

/**
Expand Down
77 changes: 37 additions & 40 deletions src/main/resources/testingbot/TestingBotBuildSummary/summary.jelly
Original file line number Diff line number Diff line change
@@ -1,43 +1,40 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<t:summary icon="/plugin/testingbot/images/logo.svg">
<strong>TestingBot results</strong>
<br />
<table>
<thead>
<th>TestName</th>
<th>Environment</th>
<th>Passed?</th>
</thead>
<tbody>
<j:forEach var="cs" items="${it.sessionIds}">
<l:ajax>
<tr>
<td>
<a href="testingbot-embed/?sessionId=${cs.sessionId}">${cs.testName}</a>
</td>
<td>
${cs.environmentName}
</td>
<td style="text-align: center">
<j:if test="${cs.isPassed == true}">
<svg x="0px" y="0px" width="16px" height="16px" viewBox="0 0 97.619 97.618" style="enable-background:new 0 0 97.619 97.618;" xml:space="preserve">
<g>
<path d="M96.939,17.358L83.968,5.959c-0.398-0.352-0.927-0.531-1.449-0.494C81.99,5.5,81.496,5.743,81.146,6.142L34.1,59.688 L17.372,37.547c-0.319-0.422-0.794-0.701-1.319-0.773c-0.524-0.078-1.059,0.064-1.481,0.385L0.794,47.567 c-0.881,0.666-1.056,1.92-0.39,2.801l30.974,40.996c0.362,0.479,0.922,0.771,1.522,0.793c0.024,0,0.049,0,0.073,0 c0.574,0,1.122-0.246,1.503-0.68l62.644-71.297C97.85,19.351,97.769,18.086,96.939,17.358z" fill="#91DC5A"/>
</g>
<strong>TestingBot results</strong>
<br />
<j:forEach var="cs" items="${it.sessionIds}">
<details style="margin: 8px 0;">
<summary style="cursor: pointer; display: flex; align-items: center; gap: 8px;">
<j:if test="${cs.thumbUrl != ''}">
<img src="${cs.thumbUrl}" alt="" loading="lazy"
style="height: 48px; border: 1px solid var(--medium-grey, #ccc); border-radius: 3px;" />
</j:if>
<span><strong>${cs.testName}</strong></span>
<span style="color: var(--text-color-secondary, #666);">${cs.environmentName}</span>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<j:choose>
<j:when test="${cs.isPassed == true}">
<svg width="14px" height="14px" viewBox="0 0 97.619 97.618" xml:space="preserve" role="img" aria-label="Passed">
<path d="M96.939,17.358L83.968,5.959c-0.398-0.352-0.927-0.531-1.449-0.494C81.99,5.5,81.496,5.743,81.146,6.142L34.1,59.688 L17.372,37.547c-0.319-0.422-0.794-0.701-1.319-0.773c-0.524-0.078-1.059,0.064-1.481,0.385L0.794,47.567 c-0.881,0.666-1.056,1.92-0.39,2.801l30.974,40.996c0.362,0.479,0.922,0.771,1.522,0.793c0.024,0,0.049,0,0.073,0 c0.574,0,1.122-0.246,1.503-0.68l62.644-71.297C97.85,19.351,97.769,18.086,96.939,17.358z" fill="#91DC5A"/>
</svg>
</j:if>
<j:if test="${cs.isPassed == false}">
<svg x="0px" y="0px" width="16px" height="16px" viewBox="0 0 348.333 348.334" style="enable-background:new 0 0 348.333 348.334;" xml:space="preserve">
<g>
<path d="M336.559,68.611L231.016,174.165l105.543,105.549c15.699,15.705,15.699,41.145,0,56.85 c-7.844,7.844-18.128,11.769-28.407,11.769c-10.296,0-20.581-3.919-28.419-11.769L174.167,231.003L68.609,336.563 c-7.843,7.844-18.128,11.769-28.416,11.769c-10.285,0-20.563-3.919-28.413-11.769c-15.699-15.698-15.699-41.139,0-56.85 l105.54-105.549L11.774,68.611c-15.699-15.699-15.699-41.145,0-56.844c15.696-15.687,41.127-15.687,56.829,0l105.563,105.554 L279.721,11.767c15.705-15.687,41.139-15.687,56.832,0C352.258,27.466,352.258,52.912,336.559,68.611z" fill="#D80027"/>
</g>
</svg>
</j:if>
</td>
</tr>
</l:ajax>
</j:forEach>
</tbody>
</table>
</t:summary>
</j:jelly>
</j:when>
<j:otherwise>
<svg width="14px" height="14px" viewBox="0 0 348.333 348.334" xml:space="preserve" role="img" aria-label="Failed">
<path d="M336.559,68.611L231.016,174.165l105.543,105.549c15.699,15.705,15.699,41.145,0,56.85 c-7.844,7.844-18.128,11.769-28.407,11.769c-10.296,0-20.581-3.919-28.419-11.769L174.167,231.003L68.609,336.563 c-7.843,7.844-18.128,11.769-28.416,11.769c-10.285,0-20.563-3.919-28.413-11.769c-15.699-15.698-15.699-41.139,0-56.85 l105.54-105.549L11.774,68.611c-15.699-15.699-15.699-41.145,0-56.844c15.696-15.687,41.127-15.687,56.829,0l105.563,105.554 L279.721,11.767c15.705-15.687,41.139-15.687,56.832,0C352.258,27.466,352.258,52.912,336.559,68.611z" fill="#D80027"/>
</svg>
</j:otherwise>
</j:choose>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</summary>
<div style="margin: 6px 0 12px;">
<a href="https://testingbot.com/tests/${cs.sessionId}?auth=${cs.authHash}&amp;ref=jenkins" target="_blank">View on TestingBot</a>
<div>
<!-- loading="lazy" defers the embed until the row is expanded (the iframe is
display:none inside a closed details), so collapsed rows stay lightweight. -->
<iframe src="https://testingbot.com/mini/?id=${cs.sessionId}&amp;auth=${cs.authHash}&amp;ref=jenkins"
loading="lazy" style="width: 100%; min-height: 600px; border: none"></iframe>
Comment on lines +31 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files first
printf '\n== Files ==\n'
git ls-files | rg '^(src/main/resources/testingbot/TestingBot(BuildSummary|TestEmbed)/|README\.md)'

printf '\n== Outline: summary.jelly ==\n'
ast-grep outline src/main/resources/testingbot/TestingBotBuildSummary/summary.jelly --view expanded || true

printf '\n== Outline: index.jelly ==\n'
ast-grep outline src/main/resources/testingbot/TestingBotTestEmbed/index.jelly --view expanded || true

printf '\n== Read summary.jelly around the reported lines ==\n'
sed -n '1,220p' src/main/resources/testingbot/TestingBotBuildSummary/summary.jelly | nl -ba | sed -n '1,120p'

printf '\n== Read index.jelly around the reported lines ==\n'
sed -n '1,220p' src/main/resources/testingbot/TestingBotTestEmbed/index.jelly | nl -ba | sed -n '1,140p'

printf '\n== Search for expansion/loading logic or data-src usage ==\n'
rg -n "data-src|loading=|details|toggle|expand|iframe" src/main/resources src/main/java README.md

Repository: testingbot/TestingBot-Jenkins-Plugin

Length of output: 566


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# If the first search suggests client-side behavior, inspect any associated JS/Stapler resources in this plugin.
git ls-files | rg '(^src/main/(resources|webapp|javascript|js|java)/|package\.json$|pom\.xml$)'

printf '\n== Search for TestingBot-related client code ==\n'
rg -n "TestingBot|details|toggle|iframe|loading=\"lazy\"|data-src" src/main/resources src/main/java src/main/webapp src/main/js src/main/javascript 2>/dev/null || true

Repository: testingbot/TestingBot-Jenkins-Plugin

Length of output: 28441


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether README wording aligns with the current UI markup
sed -n '45,70p' README.md | nl -ba

printf '\n== Search for the exact README claim elsewhere ==\n'
rg -n "preview thumbnail|expands in place|Collapsed sessions stay lightweight|media only loads" .

Repository: testingbot/TestingBot-Jenkins-Plugin

Length of output: 214


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## summary.jelly"
sed -n '1,120p' src/main/resources/testingbot/TestingBotBuildSummary/summary.jelly | nl -ba

echo
echo "## index.jelly"
sed -n '1,140p' src/main/resources/testingbot/TestingBotTestEmbed/index.jelly | nl -ba

echo
echo "## README.md"
sed -n '50,62p' README.md | nl -ba

echo
echo "## TestingBot-related code search"
rg -n "TestingBot|loading=\"lazy\"|data-src|details|toggle|expand|iframe" src/main/resources src/main/java README.md || true

Repository: testingbot/TestingBot-Jenkins-Plugin

Length of output: 231


🌐 Web query:

HTMLIFrameElement loading lazy is a hint not a guarantee iframe inside closed details display:none loads when expanded

💡 Result:

The loading="lazy" attribute on an iframe is a hint to the browser to defer loading until the element is near the viewport, not a guarantee that it will behave in a specific way relative to CSS visibility [1][2]. When an iframe with loading="lazy" is placed inside a closed

Details element (which applies display: none to its contents), the browser's behavior is inconsistent and generally unreliable for triggering a load upon expansion [3][4]. 1. Why it often fails: The native lazy-loading mechanism for iframes typically relies on an Intersection Observer [5]. When an element is inside a closed
element, it has no layout dimensions (it is essentially display: none) [4]. Consequently, it cannot intersect with the viewport. Even when the
element is expanded, browsers may not always re-evaluate the lazy-loading trigger for that specific iframe, causing it to remain in a non-loaded state [3][4]. 2. Not a Guarantee: As noted, loading="lazy" is a hint [2]. Browsers prioritize performance, and their heuristics for when to load these elements can vary across implementations and versions [2][3]. 3. Recommended Workaround: If you need to ensure an iframe loads only when a
element is opened, you should not rely on the loading="lazy" attribute alone [4]. Instead, use a JavaScript-based approach: - Keep the iframe's src attribute empty (or set to a placeholder) initially. - Add an event listener to the
element for the toggle event. - When the
element opens, set the src attribute of the iframe via JavaScript to trigger the load [6][5]. This provides a deterministic way to load the content only when the user interacts with the UI [6].

Citations:


Defer the TestingBot iframe until the row opens. loading="lazy" is only a hint, so the embed can still start fetching before expansion. Move the URL to data-src and set src on first open in:

  • src/main/resources/testingbot/TestingBotBuildSummary/summary.jelly
  • src/main/resources/testingbot/TestingBotTestEmbed/index.jelly

Keep the README claim only once that behavior is enforced.

📍 Affects 3 files
  • src/main/resources/testingbot/TestingBotBuildSummary/summary.jelly#L30-L33 (this comment)
  • src/main/resources/testingbot/TestingBotTestEmbed/index.jelly#L31-L35
  • README.md#L57-L57
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/resources/testingbot/TestingBotBuildSummary/summary.jelly` around
lines 30 - 33, The TestingBot iframes must not fetch until their rows open. In
TestingBotBuildSummary/summary.jelly and TestingBotTestEmbed/index.jelly,
replace the iframe src with data-src and add first-open handling that copies
data-src to src when the containing row is expanded; update README.md to retain
the lazy-loading behavior claim only once.

</div>
</div>
</details>
</j:forEach>
</t:summary>
</j:jelly>
73 changes: 33 additions & 40 deletions src/main/resources/testingbot/TestingBotTestEmbed/index.jelly
Original file line number Diff line number Diff line change
@@ -1,49 +1,42 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:st="jelly:stapler">
<l:layout title="Greeting">
<l:layout title="TestingBot">
<l:side-panel>
<st:include page="sidepanel.jelly" it="${it.run}" optional="true" />
</l:side-panel>
<l:main-panel>
<h1>TestingBot results</h1>
<table>
<thead>
<th>TestName</th>
<th>Environment</th>
<th>Passed?</th>
</thead>
<tbody>
<j:forEach var="cs" items="${it.sessionIds}">
<l:ajax>
<tr>
<td>
<a href="?sessionId=${cs.sessionId}">${cs.testName}</a>
</td>
<td>
${cs.environmentName}
</td>
<td style="text-align: center">
<j:if test="${cs.isPassed == true}">
<svg x="0px" y="0px" width="16px" height="16px" viewBox="0 0 97.619 97.618" style="enable-background:new 0 0 97.619 97.618;" xml:space="preserve">
<g>
<path d="M96.939,17.358L83.968,5.959c-0.398-0.352-0.927-0.531-1.449-0.494C81.99,5.5,81.496,5.743,81.146,6.142L34.1,59.688 L17.372,37.547c-0.319-0.422-0.794-0.701-1.319-0.773c-0.524-0.078-1.059,0.064-1.481,0.385L0.794,47.567 c-0.881,0.666-1.056,1.92-0.39,2.801l30.974,40.996c0.362,0.479,0.922,0.771,1.522,0.793c0.024,0,0.049,0,0.073,0 c0.574,0,1.122-0.246,1.503-0.68l62.644-71.297C97.85,19.351,97.769,18.086,96.939,17.358z" fill="#91DC5A"/>
</g>
</svg>
</j:if>
<j:if test="${cs.isPassed == false}">
<svg x="0px" y="0px" width="16px" height="16px" viewBox="0 0 348.333 348.334" style="enable-background:new 0 0 348.333 348.334;" xml:space="preserve">
<g>
<path d="M336.559,68.611L231.016,174.165l105.543,105.549c15.699,15.705,15.699,41.145,0,56.85 c-7.844,7.844-18.128,11.769-28.407,11.769c-10.296,0-20.581-3.919-28.419-11.769L174.167,231.003L68.609,336.563 c-7.843,7.844-18.128,11.769-28.416,11.769c-10.285,0-20.563-3.919-28.413-11.769c-15.699-15.698-15.699-41.139,0-56.85 l105.54-105.549L11.774,68.611c-15.699-15.699-15.699-41.145,0-56.844c15.696-15.687,41.127-15.687,56.829,0l105.563,105.554 L279.721,11.767c15.705-15.687,41.139-15.687,56.832,0C352.258,27.466,352.258,52.912,336.559,68.611z" fill="#D80027"/>
</g>
</svg>
</j:if>
</td>
</tr>
</l:ajax>
</j:forEach>
</tbody>
</table>

<j:forEach var="cs" items="${it.sessionIds}">
<details style="margin: 8px 0;">
<summary style="cursor: pointer; display: flex; align-items: center; gap: 8px;">
<j:if test="${cs.thumbUrl != ''}">
<img src="${cs.thumbUrl}" alt="" loading="lazy"
style="height: 60px; border: 1px solid var(--medium-grey, #ccc); border-radius: 3px;" />
</j:if>
<span><strong>${cs.testName}</strong></span>
<span style="color: var(--text-color-secondary, #666);">${cs.environmentName}</span>
<j:choose>
<j:when test="${cs.isPassed == true}">
<svg width="14px" height="14px" viewBox="0 0 97.619 97.618" xml:space="preserve" role="img" aria-label="Passed">
<path d="M96.939,17.358L83.968,5.959c-0.398-0.352-0.927-0.531-1.449-0.494C81.99,5.5,81.496,5.743,81.146,6.142L34.1,59.688 L17.372,37.547c-0.319-0.422-0.794-0.701-1.319-0.773c-0.524-0.078-1.059,0.064-1.481,0.385L0.794,47.567 c-0.881,0.666-1.056,1.92-0.39,2.801l30.974,40.996c0.362,0.479,0.922,0.771,1.522,0.793c0.024,0,0.049,0,0.073,0 c0.574,0,1.122-0.246,1.503-0.68l62.644-71.297C97.85,19.351,97.769,18.086,96.939,17.358z" fill="#91DC5A"/>
</svg>
</j:when>
<j:otherwise>
<svg width="14px" height="14px" viewBox="0 0 348.333 348.334" xml:space="preserve" role="img" aria-label="Failed">
<path d="M336.559,68.611L231.016,174.165l105.543,105.549c15.699,15.705,15.699,41.145,0,56.85 c-7.844,7.844-18.128,11.769-28.407,11.769c-10.296,0-20.581-3.919-28.419-11.769L174.167,231.003L68.609,336.563 c-7.843,7.844-18.128,11.769-28.416,11.769c-10.285,0-20.563-3.919-28.413-11.769c-15.699-15.698-15.699-41.139,0-56.85 l105.54-105.549L11.774,68.611c-15.699-15.699-15.699-41.145,0-56.844c15.696-15.687,41.127-15.687,56.829,0l105.563,105.554 L279.721,11.767c15.705-15.687,41.139-15.687,56.832,0C352.258,27.466,352.258,52.912,336.559,68.611z" fill="#D80027"/>
</svg>
</j:otherwise>
</j:choose>
</summary>
<div style="margin: 6px 0 12px;">
<a href="https://testingbot.com/tests/${cs.sessionId}?auth=${cs.authHash}&amp;ref=jenkins" target="_blank">View on TestingBot</a>
<div>
<iframe src="https://testingbot.com/mini/?id=${cs.sessionId}&amp;auth=${cs.authHash}&amp;ref=jenkins"
loading="lazy" style="width: 100%; min-height: 600px; border: none"></iframe>
</div>
</div>
</details>
</j:forEach>
</l:main-panel>
</l:layout>
</j:jelly>
</j:jelly>