diff --git a/bigbluebutton-html5/.travis.yml b/bigbluebutton-html5/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..496996c7214158d95f3652a24f8a60cc12b4243f
--- /dev/null
+++ b/bigbluebutton-html5/.travis.yml
@@ -0,0 +1,16 @@
+language: node_js
+
+install:
+  # - git clone git@github.com:browniecab/bigbluebutton-tests.git tests
+  - cd tests/puppeteer
+  - npm install
+
+script:
+  - cd tests/puppeteer
+  - node test-chat.js
+  - node test-draw.js
+  - node test-status.js
+  - node test-switch-slides.js
+  - node test-upload.js
+#   - node tests/puppeteer/test-hotkeys.js
+#   - node tests/puppeteer/test-hotkeys-mic-first.js
\ No newline at end of file
diff --git a/bigbluebutton-html5/Dockerfile.test b/bigbluebutton-html5/Dockerfile.test
new file mode 100644
index 0000000000000000000000000000000000000000..99f79c12e6778e6073dc208de23fd04d19b905e7
--- /dev/null
+++ b/bigbluebutton-html5/Dockerfile.test
@@ -0,0 +1,72 @@
+FROM ubuntu:16.04
+MAINTAINER ffdixon@bigbluebutton.org
+
+ENV DEBIAN_FRONTEND noninteractive
+RUN echo 'Acquire::http::Proxy "http://192.168.0.130:3142 ";'  > /etc/apt/apt.conf.d/01proxy
+RUN apt-get update && apt-get install -y wget software-properties-common
+
+RUN echo "deb http://ubuntu.bigbluebutton.org/xenial-200-dev bigbluebutton-xenial main   " | tee /etc/apt/sources.list.d/bigbluebutton.list
+RUN wget http://ubuntu.bigbluebutton.org/repo/bigbluebutton.asc -O- | apt-key add -
+RUN add-apt-repository ppa:jonathonf/ffmpeg-4 -y
+RUN apt-get update && apt-get -y dist-upgrade
+
+# -- Setup tomcat7 to run under docker
+RUN apt-get install -y \
+  haveged    \
+  net-tools  \
+  supervisor \
+  sudo       \
+  tomcat7
+
+RUN sed -i 's|securerandom.source=file:/dev/random|securerandom.source=file:/dev/urandom|g'  /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/security/java.security
+ADD mod/tomcat7 /etc/init.d/tomcat7
+RUN chmod +x /etc/init.d/tomcat7
+
+RUN apt-get install -y language-pack-en
+RUN update-locale LANG=en_US.UTF-8
+
+# -- Install BigBlueButton
+RUN echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | debconf-set-selections
+RUN apt-get install -y bigbluebutton 
+RUN apt-get install -y bbb-demo 
+
+# -- Install mongodb (for HTML5 client)
+RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6
+RUN echo "deb [ arch=amd64,arm64 ] http://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.4.list
+RUN sudo apt-get update && sudo apt-get install -y mongodb-org curl
+
+# -- Install nodejs (for HTML5 client)
+RUN apt-get install -y apt-transport-https
+RUN curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add -
+RUN echo 'deb http://deb.nodesource.com/node_8.x xenial main' > /etc/apt/sources.list.d/nodesource.list
+RUN echo 'deb-src http://deb.nodesource.com/node_8.x xenial main' >> /etc/apt/sources.list.d/nodesource.list
+RUN apt-get update && apt-get install -y nodejs 
+
+# -- Install HTML5 client
+RUN apt-get install -y bbb-html5
+RUN apt-get install -y coturn vim mlocate
+
+# -- Install Meteor
+RUN curl https://install.meteor.com/ | sh
+ENV METEOR_ALLOW_SUPERUSER true
+
+# -- Install supervisor to run all the BigBlueButton processes (replaces systemd)
+RUN apt-get install -y supervisor
+RUN mkdir -p /var/log/supervisor
+ADD supervisord.conf /etc/supervisor/conf.d/supervisord.conf
+
+# -- Modify FreeSWITCH event_socket.conf.xml to listen to IPV4
+ADD mod/event_socket.conf.xml /opt/freeswitch/etc/freeswitch/autoload_configs
+ADD mod/external.xml          /opt/freeswitch/conf/sip_profiles/external.xml
+
+# -- Install latest HTML5 client from source
+RUN supervisorctl stop bbb-html5
+ADD . /bigbluebutton-html5
+WORKDIR /bigbluebutton-html5
+RUN meteor npm install
+WORKDIR /
+
+# -- Finish startup
+ADD setup.sh /root/setup.sh
+ENTRYPOINT ["/root/setup.sh"]
+CMD []
diff --git a/bigbluebutton-html5/mod/event_socket.conf.xml b/bigbluebutton-html5/mod/event_socket.conf.xml
new file mode 100644
index 0000000000000000000000000000000000000000..05e74d45ebcd09ca276972318fc2343892120485
--- /dev/null
+++ b/bigbluebutton-html5/mod/event_socket.conf.xml
@@ -0,0 +1,11 @@
+<configuration name="event_socket.conf" description="Socket Client">
+  <settings>
+    <param name="nat-map" value="false"/>
+    <param name="listen-ip" value="127.0.0.1"/> 
+    <param name="listen-port" value="8021"/>
+    <param name="password" value="ClueCon"/>
+    <!--<param name="apply-inbound-acl" value="loopback.auto"/>-->
+    <!--<param name="stop-on-bind-error" value="true"/>-->
+  </settings>
+</configuration>
+
diff --git a/bigbluebutton-html5/mod/external.xml b/bigbluebutton-html5/mod/external.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f4b5c9463fb2026c3aecbcb87bf65f90bc42faab
--- /dev/null
+++ b/bigbluebutton-html5/mod/external.xml
@@ -0,0 +1,113 @@
+<profile name="external">
+  <!-- http://wiki.freeswitch.org/wiki/Sofia_Configuration_Files -->
+  <!-- This profile is only for outbound registrations to providers -->
+  <gateways>
+    <X-PRE-PROCESS cmd="include" data="external/*.xml"/>
+  </gateways>
+
+  <aliases>
+    <!--
+        <alias name="outbound"/>
+        <alias name="nat"/>
+    -->
+  </aliases>
+
+  <domains>
+    <domain name="all" alias="false" parse="true"/>
+  </domains>
+
+  <settings>
+    <param name="debug" value="0"/>
+    <!-- If you want FreeSWITCH to shutdown if this profile fails to load, uncomment the next line. -->
+    <!-- <param name="shutdown-on-fail" value="true"/> -->
+    <param name="sip-trace" value="no"/>
+    <param name="sip-capture" value="no"/>
+    <param name="rfc2833-pt" value="101"/>
+    <!-- RFC 5626 : Send reg-id and sip.instance -->
+    <!--<param name="enable-rfc-5626" value="true"/> -->
+    <param name="sip-port" value="$${external_sip_port}"/>
+    <param name="dialplan" value="XML"/>
+    <param name="context" value="public"/>
+    <param name="dtmf-duration" value="2000"/>
+    <param name="inbound-codec-prefs" value="$${global_codec_prefs}"/>
+    <param name="outbound-codec-prefs" value="$${outbound_codec_prefs}"/>
+    <param name="hold-music" value="$${hold_music}"/>
+    <param name="rtp-timer-name" value="soft"/>
+    <!--<param name="enable-100rel" value="true"/>-->
+    <!--<param name="disable-srv503" value="true"/>-->
+    <!-- This could be set to "passive" -->
+    <param name="local-network-acl" value="localnet.auto"/>
+    <param name="manage-presence" value="false"/>
+
+    <!-- used to share presence info across sofia profiles
+         manage-presence needs to be set to passive on this profile
+         if you want it to behave as if it were the internal profile
+         for presence.
+    -->
+    <!-- Name of the db to use for this profile -->
+    <!--<param name="dbname" value="share_presence"/>-->
+    <!--<param name="presence-hosts" value="$${domain}"/>-->
+    <!--<param name="force-register-domain" value="$${domain}"/>-->
+    <!--all inbound reg will stored in the db using this domain -->
+    <!--<param name="force-register-db-domain" value="$${domain}"/>-->
+    <!-- ************************************************* -->
+
+    <!--<param name="aggressive-nat-detection" value="true"/>-->
+    <param name="inbound-codec-negotiation" value="generous"/>
+    <param name="nonce-ttl" value="60"/>
+    <param name="auth-calls" value="false"/>
+    <param name="inbound-late-negotiation" value="true"/>
+    <param name="inbound-zrtp-passthru" value="true"/> <!-- (also enables late negotiation) -->
+    <!--
+        DO NOT USE HOSTNAMES, ONLY IP ADDRESSES IN THESE SETTINGS!
+    <param name="rtp-ip" value="$${local_ip_v4}"/>
+    <param name="sip-ip" value="$${local_ip_v4}"/>
+    <param name="ext-rtp-ip" value="auto-nat"/>
+    <param name="ext-sip-ip" value="auto-nat"/>
+    -->
+
+    <param name="rtp-ip" value="$${local_ip_v4}"/>
+    <param name="sip-ip" value="$${local_ip_v4}"/>
+    <param name="ext-rtp-ip" value="$${local_ip_v4}"/>
+    <param name="ext-sip-ip" value="$${local_ip_v4}"/>
+
+    <param name="rtp-timeout-sec" value="300"/>
+    <param name="rtp-hold-timeout-sec" value="1800"/>
+    <!--<param name="enable-3pcc" value="true"/>-->
+
+    <!-- TLS: disabled by default, set to "true" to enable -->
+    <param name="tls" value="$${external_ssl_enable}"/>
+    <!-- Set to true to not bind on the normal sip-port but only on the TLS port -->
+    <param name="tls-only" value="false"/>
+    <!-- additional bind parameters for TLS -->
+    <param name="tls-bind-params" value="transport=tls"/>
+    <!-- Port to listen on for TLS requests. (5081 will be used if unspecified) -->
+    <param name="tls-sip-port" value="$${external_tls_port}"/>
+    <!-- Location of the agent.pem and cafile.pem ssl certificates (needed for TLS server) -->
+    <!--<param name="tls-cert-dir" value=""/>-->
+    <!-- Optionally set the passphrase password used by openSSL to encrypt/decrypt TLS private key files -->
+    <param name="tls-passphrase" value=""/>
+    <!-- Verify the date on TLS certificates -->
+    <param name="tls-verify-date" value="true"/>
+    <!-- TLS verify policy, when registering/inviting gateways with other servers (outbound) or handling inbound registration/invite requests how should we verify their certificate -->
+    <!-- set to 'in' to only verify incoming connections, 'out' to only verify outgoing connections, 'all' to verify all connections, also 'in_subjects', 'out_subjects' and 'all_subjects' for subject validation. Multiple policies can be split with a '|' pipe -->
+    <param name="tls-verify-policy" value="none"/>
+    <!-- Certificate max verify depth to use for validating peer TLS certificates when the verify policy is not none -->
+    <param name="tls-verify-depth" value="2"/>
+    <!-- If the tls-verify-policy is set to subjects_all or subjects_in this sets which subjects are allowed, multiple subjects can be split with a '|' pipe -->
+    <param name="tls-verify-in-subjects" value=""/>
+    <!-- TLS version ("sslv23" (default), "tlsv1"). NOTE: Phones may not work with TLSv1 -->
+    <param name="tls-version" value="$${sip_tls_version}"/>
+    <param name="ws-binding"  value=":5066"/>
+    <param name="apply-candidate-acl" value="webrtc-turn"/>
+
+    <!-- enable rtcp on every channel also can be done per leg basis with rtcp_audio_interval_msec variable set to passthru to pass it across a call-->
+    <param name="rtcp-audio-interval-msec" value="5000"/>
+    <param name="rtcp-video-interval-msec" value="5000"/>
+
+    <!-- Cut down in the join time -->
+    <param name="dtmf-type" value="info"/>
+    <param name="liberal-dtmf" value="true"/>
+  </settings>
+</profile>
+
diff --git a/bigbluebutton-html5/mod/tomcat7 b/bigbluebutton-html5/mod/tomcat7
new file mode 100755
index 0000000000000000000000000000000000000000..1e4a45a6d33a6aeb5ad152ffaf00ffe3d8cbd5d8
--- /dev/null
+++ b/bigbluebutton-html5/mod/tomcat7
@@ -0,0 +1,287 @@
+#!/bin/sh
+#
+# /etc/init.d/tomcat7 -- startup script for the Tomcat 6 servlet engine
+#
+# Written by Miquel van Smoorenburg <miquels@cistron.nl>.
+# Modified for Debian GNU/Linux	by Ian Murdock <imurdock@gnu.ai.mit.edu>.
+# Modified for Tomcat by Stefan Gybas <sgybas@debian.org>.
+# Modified for Tomcat6 by Thierry Carrez <thierry.carrez@ubuntu.com>.
+# Modified for Tomcat7 by Ernesto Hernandez-Novich <emhn@itverx.com.ve>.
+# Additional improvements by Jason Brittain <jason.brittain@mulesoft.com>.
+#
+### BEGIN INIT INFO
+# Provides:          tomcat7
+# Required-Start:    $local_fs $remote_fs $network
+# Required-Stop:     $local_fs $remote_fs $network
+# Should-Start:      $named
+# Should-Stop:       $named
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: Start Tomcat.
+# Description:       Start the Tomcat servlet engine.
+### END INIT INFO
+
+set -e
+
+PATH=/bin:/usr/bin:/sbin:/usr/sbin
+NAME=tomcat7
+DESC="Tomcat servlet engine"
+DEFAULT=/etc/default/$NAME
+JVM_TMP=/tmp/tomcat7-$NAME-tmp
+
+if [ `id -u` -ne 0 ]; then
+	echo "You need root privileges to run this script"
+	exit 1
+fi
+ 
+# Make sure tomcat is started with system locale
+if [ -r /etc/default/locale ]; then
+	. /etc/default/locale
+	export LANG
+fi
+
+. /lib/lsb/init-functions
+
+if [ -r /etc/default/rcS ]; then
+	. /etc/default/rcS
+fi
+
+
+# The following variables can be overwritten in $DEFAULT
+
+# Run Tomcat 7 as this user ID and group ID
+TOMCAT7_USER=tomcat7
+TOMCAT7_GROUP=tomcat7
+
+# this is a work-around until there is a suitable runtime replacement 
+# for dpkg-architecture for arch:all packages
+# this function sets the variable JDK_DIRS
+find_jdks()
+{
+    for java_version in 9 8 7 6
+    do
+        for jvmdir in /usr/lib/jvm/java-${java_version}-openjdk-* \
+                      /usr/lib/jvm/jdk-${java_version}-oracle-* \
+                      /usr/lib/jvm/jre-${java_version}-oracle-*
+        do
+            if [ -d "${jvmdir}" -a "${jvmdir}" != "/usr/lib/jvm/java-${java_version}-openjdk-common" ]
+            then
+                JDK_DIRS="${JDK_DIRS} ${jvmdir}"
+            fi
+        done
+    done
+
+    # Add older non multi arch installations
+    JDK_DIRS="${JDK_DIRS} /usr/lib/jvm/java-6-openjdk /usr/lib/jvm/java-6-sun /usr/lib/jvm/java-7-oracle"
+}
+
+# The first existing directory is used for JAVA_HOME (if JAVA_HOME is not
+# defined in $DEFAULT)
+JDK_DIRS="/usr/lib/jvm/default-java"
+find_jdks
+
+# Look for the right JVM to use
+for jdir in $JDK_DIRS; do
+    if [ -r "$jdir/bin/java" -a -z "${JAVA_HOME}" ]; then
+	JAVA_HOME="$jdir"
+    fi
+done
+export JAVA_HOME
+
+# Directory where the Tomcat 6 binary distribution resides
+CATALINA_HOME=/usr/share/$NAME
+
+# Directory for per-instance configuration files and webapps
+CATALINA_BASE=/var/lib/$NAME
+
+# Use the Java security manager? (yes/no)
+TOMCAT7_SECURITY=no
+
+# Default Java options
+# Set java.awt.headless=true if JAVA_OPTS is not set so the
+# Xalan XSL transformer can work without X11 display on JDK 1.4+
+# It also looks like the default heap size of 64M is not enough for most cases
+# so the maximum heap size is set to 128M
+if [ -z "$JAVA_OPTS" ]; then
+	JAVA_OPTS="-Djava.awt.headless=true -Xmx128M"
+fi
+
+# End of variables that can be overwritten in $DEFAULT
+
+# overwrite settings from default file
+if [ -f "$DEFAULT" ]; then
+	. "$DEFAULT"
+fi
+
+if [ ! -f "$CATALINA_HOME/bin/bootstrap.jar" ]; then
+	log_failure_msg "$NAME is not installed"
+	exit 1
+fi
+
+POLICY_CACHE="$CATALINA_BASE/work/catalina.policy"
+
+if [ -z "$CATALINA_TMPDIR" ]; then
+	CATALINA_TMPDIR="$JVM_TMP"
+fi
+
+# Set the JSP compiler if set in the tomcat7.default file
+if [ -n "$JSP_COMPILER" ]; then
+	JAVA_OPTS="$JAVA_OPTS -Dbuild.compiler=\"$JSP_COMPILER\""
+fi
+JAVA_OPTS="$JAVA_OPTS -Djava.security.egd=file:/dev/./urandom"
+
+SECURITY=""
+if [ "$TOMCAT7_SECURITY" = "yes" ]; then
+	SECURITY="-security"
+fi
+
+# Define other required variables
+CATALINA_PID="/var/run/$NAME.pid"
+CATALINA_SH="$CATALINA_HOME/bin/catalina.sh"
+
+# Look for Java Secure Sockets Extension (JSSE) JARs
+if [ -z "${JSSE_HOME}" -a -r "${JAVA_HOME}/jre/lib/jsse.jar" ]; then
+    JSSE_HOME="${JAVA_HOME}/jre/"
+fi
+
+catalina_sh() {
+	# Escape any double quotes in the value of JAVA_OPTS
+	JAVA_OPTS="$(echo $JAVA_OPTS | sed 's/\"/\\\"/g')"
+
+	AUTHBIND_COMMAND=""
+	if [ "$AUTHBIND" = "yes" -a "$1" = "start" ]; then
+		AUTHBIND_COMMAND="/usr/bin/authbind --deep /bin/bash -c "
+	fi
+
+	# Define the command to run Tomcat's catalina.sh as a daemon
+	# set -a tells sh to export assigned variables to spawned shells.
+	TOMCAT_SH="set -a; JAVA_HOME=\"$JAVA_HOME\"; source \"$DEFAULT\"; \
+		CATALINA_HOME=\"$CATALINA_HOME\"; \
+		CATALINA_BASE=\"$CATALINA_BASE\"; \
+		JAVA_OPTS=\"$JAVA_OPTS\"; \
+		CATALINA_PID=\"$CATALINA_PID\"; \
+		CATALINA_TMPDIR=\"$CATALINA_TMPDIR\"; \
+		LANG=\"$LANG\"; JSSE_HOME=\"$JSSE_HOME\"; \
+		cd \"$CATALINA_BASE\"; \
+		\"$CATALINA_SH\" $@"
+
+	if [ "$AUTHBIND" = "yes" -a "$1" = "start" ]; then
+		TOMCAT_SH="'$TOMCAT_SH'"
+	fi
+
+	# Run the catalina.sh script as a daemon
+	set +e
+	touch "$CATALINA_PID" "$CATALINA_BASE"/logs/catalina.out
+	chown $TOMCAT7_USER "$CATALINA_PID" "$CATALINA_BASE"/logs/catalina.out
+	start-stop-daemon --start -b -u "$TOMCAT7_USER" -g "$TOMCAT7_GROUP" \
+		-c "$TOMCAT7_USER" -d "$CATALINA_TMPDIR" -p "$CATALINA_PID" \
+		-x /bin/bash -- -c "$AUTHBIND_COMMAND $TOMCAT_SH"
+	status="$?"
+	set +a -e
+	return $status
+}
+
+case "$1" in
+  start)
+	if [ -z "$JAVA_HOME" ]; then
+		log_failure_msg "no JDK or JRE found - please set JAVA_HOME"
+		exit 1
+	fi
+
+	if [ ! -d "$CATALINA_BASE/conf" ]; then
+		log_failure_msg "invalid CATALINA_BASE: $CATALINA_BASE"
+		exit 1
+	fi
+
+	log_daemon_msg "Starting $DESC" "$NAME"
+	if start-stop-daemon --test --start --pidfile "$CATALINA_PID" \
+		--user $TOMCAT7_USER --exec "$JAVA_HOME/bin/java" \
+		>/dev/null; then
+
+		# Regenerate POLICY_CACHE file
+		umask 022
+		echo "// AUTO-GENERATED FILE from /etc/tomcat7/policy.d/" \
+			> "$POLICY_CACHE"
+		echo ""  >> "$POLICY_CACHE"
+		cat $CATALINA_BASE/conf/policy.d/*.policy \
+			>> "$POLICY_CACHE"
+
+		# Remove / recreate JVM_TMP directory
+		rm -rf "$JVM_TMP"
+		mkdir -p "$JVM_TMP" || {
+			log_failure_msg "could not create JVM temporary directory"
+			exit 1
+		}
+		chown $TOMCAT7_USER "$JVM_TMP"
+
+		catalina_sh start $SECURITY
+		sleep 5
+			log_end_msg 0
+	else
+	        log_progress_msg "(already running)"
+		log_end_msg 0
+	fi
+	;;
+  stop)
+	log_daemon_msg "Stopping $DESC" "$NAME"
+
+	set +e
+	if [ -f "$CATALINA_PID" ]; then 
+		start-stop-daemon --stop --pidfile "$CATALINA_PID" \
+			--user "$TOMCAT7_USER" \
+			--retry=TERM/20/KILL/5 >/dev/null
+		if [ $? -eq 1 ]; then
+			log_progress_msg "$DESC is not running but pid file exists, cleaning up"
+		elif [ $? -eq 3 ]; then
+			PID="`cat $CATALINA_PID`"
+			log_failure_msg "Failed to stop $NAME (pid $PID)"
+			exit 1
+		fi
+		rm -f "$CATALINA_PID"
+		rm -rf "$JVM_TMP"
+	else
+		log_progress_msg "(not running)"
+	fi
+	log_end_msg 0
+	set -e
+	;;
+   status)
+	set +e
+	start-stop-daemon --test --start --pidfile "$CATALINA_PID" \
+		--user $TOMCAT7_USER --exec "$JAVA_HOME/bin/java" \
+		>/dev/null 2>&1
+	if [ "$?" = "0" ]; then
+
+		if [ -f "$CATALINA_PID" ]; then
+		    log_success_msg "$DESC is not running, but pid file exists."
+			exit 1
+		else
+		    log_success_msg "$DESC is not running."
+			exit 3
+		fi
+	else
+		log_success_msg "$DESC is running with pid `cat $CATALINA_PID`"
+	fi
+	set -e
+        ;;
+  restart|force-reload)
+	if [ -f "$CATALINA_PID" ]; then
+		$0 stop
+		sleep 1
+	fi
+	$0 start
+	;;
+  try-restart)
+        if start-stop-daemon --test --start --pidfile "$CATALINA_PID" \
+		--user $TOMCAT7_USER --exec "$JAVA_HOME/bin/java" \
+		>/dev/null; then
+		$0 start
+	fi
+        ;;
+  *)
+	log_success_msg "Usage: $0 {start|stop|restart|try-restart|force-reload|status}"
+	exit 1
+	;;
+esac
+
+exit 0
diff --git a/bigbluebutton-html5/restart.sh b/bigbluebutton-html5/restart.sh
new file mode 100755
index 0000000000000000000000000000000000000000..b92334efd78042b2c32bb3b5e7074dd1aa11d2f0
--- /dev/null
+++ b/bigbluebutton-html5/restart.sh
@@ -0,0 +1,19 @@
+#!/bin/bash -x
+
+ID=`docker ps --format "{{.ID}}" --filter ancestor=bigbluebutton/b2`
+
+if [ "$ID" != "" ]; then
+  docker stop $ID
+fi
+docker build -t bigbluebutton/b2 .
+
+docker run -p 80:80/tcp -p 443:443/tcp -p 1935:1935/tcp -p 5066:5066/tcp -p 3478:3478/udp -p 3478:3478 -v /home/firstuser/dev/bigbluebutton/bigbluebutton-html5:/root/bigbluebutton-html5 --cap-add=NET_ADMIN bigbluebutton/b2 -h 192.168.0.130 > /dev/null
+
+cat << HERE
+
+   docker exec -it `docker ps --format "{{.ID}}" --filter ancestor=bigbluebutton/b2` supervisorctl status
+   docker exec -it `docker ps --format "{{.ID}}" --filter ancestor=bigbluebutton/b2` /bin/bash
+
+HERE
+
+ID=`docker ps --format "{{.ID}}" --filter ancestor=bigbluebutton/b2`
diff --git a/bigbluebutton-html5/setup.sh b/bigbluebutton-html5/setup.sh
new file mode 100755
index 0000000000000000000000000000000000000000..9ab64272478cfc348bf557edb43fd6d8642584b6
--- /dev/null
+++ b/bigbluebutton-html5/setup.sh
@@ -0,0 +1,230 @@
+#!/bin/bash
+
+#
+# BlueButton open source conferencing system - http://www.bigbluebutton.org/
+#
+# Copyright (c) 2018 BigBlueButton Inc.
+#
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
+#
+set -x
+
+change_var_value () {
+        sed -i "s<^[[:blank:]#]*\(${2}\).*<\1=${3}<" $1
+}
+
+# docker run -p 80:80/tcp -p 443:443/tcp -p 1935:1935 -p 5066:5066 -p 3478:3478 -p 3478:3478/udp b2 -h 192.168.0.130
+
+while getopts "eh:" opt; do
+  case $opt in
+    e)
+      env
+      exit
+      ;;
+    h)
+      HOST=$OPTARG
+      ;;
+    e)
+      SECRET=$OPTARG
+      ;;
+    :)
+      echo "Missing option argument for -$OPTARG" >&2;
+      exit 1
+      ;;
+    \?)
+      echo "Invalid option: -$OPTARG" >&2
+      cat<<HERE
+Docker startup script for BigBlueButton.
+
+  -h   Hostname for BigBlueButton server
+  -s   Shared secret
+
+HERE
+      exit 1
+      ;;
+    :)
+      echo "Option -$OPTARG requires an argument." >&2
+      exit 1
+      ;;
+  esac
+done
+
+apt-get install -y bbb-demo && /etc/init.d/tomcat7 start
+while [ ! -f /var/lib/tomcat7/webapps/demo/bbb_api_conf.jsp ]; do sleep 1; done
+sudo /etc/init.d/tomcat7 stop
+
+
+# Setup the BigBlueButton configuration files
+#
+PROTOCOL_HTTP=http
+PROTOCOL_RTMP=rtmp
+
+IP=$(echo "$(LANG=c ifconfig  | awk -v RS="" '{gsub (/\n[ ]*inet /," ")}1' | grep ^et.* | grep addr: | head -n1 | sed 's/.*addr://g' | sed 's/ .*//g')$(LANG=c ifconfig  | awk -v RS="" '{gsub (/\n[ ]*inet /," ")}1' | grep ^en.* | grep addr: | head -n1 | sed 's/.*addr://g' | sed 's/ .*//g')" | head -n1)
+
+xmlstarlet edit --inplace --update '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "external_rtp_ip=")]/@data' --value "stun:coturn" /opt/freeswitch/conf/vars.xml
+xmlstarlet edit --inplace --update '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "external_sip_ip=")]/@data' --value "stun:coturn" /opt/freeswitch/conf/vars.xml
+xmlstarlet edit --inplace --update '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "local_ip_v4=")]/@data' --value "${IP}" /opt/freeswitch/conf/vars.xml
+
+sed -i "s/proxy_pass .*/proxy_pass $PROTOCOL_HTTP:\/\/$IP:5066;/g" /etc/bigbluebutton/nginx/sip.nginx
+
+sed -i "s/http[s]*:\/\/\([^\"\/]*\)\([\"\/]\)/$PROTOCOL_HTTP:\/\/$HOST\2/g"  /var/www/bigbluebutton/client/conf/config.xml
+sed -i "s/rtmp[s]*:\/\/\([^\"\/]*\)\([\"\/]\)/$PROTOCOL_RTMP:\/\/$HOST\2/g" /var/www/bigbluebutton/client/conf/config.xml
+
+sed -i "s/server_name  .*/server_name  $HOST;/g" /etc/nginx/sites-available/bigbluebutton
+
+sed -i "s/bigbluebutton.web.serverURL=http[s]*:\/\/.*/bigbluebutton.web.serverURL=$PROTOCOL_HTTP:\/\/$HOST/g" \
+  /var/lib/tomcat7/webapps/bigbluebutton/WEB-INF/classes/bigbluebutton.properties
+
+# Update Java screen share configuration
+change_var_value /usr/share/red5/webapps/screenshare/WEB-INF/screenshare.properties streamBaseUrl rtmp://$HOST/screenshare
+change_var_value /usr/share/red5/webapps/screenshare/WEB-INF/screenshare.properties jnlpUrl $PROTOCOL_HTTP://$HOST/screenshare
+change_var_value /usr/share/red5/webapps/screenshare/WEB-INF/screenshare.properties jnlpFile $PROTOCOL_HTTP://$HOST/screenshare/screenshare.jnlp
+
+change_var_value /usr/share/red5/webapps/sip/WEB-INF/bigbluebutton-sip.properties bbb.sip.app.ip $IP
+change_var_value /usr/share/red5/webapps/sip/WEB-INF/bigbluebutton-sip.properties freeswitch.ip $IP
+
+sed -i  "s/bbbWebAPI[ ]*=[ ]*\"[^\"]*\"/bbbWebAPI=\"${PROTOCOL_HTTP}:\/\/$HOST\/bigbluebutton\/api\"/g" \
+  /usr/share/bbb-apps-akka/conf/application.conf
+sed -i "s/bbbWebHost[ ]*=[ ]*\"[^\"]*\"/bbbWebHost=\"$HOST\"/g" \
+  /usr/share/bbb-apps-akka/conf/application.conf
+sed -i "s/deskshareip[ ]*=[ ]*\"[^\"]*\"/deskshareip=\"$HOST\"/g" \
+  /usr/share/bbb-apps-akka/conf/application.conf
+sed -i  "s/defaultPresentationURL[ ]*=[ ]*\"[^\"]*\"/defaultPresentationURL=\"${PROTOCOL_HTTP}:\/\/$HOST\/default.pdf\"/g" \
+  /usr/share/bbb-apps-akka/conf/application.conf
+
+cat > /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini  << HERE
+; Only IP address are supported, not domain names for addresses
+; You have to find a valid stun server. You can check if it works
+; using this tool:
+;   http://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/
+;stunServerAddress=64.233.177.127
+;stunServerPort=19302
+
+turnURL=kurento:kurento@${HOST}:3478
+
+;pemCertificate is deprecated. Please use pemCertificateRSA instead
+;pemCertificate=<path>
+;pemCertificateRSA=<path>
+;pemCertificateECDSA=<path>
+HERE
+
+TURN_SECRET=`openssl rand -hex 16`
+
+# Configure coturn to handle incoming UDP connections
+cat > /etc/turnserver.conf << HERE
+denied-peer-ip=0.0.0.0-255.255.255.255
+allowed-peer-ip=$IP
+fingerprint
+lt-cred-mech
+use-auth-secret
+static-auth-secret=$TURN_SECRET
+user=user:password
+log-file=/var/log/turn.log
+HERE
+
+# Setup tomcat7 to share the TURN server information with clients (with matching secret)
+cat > /var/lib/tomcat7/webapps/bigbluebutton/WEB-INF/spring/turn-stun-servers.xml << HERE
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" 
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
+   <bean id="turn0" class="org.bigbluebutton.web.services.turn.TurnServer">
+      <constructor-arg index="0" value="$TURN_SECRET" />
+      <constructor-arg index="1" value="turn:$HOST:3478" />
+      <constructor-arg index="2" value="86400" />
+   </bean>
+   <bean id="turn1" class="org.bigbluebutton.web.services.turn.TurnServer">
+      <constructor-arg index="0" value="$TURN_SECRET" />
+      <constructor-arg index="1" value="turn:$HOST:3478?transport=tcp" />
+      <constructor-arg index="2" value="86400" />
+   </bean>
+   <bean id="stunTurnService" class="org.bigbluebutton.web.services.turn.StunTurnService">
+      <property name="stunServers">
+         <set />
+      </property>
+      <property name="turnServers">
+         <set>
+            <ref bean="turn0" />
+            <ref bean="turn1" />
+         </set>
+      </property>
+      <property name="remoteIceCandidates">
+         <set />
+      </property>
+   </bean>
+</beans>
+HERE
+
+cat > /opt/freeswitch/conf/autoload_configs/acl.conf.xml << HERE
+<configuration name="acl.conf" description="Network Lists">
+  <network-lists>
+    <list name="domains" default="allow">
+      <!-- domain= is special it scans the domain from the directory to build the ACL -->
+      <node type="allow" domain="\$\${domain}"/>
+      <!-- use cidr= if you wish to allow ip ranges to this domains acl. -->
+      <!-- <node type="allow" cidr="192.168.0.0/24"/> -->
+    </list>
+
+    <list name="webrtc-turn" default="deny">
+      <node type="allow" cidr="$IP/32"/>
+    </list>
+
+  </network-lists>
+</configuration>
+HERE
+
+
+# Ensure bbb-apps-akka has the latest shared secret from bbb-web
+SECRET=$(cat /var/lib/tomcat7/webapps/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | grep securitySalt | cut -d= -f2);
+sed -i "s/sharedSecret[ ]*=[ ]*\"[^\"]*\"/sharedSecret=\"$SECRET\"/g" \
+  /usr/share/bbb-apps-akka/conf/application.conf
+
+sed -i "s/BigBlueButtonURL = \"http[s]*:\/\/\([^\"\/]*\)\([\"\/]\)/BigBlueButtonURL = \"$PROTOCOL_HTTP:\/\/$HOST\2/g" \
+  /var/lib/tomcat7/webapps/demo/bbb_api_conf.jsp
+
+sed -i "s/playback_host: .*/playback_host: $HOST/g" /usr/local/bigbluebutton/core/scripts/bigbluebutton.yml
+
+sed -i 's/daemonize no/daemonize yes/g' /etc/redis/redis.conf
+
+sed -i "s|\"wsUrl.*|\"wsUrl\": \"ws://$HOST/bbb-webrtc-sfu\",|g" \
+  /usr/share/meteor/bundle/programs/server/assets/app/config/settings-production.json
+
+rm /usr/share/red5/log/sip.log
+
+# Add a sleep to each recording process so we can restart with supervisord
+# (This works around the limitation that supervisord can't restart after intervals)
+sed -i 's/BigBlueButton.logger.debug("rap-archive-worker done")/sleep 20; BigBlueButton.logger.debug("rap-archive-worker done")/g' /usr/local/bigbluebutton/core/scripts/rap-archive-worker.rb
+sed -i 's/BigBlueButton.logger.debug("rap-process-worker done")/sleep 20; BigBlueButton.logger.debug("rap-process-worker done")/g' /usr/local/bigbluebutton/core/scripts/rap-process-worker.rb
+sed -i 's/BigBlueButton.logger.debug("rap-sanity-worker done")/sleep 20 ; BigBlueButton.logger.debug("rap-sanity-worker done")/g'  /usr/local/bigbluebutton/core/scripts/rap-sanity-worker.rb
+sed -i 's/BigBlueButton.logger.debug("rap-publish-worker done")/sleep 20; BigBlueButton.logger.debug("rap-publish-worker done")/g' /usr/local/bigbluebutton/core/scripts/rap-publish-worker.rb
+
+# Start BigBlueButton!
+#
+
+export NODE_ENV=production
+
+export DAEMON_LOG=/var/log/kurento-media-server
+export GST_DEBUG="3,Kurento*:4,kms*:4"
+export KURENTO_LOGS_PATH=$DAEMON_LOG
+
+cat << HERE
+
+BigBlueButton is now starting up at this address
+
+  http://$HOST
+
+HERE
+
+updatedb
+exec /usr/bin/supervisord > /var/log/supervisord.log
+
diff --git a/bigbluebutton-html5/supervisord.conf b/bigbluebutton-html5/supervisord.conf
new file mode 100644
index 0000000000000000000000000000000000000000..953a8dda08dad7e644ef0c58cde36ca76d2de050
--- /dev/null
+++ b/bigbluebutton-html5/supervisord.conf
@@ -0,0 +1,112 @@
+[supervisord]
+nodaemon=true
+
+[supervisorctl]
+serverurl=unix:///var/run/supervisor.sock
+
+[program:redis-server]
+startsecs = 0
+autorestart = false
+#user=redis
+command=/usr/bin/redis-server /etc/redis/redis.conf
+stdout_logfile=/var/log/redis/stdout.log
+stderr_logfile=/var/log/redis/stderr.log
+
+[program:nginx]
+startsecs = 0
+autorestart = false
+command=/usr/sbin/nginx -g "daemon off;"
+
+[program:freeswitch]
+startsecs = 0
+autorestart = false
+user=freeswitch
+group=daemon
+directory=/opt/freeswitch
+command=/opt/freeswitch/bin/freeswitch -nc -nf -core -nonat
+
+[program:bbb-apps-akka]
+startsecs = 0
+autorestart = false
+user=bigbluebutton
+directory=/usr/share/bbb-apps-akka
+command=/usr/share/bbb-apps-akka/bin/bbb-apps-akka
+
+[program:bbb-fsesl-akka]
+startsecs = 0
+autorestart = false
+user=bigbluebutton
+directory=/usr/share/bbb-fsesl-akka
+command=/usr/share/bbb-fsesl-akka/bin/bbb-fsesl-akka
+
+[program:red5]
+startsecs = 0
+autorestart = false
+user=red5
+directory=/usr/share/red5
+command=/usr/share/red5/red5.sh
+
+[program:rap-archive-worker]
+command=/usr/local/bigbluebutton/core/scripts/rap-archive-worker.rb
+directory=/usr/local/bigbluebutton/core/scripts
+user=tomcat7
+autorestart=true
+
+[program:rap-process-worker]
+command=/usr/local/bigbluebutton/core/scripts/rap-process-worker.rb
+directory=/usr/local/bigbluebutton/core/scripts
+user=tomcat7
+autorestart=true
+
+[program:rap-sanity-worker]
+command=/usr/local/bigbluebutton/core/scripts/rap-sanity-worker.rb
+directory=/usr/local/bigbluebutton/core/scripts
+user=tomcat7
+autorestart=true
+
+[program:rap-publish-worker]
+command=/usr/local/bigbluebutton/core/scripts/rap-publish-worker.rb 
+directory=/usr/local/bigbluebutton/core/scripts
+user=tomcat7
+autorestart=true
+
+[program:mongod]
+command=/usr/bin/mongod --quiet --config /etc/mongod.conf
+stdout_logfile=/var/log/supervisor/%(program_name)s.log
+stderr_logfile=/var/log/supervisor/%(program_name)s.log
+user=mongodb
+autorestart=true
+
+[program:bbb-webrtc-sfu]
+command=/usr/bin/node server.js
+directory=/usr/local/bigbluebutton/bbb-webrtc-sfu
+user=bigbluebutton
+group=bigbluebutton
+autorestart=true
+
+[program:kurento-media-server]
+command=/usr/bin/kurento-media-server
+directory=/usr/share/meteor/bundle
+user=kurento
+group=kurento
+autorestart=true
+
+[program:bbb-html5]
+command=npm start
+directory=/bigbluebutton-html5
+#user=meteor
+#group=meteor
+autorestart=true
+
+[program:tomcat7]
+startsecs = 0
+autorestart = false
+user=tomcat7
+command=/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/bin/java -Djava.util.logging.config.file=/var/lib/tomcat7/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djava.awt.headless=true -Xmx128m -XX:+UseConcMarkSweepGC -Xms256m -Xmx256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/bigbluebutton/diagnostics -Djava.endorsed.dirs=/usr/share/tomcat7/endorsed -classpath /usr/share/tomcat7/bin/bootstrap.jar:/usr/share/tomcat7/bin/tomcat-juli.jar -Dcatalina.base=/var/lib/tomcat7 -Dcatalina.home=/usr/share/tomcat7 -Djava.io.tmpdir=/tmp/tomcat7-tomcat7-tmp org.apache.catalina.startup.Bootstrap start
+
+[program:coturn]
+startsecs = 0
+autorestart = false
+user=turnserver
+command=/usr/bin/turnserver -c /etc/turnserver.conf -u kurento:kurento
+
diff --git a/bigbluebutton-html5/test-html5.sh b/bigbluebutton-html5/test-html5.sh
new file mode 100755
index 0000000000000000000000000000000000000000..0ed4a0acd9954996e0ff0613ea10e5281d4f6649
--- /dev/null
+++ b/bigbluebutton-html5/test-html5.sh
@@ -0,0 +1,26 @@
+# Change to HTML5 directory
+cd $(dirname $0)
+echo "Working directory: $PWD"
+
+# Build and run Docker image
+docker build -t b2 .
+docker=$(docker run -d -p 80:80/tcp -p 443:443/tcp -p 1935:1935 -p 5066:5066 -p 3478:3478 -p 3478:3478/udp b2 -h 10.130.218.149)
+echo $docker
+
+# Check if HTML5 client is ready 
+cd tests/puppeteer
+node test-html5-check.js
+status=$?
+echo $status
+
+# Run tests
+if [ $status -eq 0 ]; then
+  node test-chat.js
+  node test-draw.js
+  node test-upload.js
+  node test-switch-slides.js
+  node test-status.js
+fi
+
+# Stop Docker container
+docker stop $docker
diff --git a/bigbluebutton-html5/tests/puppeteer/.env_template b/bigbluebutton-html5/tests/puppeteer/.env_template
new file mode 100644
index 0000000000000000000000000000000000000000..d8e887232b5e5c27cb92ec9c33f99f39e7c7b5af
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/.env_template
@@ -0,0 +1,2 @@
+BBB_SERVER_URL=""
+BBB_SHARED_SECRET=""
\ No newline at end of file
diff --git a/bigbluebutton-html5/tests/puppeteer/.gitignore b/bigbluebutton-html5/tests/puppeteer/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..fc13734ed0d13b5411b3be4010853d63c9e961f3
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+screenshots/*
+!screenshots/screenshots.txt
+package-lock.json
+.directory
+.env
\ No newline at end of file
diff --git a/bigbluebutton-html5/tests/puppeteer/README.md b/bigbluebutton-html5/tests/puppeteer/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4b51d976302d75ab8235de841649b5f30b9db624
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/README.md
@@ -0,0 +1 @@
+# bigbluebutton-tests
\ No newline at end of file
diff --git a/bigbluebutton-html5/tests/puppeteer/elements.js b/bigbluebutton-html5/tests/puppeteer/elements.js
new file mode 100644
index 0000000000000000000000000000000000000000..7a1fe15664df8eeb5f6350e16db5c6d167c5ac8c
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/elements.js
@@ -0,0 +1,43 @@
+exports.audioDialog = '[aria-label="Modal"]';
+exports.closeAudio = '[aria-label="Close"]';
+exports.microphoneButton = 'button[aria-label="Microphone"]'
+exports.listenButton = 'button[aria-label="Listen Only"]';
+exports.echoYes = 'button[aria-label="Echo is audible"]';
+exports.title = '._imports_ui_components_nav_bar__styles__presentationTitle';
+exports.alerts = '.toastify-content';
+exports.skipSlide = '#skipSlide';
+exports.statusIcon = '._imports_ui_components_user_avatar__styles__content';
+
+exports.actions = 'button[aria-label="Actions"]';
+exports.options = 'button[aria-label="Options"]';
+exports.userList = 'button[aria-label="Users and Messages Toggle"]';
+exports.uploadPresentation = '._imports_ui_components_dropdown__styles__top-left > div:nth-child(1) > ul:nth-child(1) > li:nth-child(1)';
+exports.joinAudio = 'button[aria-label="Join Audio"]';
+exports.leaveAudio = 'button[aria-label="Leave Audio"]';
+exports.videoMenu = 'button[aria-label="Open video menu dropdown"]';
+exports.screenShare = 'button[aria-label="Share your screen"]';
+
+exports.chatButton = 'div._imports_ui_components_user_list_chat_list_item__styles__chatName';
+exports.chatBox = '#message-input';
+exports.sendButton = '[aria-label="Send Message"]';
+exports.chatMessages = '#chat-messages';
+
+exports.whiteboard = 'svg._imports_ui_components_presentation__styles__svgStyles';
+exports.toolbox = '._imports_ui_components_whiteboard_whiteboard_toolbar__styles__toolbarContainer';
+exports.tools = 'button[aria-label="Tools"]';
+exports.pencil = 'button[aria-label="Pencil"]';
+exports.rectangle = 'button[aria-label="Rectangle"]';
+
+exports.presentationToolbarWrapper = '#presentationToolbarWrapper';
+exports.nextSlide = 'button[aria-label="Next slide"]';
+exports.prevSlide = 'button[aria-label="Previous slide"]';
+
+exports.fileUpload = 'input[type="file"]';
+exports.start = 'button[aria-label="Start"]';
+exports.cancel = 'button[aria-label="Cancel]';
+
+exports.firstUser = 'div._imports_ui_components_user_list_user_list_content__styles__participantsList:nth-child(1)';
+exports.setStatus = '._imports_ui_components_user_list_user_list_content_user_participants_user_list_item_user_dropdown__styles__usertListItemWithMenu > div:nth-child(2) > div:nth-child(1) > ul:nth-child(1) > li:nth-child(1)';
+exports.away = '._imports_ui_components_user_list_user_list_content_user_participants_user_list_item_user_dropdown__styles__usertListItemWithMenu > div:nth-child(2) > div:nth-child(1) > ul:nth-child(1) > li:nth-child(3)';
+exports.applaud = 'li._imports_ui_components_dropdown_list__styles__item:nth-child(9)';
+exports.clearStatus = '._imports_ui_components_user_list_user_list_content_user_participants_user_list_item_user_dropdown__styles__usertListItemWithMenu > div:nth-child(2) > div:nth-child(1) > ul:nth-child(1) > li:nth-child(2)';
\ No newline at end of file
diff --git a/bigbluebutton-html5/tests/puppeteer/helper.js b/bigbluebutton-html5/tests/puppeteer/helper.js
new file mode 100644
index 0000000000000000000000000000000000000000..4ec65b4cc550af458d41431fc07956acb64163af
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/helper.js
@@ -0,0 +1,49 @@
+require('dotenv').config();
+const sha1 = require('sha1');
+const axios = require('axios');
+
+const params = require('./params');
+const e = require('./elements');
+
+function getRandomInt(min, max) {
+  min = Math.ceil(min);
+  max = Math.floor(max);
+  return Math.floor(Math.random() * (max - min)) + min;
+}
+
+async function createMeeting(params)
+{
+  var meetingID = "random-" + getRandomInt(1000000, 10000000).toString();
+  var mp = params.moderatorPW;
+  var ap = params.attendeePW;
+  var query = `name=${meetingID}&meetingID=${meetingID}&attendeePW=${ap}&moderatorPW=${mp}&joinViaHtml5=true` +
+    `&record=false&allowStartStopRecording=true&autoStartRecording=false&welcome=${params.welcome}`;
+  var apicall = "create" + query + params.secret;
+  var checksum = sha1(apicall);
+  var url = params.server + "/create?" + query + "&checksum=" + checksum;
+  var response = await axios.get(url);
+  return meetingID;
+}
+
+function getJoinURL(meetingID, params, moderator)
+{
+  var pw = moderator ? params.moderatorPW : params.attendeePW;
+  var query = `fullName=${params.fullName}&joinViaHtml5=true&meetingID=${meetingID}&password=${pw}`;
+  var apicall = "join" + query + params.secret;
+  var checksum = sha1(apicall);
+  var url = params.server + "/join?" + query + "&checksum=" + checksum;
+  return url;
+}
+
+function sleep(time)
+{
+  return new Promise((resolve) =>
+  {
+    setTimeout(resolve, time);
+  });
+}
+
+exports.getRandomInt = getRandomInt;
+exports.createMeeting = createMeeting;
+exports.getJoinURL = getJoinURL;
+exports.sleep = sleep;
\ No newline at end of file
diff --git a/bigbluebutton-html5/tests/puppeteer/package.json b/bigbluebutton-html5/tests/puppeteer/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..96b7c4162cc5f0dd5657d8217f6bfe68b5b24a2f
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/package.json
@@ -0,0 +1,17 @@
+{
+  "name": "bigbluebutton-tests",
+  "version": "1.0.0",
+  "description": "",
+  "main": "app.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "axios": "^0.18.0",
+    "dotenv": "^6.0.0",
+    "puppeteer": "^1.7.0",
+    "sha1": "^1.1.1"
+  }
+}
diff --git a/bigbluebutton-html5/tests/puppeteer/page.js b/bigbluebutton-html5/tests/puppeteer/page.js
new file mode 100644
index 0000000000000000000000000000000000000000..7471b0c6fe97ccdb77dda70214d6a13550399d8c
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/page.js
@@ -0,0 +1,142 @@
+const puppeteer = require('puppeteer');
+const helper = require('./helper');
+const params = require('./params');
+const e = require('./elements');
+
+class Page
+{
+  // Initializes the page
+  async init(args)
+  {
+    this.browser = await puppeteer.launch(args);
+    this.page = await this.browser.newPage();
+  }
+
+  // Navigates to a page
+  async goto(page)
+  {
+    this.page.goto(page);
+  }
+
+  // Run the test for the page
+  async test()
+  {
+  }
+
+  // Closes the page
+  async close()
+  {
+    await this.browser.close();
+  }
+
+  // Gets the DOM elements being tested, as strings
+  async getTestElements()
+  {
+  }
+
+  // Get the default arguments for creating a page
+  static getArgs()
+  {
+    return {headless: true, args: ["--no-sandbox", "--use-fake-ui-for-media-stream"]};
+  }
+
+  // Creates a BigBlueButton meeting
+  async createBBBMeeting()
+  {
+    var meetingID = await helper.createMeeting(params);
+    await this.joinBBBMeeting(meetingID);
+    return meetingID;
+  }
+
+  // Navigates the page to join a BigBlueButton meeting
+  async joinBBBMeeting(meetingID)
+  {
+    var joinURL = helper.getJoinURL(meetingID, params, true);
+    await this.goto(joinURL);
+  }
+
+  // Joins a BigBlueButton as a listener
+  async joinAudioListenOnly()
+  {
+    await this.page.waitFor(e.listenButton);
+    await this.page.click(e.listenButton);
+    await this.elementRemoved(e.audioDialog);
+    console.log("Joined meeting as listener");
+  }
+
+  // Joins a BigBlueButton meeting with a microphone
+  async joinAudioMicrophone()
+  {
+    await this.page.waitFor(e.microphoneButton);
+    await this.page.click(e.microphoneButton);
+    await this.page.waitFor(e.echoYes);
+    await helper.sleep(500); // Echo test confirmation sometimes fails without this
+    await this.page.click(e.echoYes);
+    await this.elementRemoved(e.audioDialog);
+    console.log("Joined meeting with microphone");
+  }
+
+  // Joins a BigBlueButton meeting without audio
+  async joinWithoutAudio()
+  {
+    await this.page.waitFor(e.closeAudio);
+    await this.page.click(e.closeAudio);
+    await this.elementRemoved(e.audioDialog);
+    console.log("Joined meeting without audio");
+  }
+
+  // Returns a Promise that resolves when an element does not exist/is removed from the DOM
+  elementRemoved(element)
+  {
+    return this.page.waitFor((element) => {return !document.querySelector(element);}, {}, element);
+  }
+
+  // Presses a hotkey (Ctrl, Alt and Shift can be held down while pressing the key)
+  async hotkey(key, ctrl, alt, shift)
+  {
+    if(ctrl) {await this.page.keyboard.down('Control');}
+    if(alt) {await this.page.keyboard.down('Alt');}
+    if(shift) {await this.page.keyboard.down('Shift');}
+
+    await this.page.keyboard.press(key);
+
+    if(ctrl) {await this.page.keyboard.up('Control');}
+    if(alt) {await this.page.keyboard.up('Alt');}
+    if(shift) {await this.page.keyboard.up('Shift');}
+  }
+
+  // Presses the Tab key a set number of times
+  async tab(count)
+  {
+    for(var i = 0; i < count; i++)
+    {
+      await this.page.keyboard.press('Tab');
+    }
+  }
+
+  // Presses the Enter key
+  async enter()
+  {
+    await this.page.keyboard.press('Enter');
+  }
+
+  // Presses the Down Arrow key a set number of times
+  async down(count)
+  {
+    for(var i = 0; i < count; i++)
+    {
+      await this.page.keyboard.press('ArrowDown');
+    }
+  }
+
+  // Presses the up arrow key a set number of times
+  async up(count)
+  {
+    for(var i = 0; i < count; i++)
+    {
+      await this.page.keyboard.press('ArrowUp');
+    }
+  }
+};
+
+module.exports = exports = Page;
\ No newline at end of file
diff --git a/bigbluebutton-html5/tests/puppeteer/params.js b/bigbluebutton-html5/tests/puppeteer/params.js
new file mode 100644
index 0000000000000000000000000000000000000000..984336d7043b1cad0b68ca3c8996d68664dfc2a8
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/params.js
@@ -0,0 +1,9 @@
+module.exports = exports =
+{
+  server: process.env.BBB_SERVER_URL,
+  secret: process.env.BBB_SHARED_SECRET,
+  welcome: "%3Cbr%3EWelcome+to+%3Cb%3E%25%25CONFNAME%25%25%3C%2Fb%3E%21",
+  fullName: "User1",
+  moderatorPW: "mp",
+  attendeePW: "ap"
+};
\ No newline at end of file
diff --git a/bigbluebutton-html5/tests/puppeteer/screenshots/screenshots.txt b/bigbluebutton-html5/tests/puppeteer/screenshots/screenshots.txt
new file mode 100644
index 0000000000000000000000000000000000000000..1928861f4008bfa55dbab4cf9a8b10df464ac3ae
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/screenshots/screenshots.txt
@@ -0,0 +1 @@
+This is where screenshots from the BigBlueButton tests will be saved. 
diff --git a/bigbluebutton-html5/tests/puppeteer/test-chat.js b/bigbluebutton-html5/tests/puppeteer/test-chat.js
new file mode 100644
index 0000000000000000000000000000000000000000..e3499cadb558f1d3168f066f6d612a6ed2fb2933
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/test-chat.js
@@ -0,0 +1,72 @@
+// Test: Sending a chat message
+
+const Page = require('./page');
+const helper = require('./helper');
+const e = require('./elements');
+
+class ChatTestPage extends Page
+{
+  async test()
+  {
+    await this.createBBBMeeting();
+    await this.joinWithoutAudio();
+
+    await this.page.waitFor(e.chatButton);
+    await this.page.click(e.chatButton);
+    await this.page.waitFor(e.chatBox);
+    await this.page.waitFor(e.chatMessages);
+
+    var messages0 = await this.getTestElements();
+
+    await this.page.type(e.chatBox, "Hello world!");
+    await this.page.click(e.sendButton);
+    await helper.sleep(500);
+    
+    await this.page.screenshot({path: "screenshots/test-chat.png"});
+
+    var messages1 = await this.getTestElements();
+
+    console.log("\nChat messages before posting:");
+    console.log(JSON.stringify(messages0, null, 2));
+    console.log("\nChat messages after posting:");
+    console.log(JSON.stringify(messages1, null, 2));
+  }
+
+  async getTestElements()
+  {
+    var messages = await this.page.evaluate((chat) => 
+    {
+      var messages = [];
+      var children = document.querySelector(chat).childNodes;
+      for(var i = 0; i < children.length; i++)
+      {
+        var content = children[i].childNodes[0].childNodes[1];
+        if(content)
+        {
+          content = content.childNodes;
+          messages.push({name: content[0].innerText, message: content[1].innerText});
+        }
+      }
+      console.log(messages);
+      return messages;
+    }, e.chatMessages);
+
+    return messages;
+  }
+};
+
+var test = new ChatTestPage();
+(async() =>
+{
+  try
+  {
+    await test.init(Page.getArgs());
+    await test.test();
+    await test.close();
+  }
+  catch(e)
+  {
+    console.log(e);
+    process.exit(1);
+  }
+})();
\ No newline at end of file
diff --git a/bigbluebutton-html5/tests/puppeteer/test-draw.js b/bigbluebutton-html5/tests/puppeteer/test-draw.js
new file mode 100644
index 0000000000000000000000000000000000000000..11030b5e6533298b9eed4ae819932e4a85af83a4
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/test-draw.js
@@ -0,0 +1,60 @@
+// Test: Drawing a box
+
+const Page = require('./page');
+const helper = require('./helper');
+const e = require('./elements');
+
+class DrawTestPage extends Page
+{
+  async test()
+  {
+    await this.createBBBMeeting();
+    await this.joinWithoutAudio();
+
+    await this.page.waitFor(e.tools);
+    await this.page.click(e.tools);
+    await this.page.waitFor(e.rectangle);
+    await this.page.click(e.rectangle);
+    await this.page.waitFor(e.whiteboard);
+
+    var shapes0 = await this.getTestElements();
+
+    var wb = await this.page.$(e.whiteboard);
+    var wbBox = await wb.boundingBox();
+    await this.page.mouse.move(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height);
+    await this.page.mouse.down();
+    await this.page.mouse.move(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
+    await this.page.mouse.up();
+
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-draw.png"})
+    var shapes1 = await this.getTestElements();
+
+    console.log("\nShapes before drawing box:");
+    console.log(shapes0);
+    console.log("\nShapes after drawing box:");
+    console.log(shapes1);
+  }
+
+  async getTestElements()
+  {
+    var shapes = await this.page.evaluate(() =>{ return document.querySelector("svg g[clip-path]").children[1].outerHTML; });
+    return shapes;
+  }
+};
+
+var test = new DrawTestPage();
+(async() =>
+{
+  try
+  {
+    await test.init(Page.getArgs());
+    await test.test();
+    await test.close();
+  }
+  catch(e)
+  {
+    console.log(e);
+    process.exit(1);
+  }
+})();
\ No newline at end of file
diff --git a/bigbluebutton-html5/tests/puppeteer/test-hotkeys-mic-first.js b/bigbluebutton-html5/tests/puppeteer/test-hotkeys-mic-first.js
new file mode 100644
index 0000000000000000000000000000000000000000..d41b64b9928b21263c573f4516e87b783ab78acc
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/test-hotkeys-mic-first.js
@@ -0,0 +1,84 @@
+// Test: Hotkeys when a user first joins a meeting with a microphone: Leaving audio, rejoining as Listen Only, then rejoining with microphone
+
+const Page = require('./page');
+const helper = require('./helper');
+const e = require('./elements');
+
+class HotkeysMicFirstTestPage extends Page
+{
+  constructor()
+  {
+    super();
+    this.tabCounts = 
+    {
+      audioNoMic: 12,
+      audioMic: 13
+    }
+  }
+
+  async test()
+  {
+    await this.createBBBMeeting();
+    await this.joinAudioMicrophone();
+    await this.page.screenshot({path: "screenshots/test-hotkeys-mic-first-0.png"});
+
+    await this.page.waitFor(e.whiteboard);
+    await this.page.waitFor(e.options);
+    await this.page.waitFor(e.userList);
+    await this.page.waitFor(e.toolbox);
+    await this.page.waitFor(e.leaveAudio);
+    await this.page.waitFor(e.chatButton);
+    await this.page.waitFor(e.firstUser);
+    await this.page.waitFor(e.screenShare);
+    await this.page.waitFor(e.videoMenu);
+    await this.page.waitFor(e.actions);
+    await this.page.waitFor(e.nextSlide);
+    await this.page.waitFor(e.prevSlide);
+
+    // Leave/Join Audio as Listen Only
+    await this.elementRemoved(e.alerts);
+    await this.page.click(e.title);
+    await this.tab(this.tabCounts.audioMic);
+    await this.enter();
+    await this.enter();
+    await this.page.waitFor(e.listenButton);
+    await this.tab(3);
+    await this.enter();
+    await this.elementRemoved(e.audioDialog);
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-mic-first-1.png"});
+    
+    // Leave/Join Audio with Microphone
+    await this.elementRemoved(e.alerts);
+    await this.page.click(e.title);
+    await this.tab(this.tabCounts.audioNoMic);
+    await this.enter();
+    await this.enter();
+    await this.page.waitFor(e.microphoneButton);
+    await this.tab(2);
+    await this.enter();
+    await this.page.waitFor(e.echoYes);
+    await helper.sleep(500); // Echo test confirmation sometimes fails without this
+    await this.tab(1);
+    await this.enter();
+    await this.elementRemoved(e.audioDialog);
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-mic-first-2.png"});
+  }
+};
+
+var test = new HotkeysMicFirstTestPage();
+(async() =>
+{
+  try
+  {
+    await test.init(Page.getArgs());
+    await test.test();
+    await test.close();
+  }
+  catch(e)
+  {
+    console.log(e);
+    process.exit(1);
+  }
+})();
\ No newline at end of file
diff --git a/bigbluebutton-html5/tests/puppeteer/test-hotkeys.js b/bigbluebutton-html5/tests/puppeteer/test-hotkeys.js
new file mode 100644
index 0000000000000000000000000000000000000000..c172aee5fd8e1d67278a8446abe1a6d69ae44858
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/test-hotkeys.js
@@ -0,0 +1,180 @@
+// Test: Hotkeys: Options, User List, Leave/Join Audio, Mute/Unmute, Toggle Public Chat, Actions Menu, Status Menu
+
+const Page = require('./page');
+const helper = require('./helper');
+const e = require('./elements');
+
+class HotkeysTestPage extends Page
+{
+  constructor()
+  {
+    super();
+    this.tabCounts = 
+    {
+      options: 1,
+      actions: 11,
+      audioNoMic: 12,
+      audioMic: 13,
+      mute: 12,
+      chat: 15,
+      closeChat: 17, // Only when chat is open
+      status: 16,
+      userList: 18
+    }
+  }
+
+  async test()
+  {
+    await this.createBBBMeeting();
+    await this.joinAudioListenOnly();
+
+    await this.page.waitFor(e.whiteboard);
+    await this.page.waitFor(e.options);
+    await this.page.waitFor(e.userList);
+    await this.page.waitFor(e.toolbox);
+    await this.page.waitFor(e.leaveAudio);
+    await this.page.waitFor(e.chatButton);
+    await this.page.waitFor(e.firstUser);
+    await this.page.waitFor(e.screenShare);
+    await this.page.waitFor(e.videoMenu);
+    await this.page.waitFor(e.actions);
+    await this.page.waitFor(e.nextSlide);
+    await this.page.waitFor(e.prevSlide);
+
+    await this.elementRemoved(e.alerts);
+    
+    // Options
+    await this.page.click(e.title);
+    await this.tab(this.tabCounts.options);
+    await this.enter();
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-options-0.png"});
+    await this.enter();
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-options-1.png"});
+
+    // User List
+    await this.page.click(e.title);
+    await this.tab(this.tabCounts.userList);
+    await this.enter();
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-userlist-0.png"});
+    await this.enter();
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-userlist-1.png"});
+
+    // Toggle Public Chat
+    await this.elementRemoved(e.alerts);
+    await this.page.click(e.title);
+    await this.tab(this.tabCounts.chat);
+    await this.up(1);
+    await this.enter();
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-chat-0.png"});
+    await this.page.click(e.title);
+    await this.tab(this.tabCounts.closeChat);
+    await this.enter();
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-chat-1.png"});
+
+    // Open Actions Menu
+    await this.page.click(e.title);
+    await this.tab(this.tabCounts.actions);
+    await this.enter();
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-actions-0.png"});
+    await this.enter();
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-actions-1.png"});
+
+    // Open Status Menu
+    await this.elementRemoved(e.alerts);
+    await this.page.click(e.title);
+    await this.tab(this.tabCounts.status);
+    await this.up(1);
+    await this.enter();
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-status-0.png"});
+    await this.tab(1);
+    await this.enter();
+    await this.tab(1);
+    await this.down(7); //Applaud status
+    await this.enter();
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-status-1.png"});
+
+    // Leave/Join Audio
+    await this.page.click(e.title);
+    await this.tab(this.tabCounts.audioNoMic);
+    await this.enter();
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-audio-0.png"});
+    await this.enter();
+    await this.page.waitFor(e.microphoneButton);
+    await this.tab(2);
+    await this.enter();
+    await this.page.waitFor(e.echoYes);
+    await helper.sleep(500); // Echo test confirmation sometimes fails without this
+    await this.tab(1);
+    await this.enter();
+    await this.elementRemoved(e.audioDialog);
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-audio-1.png"});
+
+    // Mute/Unmute
+    await this.elementRemoved(e.alerts);
+    await this.page.click(e.title);
+    await this.tab(this.tabCounts.mute);
+    await this.enter();
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-mute-0.png"});
+    await this.enter();
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-hotkeys-mute-1.png"});
+  }
+
+  async tab(count)
+  {
+    for(var i = 0; i < count; i++)
+    {
+      await this.page.keyboard.press('Tab');
+    }
+  }
+
+  async enter()
+  {
+    await this.page.keyboard.press('Enter');
+  }
+
+  async down(count)
+  {
+    for(var i = 0; i < count; i++)
+    {
+      await this.page.keyboard.press('ArrowDown');
+    }
+  }
+
+  async up(count)
+  {
+    for(var i = 0; i < count; i++)
+    {
+      await this.page.keyboard.press('ArrowUp');
+    }
+  }
+};
+
+var test = new HotkeysTestPage();
+(async() =>
+{
+  try
+  {
+    await test.init(Page.getArgs());
+    await test.test();
+    await test.close();
+  }
+  catch(e)
+  {
+    console.log(e);
+    process.exit(1);
+  }
+})();
\ No newline at end of file
diff --git a/bigbluebutton-html5/tests/puppeteer/test-html5-check.js b/bigbluebutton-html5/tests/puppeteer/test-html5-check.js
new file mode 100644
index 0000000000000000000000000000000000000000..d758d81b7830143c796bcea196122e1a77ef43a4
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/test-html5-check.js
@@ -0,0 +1,48 @@
+require('dotenv').config();
+const axios = require('axios');
+const url = require('url');
+const helper = require('./helper');
+
+(async() =>
+{
+  var bbb = url.parse(process.env.BBB_SERVER_URL)
+  var check = bbb.protocol + "//" + bbb.hostname + "/html5client/check";
+  const maxRetries = 20;
+  const delay = 10000;
+  var retryCount = 0;
+  while(true)
+  {
+    try
+    {
+      var response = await axios.get(check);
+      var status = response.data.html5clientStatus
+      console.log(response.data);
+      if(status === 'running')
+      {
+        break;
+      }
+      else if (retryCount < maxRetries)
+      {
+        retryCount++;
+      }
+      else
+      {
+        process.exit(1);
+      }
+    }
+    catch(e)
+    {
+      console.log(e.message);
+      if (retryCount < maxRetries)
+      {
+        retryCount++;
+      }
+      else
+      {
+        process.exit(1);
+      }
+    }
+    console.log("Retrying (attempt " + (retryCount + 1) + "/" + maxRetries + ")...");
+    await helper.sleep(delay);
+  }
+})();
\ No newline at end of file
diff --git a/bigbluebutton-html5/tests/puppeteer/test-status.js b/bigbluebutton-html5/tests/puppeteer/test-status.js
new file mode 100644
index 0000000000000000000000000000000000000000..faad989bac984915b5b9b44c3f8d95c107118a1f
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/test-status.js
@@ -0,0 +1,67 @@
+// Test: Setting/changing/clearing a user's status
+
+const Page = require('./page');
+const helper = require('./helper');
+const e = require('./elements');
+
+class StatusTestPage extends Page
+{
+  async test()
+  {
+    await this.createBBBMeeting();
+    await this.joinWithoutAudio();
+    await this.page.screenshot({path: "screenshots/test-status-0.png"});
+    var status0 = await this.getTestElements();
+
+    await this.page.click(e.firstUser);
+    await this.page.click(e.setStatus);
+    await this.page.click(e.applaud);
+    await helper.sleep(100);
+    await this.page.screenshot({path: "screenshots/test-status-1.png"});
+    var status1 = await this.getTestElements();
+
+    await this.page.click(e.firstUser);
+    await this.page.click(e.setStatus);
+    await this.page.click(e.away);
+    await helper.sleep(100);
+    await this.page.screenshot({path: "screenshots/test-status-2.png"});
+    var status2 = await this.getTestElements();
+
+    await this.page.click(e.firstUser);
+    await this.page.click(e.clearStatus);
+    await helper.sleep(100);
+    await this.page.screenshot({path: "screenshots/test-status-3.png"});
+    var status3 = await this.getTestElements();
+
+    console.log("\nStatus at start of meeting:");
+    console.log(status0);
+    console.log("\nStatus after status set (applaud):");
+    console.log(status1);
+    console.log("\nStatus after status change (away):");
+    console.log(status2);
+    console.log("\nStatus after status clear:");
+    console.log(status3);
+  }
+
+  async getTestElements()
+  {
+    var status = await this.page.evaluate((statusIcon) => {return document.querySelector(statusIcon).innerHTML;}, e.statusIcon);
+    return status;
+  }
+};
+
+var test = new StatusTestPage();
+(async() =>
+{
+  try
+  {
+    await test.init(Page.getArgs());
+    await test.test();
+    await test.close();
+  }
+  catch(e)
+  {
+    console.log(e);
+    process.exit(1);
+  }
+})();
\ No newline at end of file
diff --git a/bigbluebutton-html5/tests/puppeteer/test-switch-slides.js b/bigbluebutton-html5/tests/puppeteer/test-switch-slides.js
new file mode 100644
index 0000000000000000000000000000000000000000..360942475b989d180aac1fe4940c340e320a93f9
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/test-switch-slides.js
@@ -0,0 +1,57 @@
+// Test: Switching slides
+
+const Page = require('./page');
+const helper = require('./helper');
+const e = require('./elements');
+
+class SlideSwitchTestPage extends Page
+{
+  async test()
+  {
+    await this.createBBBMeeting();
+    await this.joinWithoutAudio();
+
+    await this.page.waitFor(e.whiteboard);
+    await this.page.waitFor(e.presentationToolbarWrapper);
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-switch-slides-0.png"});
+    var svg0 = await this.getTestElements();
+    await this.page.click(e.nextSlide);
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-switch-slides-1.png"});
+    var svg1 = await this.getTestElements();
+    await this.page.click(e.prevSlide);
+    await helper.sleep(500);
+    await this.page.screenshot({path: "screenshots/test-switch-slides-2.png"});
+    var svg2 = await this.getTestElements();
+
+    console.log("\nStarting slide:");
+    console.log(svg0);
+    console.log("\nAfter next slide:");
+    console.log(svg1);
+    console.log("\nAfter previous slide:");
+    console.log(svg2);
+  }
+
+  async getTestElements()
+  {
+    var svg = await this.page.evaluate(() =>{ return document.querySelector("svg g g g").outerHTML; });
+    return svg;
+  }
+};
+
+var test = new SlideSwitchTestPage();
+(async() =>
+{
+  try
+  {
+    await test.init(Page.getArgs());
+    await test.test();
+    await test.close();
+  }
+  catch(e)
+  {
+    console.log(e);
+    process.exit(1);
+  }
+})();
\ No newline at end of file
diff --git a/bigbluebutton-html5/tests/puppeteer/test-upload.js b/bigbluebutton-html5/tests/puppeteer/test-upload.js
new file mode 100644
index 0000000000000000000000000000000000000000..91927dd58e1d5b24de7a4f9354d62fd67e6a5cbe
--- /dev/null
+++ b/bigbluebutton-html5/tests/puppeteer/test-upload.js
@@ -0,0 +1,65 @@
+// Test: Uploading an image
+
+const Page = require('./page');
+const helper = require('./helper');
+const e = require('./elements');
+
+class UploadTestPage extends Page
+{
+  async test()
+  {
+    await this.createBBBMeeting();
+    await this.joinWithoutAudio();
+  
+    await this.page.waitFor(e.actions);
+    await this.page.waitFor(e.whiteboard);
+    await this.page.waitFor(e.skipSlide);
+    await this.page.click(e.actions);
+    await this.page.waitFor(e.uploadPresentation);
+
+    var slides0 = await this.getTestElements();
+
+    await this.page.click(e.uploadPresentation);
+    await this.page.waitFor(e.fileUpload);
+    var fileUpload = await this.page.$(e.fileUpload);
+    await fileUpload.uploadFile(__dirname + "/upload-test.png");
+    await this.page.waitFor(e.start);
+    await this.page.click(e.start);
+    await this.elementRemoved(e.start);
+  
+    await helper.sleep(1000);
+    await this.page.screenshot({path: "screenshots/test-upload.png"});
+    var slides1 = await this.getTestElements();
+
+    console.log("\nSlides before presentation upload:");
+    console.log(slides0.slideList);
+    console.log(slides0.svg);
+    console.log("\nSlides after presentation upload:");
+    console.log(slides1.slideList);
+    console.log(slides1.svg);
+  }
+
+  async getTestElements()
+  {
+    var slides = {};
+    slides.svg = await this.page.evaluate(() =>{ return document.querySelector("svg g g g").outerHTML; });
+    slides.slideList = await this.page.evaluate((skipSlide) =>{ return document.querySelector(skipSlide).innerHTML; }, e.skipSlide);
+    return slides;
+  }
+};
+
+var test = new UploadTestPage();
+(async() =>
+{
+  try
+  {
+    await test.init(Page.getArgs());
+    await test.test();
+    await test.close();
+  }
+  catch(e)
+  {
+    console.log(e);
+    process.exit(1);
+  }
+})();
\ No newline at end of file
diff --git a/bigbluebutton-html5/tests/puppeteer/upload-test.png b/bigbluebutton-html5/tests/puppeteer/upload-test.png
new file mode 100644
index 0000000000000000000000000000000000000000..89849fb8408ed62d8b2195f51beb14ef3b4a0846
Binary files /dev/null and b/bigbluebutton-html5/tests/puppeteer/upload-test.png differ