TLDR: Script On Github

If your not using a paid service that runs your testing you probably are trying to roll your own test recording.

This method that attaches to the background xserver generated when using xfvb for background browser testing is a pretty good solution.

It will give a side-by-side of the browser interaction as well as your testing console being outputted while testing.

Script Sections

We need to start our browser testing in the background and then keep track of the display

Find an available display on the server

This could be a useful script snippet for any Xvfb operations

local tmp=$(mktemp)

# -displayfd finds unused display and sends it to file descriptor 1
Xvfb -displayfd 1 -screen 0 1920x1080x16 1>$tmp 2>/dev/null &

# wait until Xvfb finds a display with output in tmp file
# 5 seconds to find a display so we dont infini-loop
kill_count=0
while ! grep -qP "\d" $tmp;do
  [ "$kill_count" -gt 30 ] && echo "Xvfb couldn't get a display within limit" && exit 1
  sleep 1
  ((kill_count=kill_count+1))
  echo xvfb: $tmp $kill_count
done
xvfb_display=$(cat $tmp)

Start video recording in background attached to Xvfb display

function startVideo(){
  # in the background wait until the testing command
  # starts a log so we can attach to it in ffmpeg
  kill_count=0
  while [ ! -f $video_output/$1.log ];do
    [ "$kill_count" -gt 30 ] && echo "could not get testing log within limit" && exit 1
    sleep 1
    ((kill_count=kill_count+1))
    echo $video_output/$1.log $kill_count
  done

  # start recording video in background
  if [ -f $video_output/$1.log ];then
    # ffmpeg can take new commands via file content when tailed
    echo > $video_cmds
    set -x
      tail -f $video_cmds | ffmpeg -f x11grab -y \
      -video_size 1920x1080 -hide_banner \
      -loglevel error -i :$xvfb_display \
      -codec:v libx264 -r 12 \
        -vf "drawtext=textfile=$video_output/$1.log \
        :fontcolor=white \
        :fontsize=18 \
        :box=1 \
        :boxcolor=black@0.7 \
        :reload=1
        :x=w-tw-20:y=h-th-20" \
      $video_output/$1$video_extension
    set +x
  else
    echo "no log file for ffmpeg video recording. skipping video recording"
  fi
}

startVideo $test_suite_name &

Lets go over some details

-vf "drawtext=textfile=$video_output/$1.log \ insert text into the video. Normally this is done for a title screen or a persistent header during the video. This doesn’t work for a scrolling log file unless we include reload=1

:x=w-tw-20:y=h-th-20" \ will move the browser recording to the second half of the screen. In your testing code you will want to start your browser with 1/2 or 3/4 the size of 1920x1080

-i $xvfb_display attaches to our available server

Start testing under display in background

function testCommand(){

  # 1. Run gradle with arguments
  # 2. Tee stdout to tty and to stdin for sed
  # 3. replace lines >60 with a newline
  echo "Loading..." > $video_output/$2.log
  set -x
  ./gradlew test $gradle_options --rerun-tasks $1$2 2>&1 |
    tee >(sed -u -e "s/.\{75\}/&\n/g" >> $video_output/$2.log)
  set +x
}

# start testing in background
DISPLAY=":$xvfb_display" testCommand &

Cleanup

These commands will clean up our background jobs if any of the jobs fail or you manually trigger a SIG-INT with ctrl-c

# Ctrl + C kill background jobs
trap 'pkill -P $$' TERM INT HUP

If we get the exit code of our testing command we can determine if we want to delete the video on passing tests.

The command wait waits for the background pid to finish and returns the pid exit code

exit $test_fail_exit is a normal exit that will stop other background jobs. $test_fail_exit is a variable that will determine if we want a failing exit code on test failure

DISPLAY=":$xvfb_display" testCommand $1 $2 &
test_pid=$!

# wait and output exit of background process
if wait $test_pid; then
  echo q >> $video_cmds # send ffmpeg exit command
  sleep 2
  if ! $keep_logs;then
    rm -f $video_output/$2.log
  fi
  echo
  echo "Tests succeed, delete video -> $video_output/$2$video_extension"
  rm -f $video_output/$2$video_extension
  exit 0
else
  echo
  echo "1 or more test failed, keeping -> $video_output/$2$video_extension"
  echo q >> $video_cmds # send ffmpeg exit command
  sleep 2
  if ! $keep_logs;then
    rm -f $video_output/$2.log
  fi
  # kill everything else
  exit $test_fail_exit
