-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Java visual #276
base: main
Are you sure you want to change the base?
Java visual #276
Conversation
import java.io.InputStreamReader; | ||
|
||
public class GitManager { | ||
private static String currentBranch = "_default_"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does such branch (default) really exist?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you don't specify anything, Screener uses (default)
, presumably to make sure it isn't a real branch. Unfortunately, this prevents me from clicking a link with that URL because my IDE doesn't like that character. So I changed the default to _default_
, also unlikely to be the actual branch name, but I can click on resulting links.
} catch (IOException | InterruptedException e) { | ||
// Ignore exception and use default | ||
} | ||
return currentBranch; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it ok that a previously remembered value is returned if an exception happens?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trying to think of a scenario where that could happen and be bad. It's going to just use the default value set at the top. I made these static methods/fields because no one should be switching git branches in the middle of their test execution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my scenario is:
- get current branch
- the branch is changed
- get current branch and get an exception
- the previous (incorrect) branch name is returned
If we want to always return the cached value then this method should probably have an explicit condition for that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you can programmatically change branches in the middle of a test run. If it was found before, use that, if it wasn't found before, use default. This is a weird nice-to-have-but-not-very-important feature in the scheme of this project.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this totally makes sense to me. What I want to say is that this method is going to still spend time for the command line execution even though the branch name has been already retrieved. the condition I was talking about:
if (!Objects.equal(currentBranch, DEFAULT_BRANCH)) {
return currentBranch;
}
... do existing stuff to get the branch name from git
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); | ||
String line = reader.readLine(); | ||
if (!line.contains("fatal:")) { | ||
currentBranch = line; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should the line be trimmed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean? (and I don't remember what all I did to get while trying to get this to work when I wrote it over a year ago).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean whether it could be that line
ends with a line break. Also, I would assume it would be safer to check the process exit code than the actual stdout
|
||
this.passed = (Boolean) results.get("passed"); | ||
this.message = (String) results.get("message"); | ||
this.status = (String) results.get("status"); // success, failure, error, timeout, cancelled |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does it make sense to have an enum instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I usually only do enums for Constructor/Method parameters so users don't have to remember the "magic string" to get the code to work correctly. We could do it for the return value here, but the use case for 99% of people is seeing the result displayed in stdout at the end of their test. I can't see anyone asserting on this or using it in logic anywhere.
this.message = (String) results.get("message"); | ||
this.status = (String) results.get("status"); // success, failure, error, timeout, cancelled | ||
|
||
for (Map state : (List<Map>) results.get("states")) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this could also be rewritten in more functional style:
((List<Map>) results.get("states")).map(VisualSnapshot::new).forEach(snapshots::add)
|
||
@Setter @Getter | ||
public class VisualResults { | ||
Map<String, Object> results; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should the field be private?
@Override | ||
public URL getSauceUrl() { | ||
try { | ||
return new URL("https://hub.screener.io/wd/hub"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should the URL be moved into a constant/config?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. the getSauceUrl()
in the superclass is based on sauceUrl
field, which is a Data Center Enum, which doesn't apply for visual. How we use it here is probably going to depend on if we're planning on having a new visual data center or somehow merge the screener.io into saucelabs.com. I'll get more info.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sauce does have plans to add data centers (internal link- https://saucedev.atlassian.net/browse/VSLT-2178) I'll figure out what the plan is for formatting, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what I just got about future data center URL, so I should definitely create a VisualDataCenter
enum for these and update when necessary.
[11:19 AM] the future endpoints are going to be visual.eu-central-1.saucelabs.com and visual.hub.eu-central-1.saucelabs.com
@Override | ||
public void stop(String result) { | ||
if (driver != null) { | ||
System.out.println(getVisualResults().getSummary()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we really want to use println? this function does not allow any external configuration/customization
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want users to be able to know how to see Sauce info by looking at their Jenkins console logs, etc.
For instance, the Jenkins plugin does this by default.
Making it configurable is possible, but out of scope for current purposes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really, the link to the build is the most important thing here, imo, which is why it is frustrating that Screener doesn't expose it if the test passes. Trying to get that changed.
|
||
@Override | ||
public void stop(Boolean passed) { | ||
String result = passed ? "passed" : "failed"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
magic strings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You know, this is a good point. It comes from our using — https://docs.saucelabs.com/basics/test-config-annotation/test-annotation/#methods which is literally *all magic strings.
When we update the implementation to use the API instead of JS to record pass/fail, then the magic strings are completely useless... Yeah, now I want to deprecate the method in SauceSession
instead of supporting it in VisualSession
.
} | ||
} | ||
|
||
public void newVisualTest(String testName) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like when methods return this
instead of void
, so calls could be chained
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heh, I was just getting chastised for using a fluent pattern in code I wrote for Selenium, and that was for a builder construction where it makes more sense to me. In this case it doesn't matter, there is nothing else for the Session to do after this method is called, the next line will be using the driver.
if (driver != null) { | ||
VisualResults results = getVisualResults(); | ||
System.out.println(results.getSummary()); | ||
String result = results.getPassed() ? "passed" : "failed"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can observe some code duplication here
|
||
@Override | ||
public RemoteWebDriver start() { | ||
this.driver = createRemoteWebDriver(getSauceUrl(), visualOptions.toCapabilities()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume this.
is redundant
private Boolean iframes = null; | ||
private Map<String, Object> iframesOptions = null; | ||
|
||
public final List<String> validOptions = Arrays.asList( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I usually wrap constants into Collections.immutableList
public MutableCapabilities toCapabilities() { | ||
String msg = "Environment Variable or System Property for 'SCREENER_API_KEY' must be provided"; | ||
String sauceVisualApiKey = SystemManager.get("SCREENER_API_KEY", msg); | ||
capabilities.setCapability("apiKey", sauceVisualApiKey); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO it is not a very secure practice to expose API keys in capabilities as their values are visible in logs. Why not, for example, use request headers for such purpose?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Screener doesn't accept it in the header (because that's for Sauce credentials). It has to be in the capabilities.
As for body vs header, it might be pedantic, but it is security by convention rather than an actual difference in security. Nothing prevents a logger from recording header information even if it is less common. Either way, Sauce removes references to these keys in our logs.
if (key.equals("visual")) { | ||
((Map<String, Object>) entry.getValue()).forEach(this::setCapability); | ||
} else { | ||
sauceOptions.setCapability(key, entry.getValue()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can sauceOptions be null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, there are 2 constructors. One that accepts a SauceOptions
instance, the other creates one.
session.takeSnapshot("Name of Snapshot"); | ||
|
||
// 8. Stop the Session | ||
session.stop(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume this call should be in a finally
block or AfterEach section
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The examples
directory is for displaying on our website, and are designed to show how easy it is to get the functionality. Best practices would be to use it with the test runner library and not have to think about it at all. :)
|
||
assertNull(results.getMessage()); | ||
|
||
assertEquals(Long.valueOf(1), results.getTotal()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Long.valueOf(1)
could be simplified to 1L
|
||
public void newVisualTest(String testName) { | ||
validateVisualStatus(); | ||
driver.executeScript("/*@visual.init*/", testName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shall we first verify is the session is active/driver is initialised? I assume customers might be confused by NPE if they call this method without invoking start() first
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same comment to the below methods. They only make sense while the session is active
assertEquals("VisualTest getVisualResults", snapshot.getGroupName()); | ||
assertEquals("accepted", snapshot.getStatus()); | ||
String url = snapshot.getUrl(); | ||
assertTrue(url.contains(GitManager.getCurrentBranch())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer to use something like Assert.assertThat(x, CoreMatchers.containsString("foo"));
. This is giving much better error messages with actual/expected values instead of just true/false
driver.executeScript("/*@visual.snapshot*/", name); | ||
} | ||
|
||
public VisualResults getVisualResults() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shall we make it possible to call this method after the session is stopped?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It needs the driver to call executeScript, and we don't have the driver after the session is stopped
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes I know. Currently NPE is going to be thrown in such case (which is anyway not very useful for a client). My though was that we could automatically cache that result if stop
is called, so we don't need to query driver instance anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been treating stop()
as the final everything is done method, but yeah, there's no reason we couldn't cache this.
.build(); | ||
|
||
Map<String, Object> diffOptions = new HashMap<>(); | ||
diffOptions.put("structure", true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this map initialisation syntax a bit too talkative. We might use some available shortcuts instead: https://www.baeldung.com/java-initialize-hashmap
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW, guava is a part of selenium, so it is safe to use ImmutableMap.of()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lol, yeah, I just started using it. This code is a copy/paste from other stuff I've done. I'll take a look at those examples.
|
||
@SneakyThrows | ||
public Map<String, Object> serialize(String key) { | ||
InputStream input = new FileInputStream(new File("src/test/java/com/saucelabs/saucebindings/options.yml")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I usually use something like https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html with closeable streams to make sure they are properly closed
It is a good practice to document public methods in user-facing libs, not sure how much this principle applies to this framework |
Thanks @mykola-mokhnach ! Yes, this definitely needs the javadocs updated before it can be released. That's just never the fun part. :) |
And looks like there are Snapshot options that I'm not supporting yet — https://docs.saucelabs.com/visual/e2e-testing/commands-options/#arguments-1 |
9aecf9e
to
fcb089c
Compare
Inadvertently closed because I deleted the merged branch that this was PR'd against. Trying to fix now. |
This is a PR against #275, and it'll stay in Draft until that one is merged.
It subsumes #259
I finally really like this API!
Easiest to see what this does from the examples:
Using default:
sauce_bindings/java/main/src/test/java/com/saucelabs/saucebindings/examples/VisualTest.java
Lines 12 to 26 in 11d7745
Updating Options:
sauce_bindings/java/main/src/test/java/com/saucelabs/saucebindings/examples/VisualOptionsTest.java
Lines 14 to 39 in 11d7745
Cool things this does:
I still need to add javadocs because that's important and stuff