testing.mjs

import { isNodeJs, isBatchMode, setBatchMode, postponePromise } from './core.mjs';
import { select as d3_select } from './d3.mjs';
import { _loadJSDOM } from './base/BasePainter.mjs';
import { cleanup, getElementCanvPainter } from './base/ObjectPainter.mjs';
import { draw } from './draw.mjs';
import { closeMenu } from './gui/menu.mjs';


async function _test_timeout(args, portion = 1) {
   if (!args?.timeout)
      return true;

   return postponePromise(true, Math.round(portion * args.timeout));
}

class EmulationMouseEvent {

   constructor(x = 0, y = 0) {
      this.$emul = true; // special flag mark emulated event
      this.button = 0;
      this.key = '';
      this.set(x, y);
   }

   set(x, y) {
      this.clientX = Math.round(x);
      this.clientY = Math.round(y);
   }

   setTouch(x1, y1, x2, y2) {
      this.$touch_arr = [[Math.round(x1), Math.round(y1)], [Math.round(x2), Math.round(y2)]];
   }

  preventDefault() {}
  stopPropagation() {}

} // class EmulationMouseEvent

function _getAllSubPads(cp) {
   const sub = [];
   cp?.forEachPainterInPad(p => {
      if ((p !== cp) && p.getFramePainter())
         sub.push(p);
   }, 'pads');
   return sub.length > 4 ? [] : sub; // do not test large canvas with many-many subpads
}

/** @summary test zooming features
  * @private */
async function testZooming(node, args, pp) {
   const cp = getElementCanvPainter(node),
         pad_painter = pp ?? cp;
   if (!pad_painter) return;
   const fp = pad_painter.getFramePainter();
   if (!fp && !pp) {
      const sub_pads = _getAllSubPads(cp);
      for (let k = 0; k < sub_pads.length; ++k)
         await testZooming(node, args, sub_pads[k]);
      return;
   }

   if ((typeof fp?.zoom !== 'function') || (typeof fp?.zoomSingle !== 'function')) return;
   if (typeof fp.scale_xmin === 'undefined' || typeof fp.scale_ymax === 'undefined') return;

   const xmin = fp.scale_xmin, xmax = fp.scale_xmax, ymin = fp.scale_ymin, ymax = fp.scale_ymax;

   if (args.debug) console.log(`test zooming in range: ${xmin} ${xmax} ${ymin} ${ymax}`);

   await fp.zoom(xmin + 0.2*(xmax - xmin), xmin + 0.8*(xmax - xmin), ymin + 0.2*(ymax - ymin), ymin + 0.8*(ymax - ymin));
   await _test_timeout(args);
   await fp.unzoom();
   await _test_timeout(args);
   await fp.zoomSingle('x', xmin + 0.22*(xmax - xmin), xmin + 0.25*(xmax - xmin));
   await _test_timeout(args);
   await fp.zoomSingle('y', ymin + 0.12*(ymax - ymin), ymin + 0.43*(ymax - ymin));
   await _test_timeout(args);
   await fp.unzoom();
}

/** @summary test mouse zooming features
  * @private */
async function testMouseZooming(node, args, pp) {
   const cp = getElementCanvPainter(node),
         pad_painter = pp ?? cp;
   if (!pad_painter) return;
   const fp = pad_painter.getFramePainter();

   if (!fp && !pp) {
      const sub_pads = _getAllSubPads(cp);
      for (let k = 0; k < sub_pads.length; ++k)
         await testMouseZooming(node, args, sub_pads[k]);
      return;
   }

   if (fp?.mode3d) return;
   if ((typeof fp?.startRectSel !== 'function') ||
       (typeof fp?.moveRectSel !== 'function') ||
       (typeof fp?.endRectSel !== 'function')) return;

   const fw = fp.getFrameWidth(), fh = fp.getFrameHeight(),
         evnt = new EmulationMouseEvent(),
         rect = fp.getFrameSvg().node().getBoundingClientRect();

   if (args.debug) console.log(`test mouse zooming in frame: ${fw} ${fh}`);

   // region zooming

   for (let side = -1; side <= 1; side++) {
      evnt.set(rect.x + (side > 0 ? -25 : fw*0.1), rect.y + (side < 0 ? fh + 25 : fh*0.1));

      fp.startRectSel(evnt);

      await _test_timeout(args);

      for (let i = 2; i < 10; ++i) {
         evnt.set(rect.x + (side > 0 ? -5 : fw*0.1*i), rect.y + (side < 0 ? fh + 25 : fh*0.1*i));
         fp.moveRectSel(evnt);
         await _test_timeout(args, 0.2);
      }

      await fp.endRectSel(evnt);

      await _test_timeout(args);

      await fp.unzoom();
   }
}

/** @summary test touch zooming features
  * @private */