fi

Full Script

#!/usr/bin/env bash
# set -x
# PS4='+${LINENO}: '
set -euo pipefail
IFS=$'\n\t'

method=""
class=""
package=""
directory=""
test_fail_exit=0
video=false
keep_logs=false
video_output=$PWD
testIndicator="@Test"
video_extension=".mp4"
xvfb_display=""
video_cmds=$(mktemp)

function startVideo(){
  kill_count=0
  while [ ! -f $video_output/$1.log ];do
    [ "$kill_count" -gt 30 ] && echo "could not get testing log within limit" && exit 1
    sleep 1
    ((kill_count=kill_count+1))
    echo $video_output/$1.log $kill_count
  done

  if [ -f $video_output/$1.log ];then
    # start recording video in background
    echo > $video_cmds
    set -x
      tail -f $video_cmds | ffmpeg -f x11grab -y \
      -video_size 1920x1080 -hide_banner \
      -loglevel error -i :$xvfb_display \
      -codec:v libx264 -r 12 \
        -vf "drawtext=textfile=$video_output/$1.log \
        :fontcolor=white \
        :fontsize=18 \
        :box=1 \
        :boxcolor=black@0.7 \
        :reload=1
        :x=w-tw-20:y=h-th-20" \
      $video_output/$1$video_extension
    set +x
  else
    echo "no log file for ffmpeg video recording. skipping video recording"
  fi
}

function testCommand(){

  # 1. Run gradle with arguments
  # 2. Tee stdout to tty and to stdin for sed
  # 3. replace lines >60 with a newline
  echo "Loading..." > $video_output/$2.log
  if $video; then
    set -x
    ./gradlew test $gradle_options --rerun-tasks $1$2 2>&1 |
      tee >(sed -u -e "s/.\{75\}/&\n/g" >> $video_output/$2.log)
    set +x
  else
    set -x
    ./gradlew test $gradle_options --rerun-tasks $1$2
    set +x
  fi
}

function testDirectory(){
  for f in $(find $directory -name "*.groovy")
  do
      package=$(head -1 $f | awk '{print $2}')
      #xargs removes white space
      class=$(grep -Po "(?<=class)\s\w+\s" $f | xargs)
      execute="${package}.${class}"

      if $video; then
        withVideo "--tests=" "$execute"
      else
        testCommand "--tests=" "$execute"
      fi

  done
}

function withVideo(){
  local tmp=$(mktemp)

  # Ctrl + C kill background jobs
  trap 'pkill -P $$' TERM INT HUP

  # -displayfd finds unused display and send it to file descriptor 1
  Xvfb -displayfd 1 -screen 0 1920x1080x16 1>$tmp 2>/dev/null &

  # wait until Xvfb finds a display with output in tmp file
  # 5 seconds to find a display so we dont infini-loop
  kill_count=0
  while ! grep -qP "\d" $tmp;do
    [ "$kill_count" -gt 30 ] && echo "Xvfb couldn't get a display within limit" && exit 1
    sleep 1
    ((kill_count=kill_count+1))
    echo xvfb: $tmp $kill_count
  done
  xvfb_display=$(cat $tmp)

  mkdir -p $video_output

  startVideo $2 &

  # send the "headed" browser to DISPLAY which is headless
  DISPLAY=":$xvfb_display" testCommand $1 $2 &
  test_pid=$!

  # wait and output exit of background process
  if wait $test_pid; then
    echo q >> $video_cmds # send ffmpeg exit
    sleep 2
    if ! $keep_logs;then
      rm -f $video_output/$2.log
    fi
    echo
    echo "Tests succeed, delete video -> $video_output/$2$video_extension"
    rm -f $video_output/$2$video_extension
    exit 0
  else
    echo
    echo "1 or more test failed, keeping -> $video_output/$2$video_extension"
    echo q >> $video_cmds # send ffmpeg exit
    sleep 2
    if ! $keep_logs;then
      rm -f $video_output/$2.log
    fi
    # kill everything else
    exit $test_fail_exit
  fi
}

function testSuite(){
  if $video; then
    withVideo "-Dsuite=" "$suite"
  else
    testCommand "-Dsuite=" "$suite"
  fi
}

