Java tutorial
/* * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jitsi.meet.test.base; import org.apache.commons.io.*; import org.jitsi.meet.test.base.stats.*; import org.jitsi.meet.test.pageobjects.*; import org.jitsi.meet.test.util.*; import org.openqa.selenium.*; import org.openqa.selenium.logging.*; import org.openqa.selenium.support.ui.*; import java.io.*; import java.util.*; import java.util.concurrent.*; import java.util.function.*; /** * The participant instance holding the {@link WebDriver}. * @param <T> the driver type. */ public abstract class Participant<T extends WebDriver> { /** * The seconds between calls that will make sure we keep alive the * webdriver session. */ private static final int KEEP_ALIVE_SESSION_INTERVAL = 20; /** * The default config which will be set on the {@link JitsiMeetUrl}, before * conference is joined. */ private final String defaultConfig; /** * The driver. */ protected final T driver; /** * The participant type chrome, firefox etc. */ protected final ParticipantType type; /** * The name of the participant. */ protected final String name; /** * Is hung up. */ private boolean hungUp = true; /** * We store the room name we joined as if someone calls joinConference twice * to be able to detect that there is no need to load anything. */ private String joinedRoomName = null; /** * Executor that is responsible for keeping the participant session alive. */ private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); /** * The execution of the keepalive, if it is null this means * it is not started. */ private ScheduledFuture<?> keepAliveExecution = null; /** * Constructs a Participant. * @param name the name. * @param driver its driver instance. * @param type the type (type of browser). * @param defaultConfig the config which will be set on * the {@link JitsiMeetUrl}, before conference is joined. */ public Participant(String name, T driver, ParticipantType type, String defaultConfig) { this.name = Objects.requireNonNull(name, "name"); this.driver = Objects.requireNonNull(driver, "driver"); this.type = Objects.requireNonNull(type, "type"); this.defaultConfig = defaultConfig; } /** * Does anything that needs to be done just, after the new * {@link Participant} has been created. */ public void initialize() { startKeepAliveExecution(); } /** * Joins a conference. * @param roomName the room name to join. */ public void joinConference(String roomName) { this.joinConference(new JitsiMeetUrl().setRoomName(roomName)); } /** * Joins a conference. * * @param meetURL a {@link JitsiMeetUrl} which represents the full * conference URL which includes server, conference parameters and * the config part. For example: * "https://server.com/conference1?login=true#config.debug=true" */ public void joinConference(JitsiMeetUrl meetURL) { meetURL = Objects.requireNonNull(meetURL, "meetURL"); meetURL.appendConfig(defaultConfig, false /* do not override */); TestUtils.print(getName() + " is opening URL: " + meetURL); // not hungup, so not joining // FIXME this should also check for room parameters, config etc. if (!this.hungUp && this.joinedRoomName != null && this.joinedRoomName.equals(meetURL.getRoomName())) { TestUtils.print("Not joining " + this.name + " in " + meetURL.getRoomName() + ", already joined."); return; } doJoinConference(meetURL); this.joinedRoomName = meetURL.getRoomName(); this.hungUp = false; } /** * Implements the logic of joining a conference. * @param conferenceUrl a {@link JitsiMeetUrl} which represents the full * conference URL which includes server, conference parameters and * the config part. For example: * "https://server.com/conference1?login=true#config.debug=true" */ protected abstract void doJoinConference(JitsiMeetUrl conferenceUrl); /** * Starts the keep-alive execution. */ private void startKeepAliveExecution() { if (this.keepAliveExecution == null) { this.keepAliveExecution = this.executor.scheduleAtFixedRate(driver::getCurrentUrl, KEEP_ALIVE_SESSION_INTERVAL, KEEP_ALIVE_SESSION_INTERVAL, TimeUnit.SECONDS); } } /** * Cancels keep alive execution. */ private synchronized void cancelKeepAlive() { if (this.keepAliveExecution != null) { this.keepAliveExecution.cancel(true); this.keepAliveExecution = null; } } /** * Closes this {@link Participant}'s driver. */ public void close() { TestUtils.print("Closing " + name); cancelKeepAlive(); driver.quit(); // FIXME missing comment on why this is necessary ? (if it really is...) TestUtils.waitMillis(500); } /** * Will call {@link #close()}, but catching any {@link Throwable}s. */ public void closeSafely() { try { this.close(); } catch (Throwable t) { // FIXME: use Logger ? t.printStackTrace(); } } /** * Hangup participant using the UI. */ public void hangUp() { if (this.hungUp) { return; } doHangUp(); TestUtils.print("Hung up in " + name + "."); this.hungUp = true; this.joinedRoomName = null; } /** * Does hang up the participant. */ protected abstract void doHangUp(); /** * The driver instance. * @return driver instance. */ public T getDriver() { return driver; } /** * @return {@link RtpStatistics} for this participant. */ protected abstract RtpStatistics getRtpStatistics(); /** * Returns {@code true} if the ICE connection is currently in the connected * state. * * @return {@code true} for connected or {@code false} for any other state. */ protected abstract boolean isIceConnected(); @Override public String toString() { return String.format("%s[%s]@%s", this.getClass().getSimpleName(), name, String.valueOf(hashCode())); } /** * Returns the participant type. * @return the participant type. */ public ParticipantType getType() { return type; } /** * @return this participant's name. */ public String getName() { return name; } /** * Waits 15 seconds until this participant joins the MUC. */ public void waitToJoinMUC() { waitToJoinMUC(15); } /** * Waits until this participant joins the MUC. * @param timeout the maximum time to wait in seconds. */ public void waitToJoinMUC(int timeout) { waitForCondition(this::isInMuc, timeout, toString() + "#waitToJoinMUC"); } /** * Checks whether this participant is in the MUC. * * @return {@code true} if the specified {@code participant} has joined the * room; otherwise, {@code false} */ public abstract boolean isInMuc(); /** * Waits 15 sec for the given participant to enter the ICE 'connected' * state. */ public void waitForIceConnected() { waitForIceConnected(15); } /** * Waits for a condition. * * @param condition a {@link BooleanSupplier} which return {@code true} when * the condition has been met. * @param timeoutSeconds a timeout in seconds. * @param label a label which will appear in a timeout exception when * the condition is not met withing the time limit. */ public void waitForCondition(final BooleanSupplier condition, int timeoutSeconds, String label) { TestUtils.waitForCondition(driver, timeoutSeconds, new ExpectedCondition<Boolean>() { @Override public Boolean apply(WebDriver webDriver) { return condition.getAsBoolean(); } @Override public String toString() { return label; } }); } /** * Waits for the given participant to enter the ICE 'connected' state. * @param timeoutSeconds timeout in seconds. */ public void waitForIceConnected(int timeoutSeconds) { waitForCondition(this::isIceConnected, timeoutSeconds, toString() + "#isIceConnected"); } /** * Waits until data has been sent and received over the ICE connection * in this participant. */ public void waitForSendReceiveData() { this.waitForSendReceiveData(true, true); } /** * Waits until data has been sent and received over the ICE connection * in this participant. * @param checkSend should we expect and wait for send data. * @param checkReceive should we expect and wait for receive data. */ public void waitForSendReceiveData(boolean checkSend, boolean checkReceive) { waitForCondition(() -> { RtpStatistics rtpStats = getRtpStatistics(); return (!checkSend || rtpStats.getUploadBitrate() > 0) && (!checkReceive || rtpStats.getDownloadBitrate() > 0); }, 20, toString() + "#waitForSendReceiveData"); } /** * Waits for number of remote streams. * @param n number of remote streams to wait for. */ public abstract void waitForRemoteStreams(int n); /** * Sets the display name of the local participant. * @param name the name to set. */ public abstract void setDisplayName(String name); /** * Takes screenshot. * @param outputDir the screenshots output directory. * @param fileName the destination screenshot file name. */ public void takeScreenshot(File outputDir, String fileName) { if (!(driver instanceof TakesScreenshot)) { TestUtils.print("Driver does not support taking screenshots ! FileName:" + fileName); return; } TakesScreenshot takesScreenshot = (TakesScreenshot) driver; File scrFile = takesScreenshot.getScreenshotAs(OutputType.FILE); File dstFile = new File(outputDir, fileName); try { FileUtils.moveFile(scrFile, dstFile); } catch (IOException ioe) { throw new RuntimeException("Failed to move the screenshot file, from: " + scrFile.toString() + " to: " + dstFile.toString(), ioe); } } /** * Saves the html source of the supplied page. * @param outputDir the output directory. * @param fileName the destination html file name. */ public void saveHtmlSource(File outputDir, String fileName) { try (FileOutputStream fOut = FileUtils.openOutputStream(new File(outputDir, fileName))) { fOut.write(driver.getPageSource().replace(">", ">\n").getBytes()); } catch (Exception e) { e.printStackTrace(); } } /** * Obtains JitsiMeet debug logs. * * XXX On web the value is obtained from "APP.conference.getLogs()", but * it's done by executing a script, so no idea at this point when that will * be available on mobile. * * @return a string with logs. */ public abstract String getMeetDebugLog(); /** * Obtains JitsiMeet rtp stats. * * XXX On web the value is obtained from * "APP.conference._room.jvbJingleSession.peerconnection.stats", but * it's done by executing a script, so no idea at this point when that will * be available on mobile. * * @return a string with logs. */ public abstract String getRTPStats(); /** * A list of log entries, which toString() output can be written to * a log file. * * @return a list of log entries. */ public abstract List<Object> getBrowserLogs(); /** * Returns the value for the given <tt>key</tt> from the config.js loaded * for the participant. * * @param key the <tt>String</tt> key from config.js. * @return the value for the given <tt>key</tt> from the config.js loaded * for the participant. */ public abstract Object getConfigValue(String key); /** * Presses a shortcut in this {@link Participant}'s driver. * @param shortcut the {@link Character} which represents the shortcut to * press. */ public abstract void pressShortcut(Character shortcut); /** * @return the endpoint ID of this participant (i.e. the resource part of * the occupant JID in the MUC). */ public abstract String getEndpointId(); /** * @return {@code true} if the ICE connection for P2P is in state * 'connected' and {@code false} otherwise. */ public abstract boolean isP2pConnected(); /** * @return the transport protocol used by the jitsi-videobridge connection * for this {@link Participant}, or {@code null}. */ public abstract String getProtocol(); /** * Checks whether the strophe connection is connected. * @return {@code true} iff the XMPP connection is connected. */ public abstract boolean isXmppConnected(); /** * @return a representation of the filmstrip of this participant. */ public abstract Filmstrip<? extends Participant> getFilmstrip(); }