async function testTouchZooming(node, args, pp) {
   const cp = getElementCanvPainter(node),
         pad_painter = pp ?? cp;
   if (!pad_painter) return;
   const fp = pad_painter.getFramePainter();

   if (!fp && !pp) {
      const sub_pads = _getAllSubPads(cp);
      for (let k = 0; k < sub_pads.length; ++k)
         await testTouchZooming(node, args, sub_pads[k]);
      return;
   }

   if (fp?.mode3d) return;
   if ((typeof fp?.startTouchZoom !== 'function') ||
       (typeof fp?.moveTouchZoom !== 'function') ||
       (typeof fp?.endTouchZoom !== 'function')) return;

   const fw = fp.getFrameWidth(), fh = fp.getFrameHeight(),
         evnt = new EmulationMouseEvent();

   if (args.debug) console.log(`test touch zooming in frame: ${fw} ${fh}`);

   evnt.setTouch(fw*0.4, fh*0.4, fw*0.6, fh*0.6);

   fp.startTouchZoom(evnt);

   await _test_timeout(args);

   for (let i = 2; i < 9; ++i) {
      evnt.setTouch(fw*0.05*(10 - i), fh*0.05*(10 - i), fw*0.05*(10 + i), fh*0.05*(10 + i));
      fp.moveTouchZoom(evnt);
      await _test_timeout(args, 0.2);
   }

   await fp.endTouchZoom(evnt);

   await _test_timeout(args);

   await fp.unzoom();
}

/** @summary test mouse wheel zooming features
  * @private */
async function testMouseWheel(node, args, pp) {
   const cp = getElementCanvPainter(node),
         pad_painter = pp ?? cp;
   if (!pad_painter) return;
   const fp = pad_painter.getFramePainter();

   if (!fp && !pp) {
      const sub_pads = _getAllSubPads(cp);
      for (let k = 0; k < sub_pads.length; ++k)
         await testMouseWheel(node, args, sub_pads[k]);
      return;
   }

   if (fp?.mode3d) return;
   if (typeof fp?.mouseWheel !== 'function') return;

   const fw = fp.getFrameWidth(), fh = fp.getFrameHeight(),
         evnt = new EmulationMouseEvent(),
         rect = fp.getFrameSvg().node().getBoundingClientRect();

   evnt.set(rect.x + fw*0.4, rect.y + fh*0.4);

   // zoom inside
   for (let i = 0; i < 7; ++i) {
      evnt.wheelDelta = 1;
      await fp.mouseWheel(evnt);
      await _test_timeout(args, 0.2);
   }

   // zoom outside
   for (let i = 0; i < 7; ++i) {
      evnt.wheelDelta = -1;
      await fp.mouseWheel(evnt);
      await _test_timeout(args, 0.2);
   }

   await _test_timeout(args);

   await fp.unzoom();
}


async function testFrameClick(node, pp) {
   const cp = getElementCanvPainter(node),
         pad_painter = pp ?? cp;
   if (!pad_painter) return;
   const fp = pad_painter.getFramePainter();

   if (!fp && !pp) {
      const sub_pads = _getAllSubPads(cp);
      for (let k = 0; k < sub_pads.length; ++k)
         await testFrameClick(node, sub_pads[k]);
      return;
   }

   if (fp?.mode3d || typeof fp?.processFrameClick !== 'function') return;

   const fw = fp.getFrameWidth(), fh = fp.getFrameHeight();

   for (let i = 1; i < 15; i++) {
      for (let j = 1; j < 15; j++) {
         const pnt = { x: Math.round(i/15*fw), y: Math.round(j/15*fh) };
         fp.processFrameClick(pnt);
      }
   }
}

async function testFrameMouseDoubleClick(node, pp) {
   const cp = getElementCanvPainter(node),
         pad_painter = pp ?? cp;
   if (!pad_painter) return;
   const fp = pad_painter.getFramePainter();

   if (!fp && !pp) {
      const sub_pads = _getAllSubPads(cp);
      for (let k = 0; k < sub_pads.length; ++k)
         await testFrameMouseDoubleClick(node, sub_pads[k]);
      return;
   }

   if (fp?.mode3d || typeof fp?.mouseDoubleClick !== 'function') return;

   const fw = fp.getFrameWidth(), fh = fp.getFrameHeight(),
         evnt = new EmulationMouseEvent(),
         rect = fp.getFrameSvg().node().getBoundingClientRect();

   for (let i = -2; i < 14; i++) {
      for (let j = -2; j < 14; j++) {
         evnt.set(rect.x + i/10*fw, rect.y + j/10*fh);
         await fp.mouseDoubleClick(evnt);
      }
   }
}