function testPackage(){
  if $video; then
    withVideo "--tests=" "$package"
  else
     testCommand "--tests=" "$package"
  fi
}

function testClass() {
  echo Testing all @Test in class
  file="$(grep -r -w "class ${class}" | cut -d ":" -f 1 || true)"
  # Empty Check
  if [[ -z "$file" ]]; then
   echo "Error: Couldn't find an exact match"
   exit 1
  fi

  if [[ $(echo "${file}" | wc -l)  -gt 1 ]]; then
    echo Error: Found more than one file that has that class. exiting
    exit 1
  else
      package=$(head -1 ${file} | awk '{print $2}')
      execute="${package}.${class}"
      echo going to try and run class: ${execute}
      if $video; then
        withVideo "--tests=" "$execute"
      else
        testCommand "--tests=" "$execute"
      fi
  fi
}

function testMethod() {
  echo Testing a @Test method within a class
  file="$(grep -r "${testIndicator}" -A1 | grep -w "${method}" | grep  -v "${testIndicator}" | tr -s ' ' | cut -d ' ' -f 1 | awk '{print substr($1, 1, length($1)-1)}' || true)"

  # Empty Check
  if [[ -z "$file" ]]; then
   echo "Error: Couldn't find an exact match"
   exit 1
  fi

  if [[ $(echo "${file}" | wc -l)  -gt 1 ]]; then
    echo Error: Found more than one file that has that method. exiting
    exit 1
  else
    class=$(grep "class" ${file} | awk '{print $2}' )
    if [[ $(echo "$class" | wc -l) -gt 1 ]]; then
      echo Error: Found more than one class in file. This script can only work with one class
      exit 1
    else
      package=$(head -1 ${file} | awk '{print $2}')
      execute="${package}.${class}.${method}"
      echo going to try and run method:  ${execute}
      if $video; then
        withVideo "--tests=" "$execute"
      else
        testCommand "--tests=" "$execute"
      fi
    fi
  fi
}

#===  FUNCTION  ================================================================
#         NAME:  usage
#  DESCRIPTION:  Display usage information.
#===============================================================================
function usage ()
{
  echo "
  =====================================================
  Wrapper script for easier commandline test operations

  Finds test classes or methods and resolves the package
  to pass into build tools like maven or gradle
  =====================================================
  Usage :  $0 [options] [gradle arguments]
    Options:
    -h Display this message
    -d Run all tests recursivley found in directory
    -m Search for and run Method Test
    -l Keep logs from tests
    -p Put in full package declaration
    -s Run suite, must be function in build file (build.gradle)
    -x Exit code on test failure
    -v Record video (executes test in background)
    -o Video output directory (will be created)
    -c Search for and run tests inside of Class

   Examples:
    ./test.sh -p com.microfocus.sspr.tests.smokeTests.canModule.CanLookup
    ./test.sh -c CanLookup -DinternalSysProp=something
    ./test.sh -m methodInCanLookup

    # exit with exit code 1 on failure
    ./test.sh -x 1

    # Record video to this directory
    ./test.sh -m -v -o /home/me/videos methodInCanLookup"


}    # ----------  end of function usage  ----------


#-----------------------------------------------------------------------
#  Handle command line arguments
#-----------------------------------------------------------------------

while getopts "hlvd:s:x:m:c:p:o:" opt;
do
  case $opt in
  h)
    usage; exit 0   ;;
  v)
    video=true;;
  l)
    keep_logs=true;;
  o)
    video_output=$OPTARG;;
  x)
    test_fail_exit=$OPTARG;;
  m)
    method=$OPTARG
    shift $(($OPTIND - 1))
    gradle_options="$@"
    testMethod; exit 0   ;;
  c)
    class=$OPTARG
    shift $(($OPTIND - 1))
    gradle_options="$@"
    testClass; exit 0   ;;
  p)
    package=$OPTARG
    shift $(($OPTIND - 1))
    gradle_options="$@"
    testPackage; exit 0   ;;
  s)
    suite=$OPTARG
    shift $(($OPTIND - 1))
    gradle_options="$@"
    testSuite; exit 0   ;;
  d)
    directory=$OPTARG
    shift $(($OPTIND - 1))
    gradle_options="$@"
    testDirectory; exit 0   ;;
  *)  echo -e "\n  Option does not exist : OPTARG\n"
        usage; exit 1   ;;

  esac    # --- end of case ---
done