JSFiddle - React, Tailwind, and code Playground

by Amir Sharifian

HTML

<div class="container">
  <div class="elements">
    <button id="btn">
      A (Click me)
    </button>

    <button id="btn2">
      B (Right Click)
    </button>

    <button id="replay">
      Replay and stop recording
    </button>
    
    <button id="play" disabled>
      Play Again
    </button>
  </div>
  <div id="action-logs">
    <h1>
      Actions log
    </h1>
    <table id="logs">
      <thead>
        <tr>
          <th>Action</th>
          <th>Xpath</th>
        </tr>
      </thead>
      <tbody id="tbody">

      </tbody>
    </table>

  </div>
</div>

CSS

body {
  height: 1500px;
}

.container {
  display: flex;
  flex-direction: row;
  justify-content: space-around;
}

#action-logs {
  border-left: 1px solid;
  padding-left: 50px;
  height: 100vh;
  min-width: 50%;
}

table {
    border: solid 1px #DDEEEE;
    border-collapse: collapse;
    border-spacing: 0;
    font: normal 13px Arial, sans-serif;
    width: 100%;
}
table thead th {
    background-color: #DDEFEF;
    border: solid 1px #DDEEEE;
    color: #336B6B;
    padding: 10px;
    text-align: left;
    text-shadow: 1px 1px 1px #fff;
}
table tbody td {
    border: solid 1px #DDEEEE;
    color: #333;
    padding: 10px;
    text-shadow: 1px 1px 1px #fff;
}

JavaScript

const actions = [];
let replay = false;
let lastActionTime = 0;

// Sleep action
const sleep = (milliseconds) => {
  return new Promise(resolve => setTimeout(resolve, milliseconds))
}

// Get element xpath
const getXPathForElement = (element) => {
  const idx = (sib, name) => sib ?
    idx(sib.previousElementSibling, name || sib.localName) + (sib.localName == name) :
    1;
  const segs = elm => !elm || elm.nodeType !== 1 ? [''] :
    elm.id && document.getElementById(elm.id) === elm ? [`id("${elm.id}")`] : [...segs(elm.parentNode), `${elm.localName.toLowerCase()}[${idx(elm)}]`];
  return segs(element).join('/');
}

const getElementByXpath = (path) => {
  return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}

const throttle = (func, delay) => {
  let prev = Date.now() - delay;

  return (...args) => {
    let current = Date.now();
    if (current - prev >= delay) {
      prev = current;
      func.apply(null, args);
    }
  }
};

// Add log in table and actions array
const addLog = (data) => {
  // Don't log replay button actions
  if (data.data.xpath && data.data.xpath == 'id("replay")') {

    return;
  }

  // Don't log on replay
  if (replay) {
    return;
  }

  const actionTime = new Date().getTime();
  const delay = lastActionTime === 0 ? 0 : actionTime - lastActionTime;

  // Store log in array
  actions.push({
    id: actions.length + 1,
    ...data,
    delay
  });

  // Update last action time just for click and context menu
  if (data.type == "click" || data.type == "contextmenu") {
    lastActionTime = actionTime;
  }

  // Add log in table
  let table = document.getElementById("tbody");
  let row = table.insertRow(0);
  let cell1 = row.insertCell(0);
  let cell2 = row.insertCell(1);
  cell1.innerHTML = data.type;
  cell2.innerHTML = data.data.xpath ? data.data.xpath : "-";
}

// Track mouse movement
const handleMousemove = (event) => {
  addLog({
    type: "move",
    data: {
      position: {
 ...