async function testFrameContextMenu(node, args, pp) {
   const cp = getElementCanvPainter(node),
         pad_painter = pp ?? cp;
   if (!pad_painter) return;
   const fp = pad_painter.getFramePainter();

   if (!fp && !pp) {
      const sub_pads = _getAllSubPads(cp);
      for (let k = 0; k < sub_pads.length; ++k)
         await testFrameContextMenu(node, args, sub_pads[k]);
      return;
   }

   if (fp?.mode3d || typeof fp?.showContextMenu !== 'function') return;

   const fw = fp.getFrameWidth(), fh = fp.getFrameHeight(),
         evnt = new EmulationMouseEvent(),
         rect = fp.getFrameSvg().node().getBoundingClientRect();

   for (let i = 1; i < 10; i++) {
      for (let j = 1; j < 10; j++) {
         evnt.set(rect.x + i/10*fw, rect.y + j/10*fh);
         await fp.showContextMenu('', evnt);
         await _test_timeout(args, 0.03);
         closeMenu();
      }
   }

   evnt.set(rect.x + 20, rect.y + fh + 20);
   await fp.showContextMenu('x', evnt);
   await _test_timeout(args, 0.1);
   closeMenu();

   evnt.set(rect.x - 20, rect.y + 20);
   await fp.showContextMenu('y', evnt);
   await _test_timeout(args, 0.1);
   closeMenu();
}

async function testPadContextMenu(node, args, pp) {
   const cp = pp ?? getElementCanvPainter(node);

   if (!pp && cp) {
      const sub_pads = _getAllSubPads(cp);
      for (let k = 0; k < sub_pads.length; ++k)
         await testPadContextMenu(node, args, sub_pads[k]);
   }

   if (typeof cp?.padContextMenu !== 'function') return;

   const pw = cp.getPadWidth(), ph = cp.getPadHeight(),
         evnt = new EmulationMouseEvent(),
         rect = cp.svg_this_pad().node().getBoundingClientRect();

   for (let i = 1; i < 10; i++) {
      for (let j = 1; j < 10; j++) {
         evnt.set(rect.x + i/10*pw, rect.y + j/10*ph);
         await cp.padContextMenu(evnt);
         await _test_timeout(args, 0.03);
         closeMenu();
      }
   }
}

async function testPadItemContextMenu(node, args, pp) {
   const cp = pp ?? getElementCanvPainter(node);

   if (!pp && cp) {
      const sub_pads = _getAllSubPads(cp);
      for (let k = 0; k < sub_pads.length; ++k)
         await testPadItemContextMenu(node, args, sub_pads[k]);
   }

   if (typeof cp?.itemContextMenu !== 'function') return;

   const nprimitives = cp.painters?.length ?? 0,
         names = ['xaxis', 'yaxis', 'zaxis', 'pad', 'frame'];

   for (let i = -names.length; i < nprimitives; ++i) {
      const name = (i < 0) ? names[i+names.length] : i.toString();
      await cp.itemContextMenu(name);
      await _test_timeout(args, 0.1);
      closeMenu();
   }
}

async function testPadButtons(node, args) {
   const cp = getElementCanvPainter(node);
   if (typeof cp?.clickPadButton !== 'function') return;

   const evnt = new EmulationMouseEvent(50, 50),
         toggles = ['ToggleZoom', 'ToggleLogX', 'ToggleLogY', 'ToggleLogZ', 'Toggle3D', 'ToggleColorZ', 'ToggleStatBox'];

   await cp.clickPadButton('PadContextMenus', evnt);
   await _test_timeout(args, 0.1);
   closeMenu();

   if (!args.no_enlarge)
      toggles.unshift('enlargePad');

   for (let i = 0; i < toggles.length; ++i) {
      await cp.clickPadButton(toggles[i], evnt);
      await _test_timeout(args, 0.1);
      await cp.clickPadButton(toggles[i], evnt);
      await _test_timeout(args, 0.1);
   }
}


async function _testing(dom, args) {
   await testFrameClick(dom);
   await testFrameMouseDoubleClick(dom);

   await testZooming(dom, args);
   await testMouseZooming(dom, args);
   await testMouseWheel(dom, args);
   await testFrameContextMenu(dom, args);
   await testTouchZooming(dom, args);

   await testFrameClick(dom);
   await testFrameMouseDoubleClick(dom);

   await testPadContextMenu(dom, args);
   await testPadItemContextMenu(dom, args);
   await testPadButtons(dom, args);
}


/** @summary test interactive features of JSROOT drawings
  * @desc used in https://github.com/linev/jsroot-test
  * @private */
async function testInteractivity(args) {
   if (args.dom)
      return _testing(args.dom, args);

   async function build(main) {
      main.attr('width', args.width).attr('height', args.height)
          .style('width', args.width + 'px').style('height', args.height + 'px');

      setBatchMode(false);

      return main;
   }

   const flag = isBatchMode(),
         pr = isNodeJs()
          ? _loadJSDOM().then(handle => build(handle.body.append('div')))
          : build(d3_select('body').append('div'));
   return pr.then(main => {
      main.attr('width', args.width).attr('height', args.height)
          .style('width', args.width + 'px').style('height', args.height + 'px');

      setBatchMode(false);

      return draw(main.node(), args.object, args.option || '')
             .then(() => _testing(main.node(), args))
             .then(() => {
                cleanup(main.node());
                main.remove();
                setBatchMode(flag);
                return true;
              });
   });
}

export { testInteractivity };