Automating report generation using Selenium Webdriver

If you need to automate the generation of chart images or PDF, Selenium Webdriver might be the best option. It can handle different browsers even though this example only shows it using Firefox.

Setting up

  • Install NodeJS
  • Install Firefox
  • Create a folder
  • Create a file called package.json in the new folder with the content shown below:
{
  "name": "amcharts-v5-selenium",
  "version": "1.0.0",
  "description": "Automate chart export",
  "devDependencies": {
    "geckodriver": "^3.2.0",
    "selenium-webdriver": "^4.8.1"
  }
}
  • Open a terminal and run npm i on the same folder to install the dependencies

Exporting a chart from an external link

You can export charts that exist on a current page.

Add a file called external-export.js for now. You can rename it as you wish. Add the code below in it:

const {
  Builder
} = require('selenium-webdriver');
const firefox = require('selenium-webdriver/firefox');
require('geckodriver');


(async () => {
  let url = 'https://www.amcharts.com/demos/exporting-chart-to-image/',
    options = new firefox.Options();

  // Set some conditions for the download manager
  options.setPreference('browser.helperApps.neverAsk.saveToDisk', 'application/pdf');
  options.setPreference('browser.download.folderList', 2);
  options.setPreference('browser.download.manager.showWhenStarting', false);
  options.setPreference('pdfjs.disabled', true);

  // Set the directory to save the exported file
  options.setPreference('browser.download.dir', __dirname);

  // Open the browser headless
  options.addArguments('-headless');

  let driver = await new Builder()
    .setFirefoxOptions(options)
    .forBrowser('firefox')
    .build();

  try {
    await driver.get(url);
    await driver.executeAsyncScript(() => {
      const callback = arguments[arguments.length - 1],
        exportChart = () => {
          exporting.events.on('exportfinished', () => {
            setTimeout(callback, 5000);
          });

          // Start the export
          exporting.download('pdf');
        },
        startExportWhenReady = () => {
          let timeout;
          root.events.on("frameended", waitForFinalFrameEnded);

          function waitForFinalFrameEnded() {
            if (timeout) {
              clearTimeout(timeout);
            }
            timeout = setTimeout(function() {
              root.events.off("frameended", waitForFinalFrameEnded);
              exportChart();
            }, 100);
          }
        };
      if (document.readyState === 'complete')
        startExportWhenReady();
      else
        document.addEventListener('DOMContentLoaded', startExportWhenReady);
    })
  } finally {
    await driver.quit();
  }
})()

Now run node external-export.js in the terminal.

The exported files will be in the folder you created

Exporting a chart from the same package

If you prefer to create charts without requiring external pre-existing pages, you can have them built into this solution. We will setup a server using HapiJS by first adding the following dependencies in the package.json file:

{
  "name": "amcharts-v5-selenium",
  "version": "1.0.0",
  "description": "Automate chart export",
  "devDependencies": {
    "@hapi/hapi": "^21.3.0",
    "@hapi/inert": "^7.1.0",
    "geckodriver": "^3.2.0",
    "selenium-webdriver": "^4.8.1"
  }
}

Run npm i again to install the new dependencies.

Add server.js file and the following code into it:

const Hapi = require('@hapi/hapi');
const Inert = require('@hapi/inert');
const exportChart = require('./internal-export');

const server = Hapi.server({
    host: 'localhost',
    port: 3000
});

server.route({
    method: 'GET',
    path: '/',
    handler: (request, h) => {
        return h.file('./public/index.html');
    }
});

server.route({
    method: 'GET',
    path: '/{fileName*}',
    handler: (request, h) => {
        return h.file('./public/' + request.params.fileName);
    }
});

const init = async () => {

    await server.register(Inert);
    await server.start();

    console.log(`Server running at: ${server.info.uri}`);
}

server.events.on('start', () => {

    // Start the export feature
    exportChart(server);

    console.log('Server started and the export module is loaded');
});

server.events.on('stop', () => {
    console.log('Server stopped');
});

init();

Adding files and folders

At this point you should have some of the files below. Please add the missing folders and files as shown and then the correspondent content:

/
├── package.json
├── server.js
├── internal-export.js
├── external-export.js
└── public/
  ├── index.html
  ├── styles/
  |   └── app.css
  └── scripts/
    └── chart.js 

The index.html file

<!DOCTYPE html> 
<html lang="en"> 
<head> 
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 
  <title>amCharts 5</title> 
  <link rel="stylesheet" href="/styles/app.css"> 
</head> 
<body> 
    <div id="chartdiv"></div> 
  <script src="https://cdn.amcharts.com/lib/5/index.js"></script>
  <script src="https://cdn.amcharts.com/lib/5/xy.js"></script>
  <script src="https://cdn.amcharts.com/lib/5/themes/Animated.js"></script>
  <script src="https://cdn.amcharts.com/lib/5/plugins/exporting.js"></script>
  <script src="/scripts/chart.js"></script> 
</body> 
</html>

The app.css file

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}

#chartdiv {
  width: 100%;
  height: 500px;
}

The chart.js file

var root = am5.Root.new("chartdiv");

root.setThemes([
  am5themes_Animated.new(root)
]);

var chart = root.container.children.push(am5xy.XYChart.new(root, {
  panX: false,
  panY: false,
  wheelX: "panX",
  wheelY: "zoomX",
  layout: root.verticalLayout
}));


var data = [{
  country: "USA",
  visits: 4025,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "China",
  visits: 1882,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "Japan",
  visits: 1809,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "Germany",
  visits: 1322,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "UK",
  visits: 1122,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "France",
  visits: 1114,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "India",
  visits: 984,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "Spain",
  visits: 711,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "Netherlands",
  visits: 665,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "Russia",
  visits: 580,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "South Korea",
  visits: 443,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "Canada",
  visits: 441,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "Brazil",
  visits: 395,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "Italy",
  visits: 386,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "Australia",
  visits: 384,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "Taiwan",
  visits: 338,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}, {
  country: "Poland",
  visits: 328,
  columnSettings: {
    fill: chart.get("colors").next()
  }
}]


var xRenderer = am5xy.AxisRendererX.new(root, {
  cellStartLocation: 0.1,
  cellEndLocation: 0.9,
  minGridDistance: 50
});

var xAxis = chart.xAxes.push(am5xy.CategoryAxis.new(root, {
  categoryField: "country",
  renderer: xRenderer,
  tooltip: am5.Tooltip.new(root, {})
}));

xRenderer.grid.template.setAll({
  location: 1
});

xRenderer.labels.template.setAll({
  multiLocation: 0.5
})

xAxis.data.setAll(data);

var yAxis = chart.yAxes.push(am5xy.ValueAxis.new(root, {
  renderer: am5xy.AxisRendererY.new(root, {
    strokeOpacity: 0.1
  })
}));


var series = chart.series.push(am5xy.ColumnSeries.new(root, {
  xAxis: xAxis,
  yAxis: yAxis,
  valueYField: "visits",
  categoryXField: "country"
}));

series.columns.template.setAll({
  tooltipText: "{categoryX}: {valueY}",
  width: am5.percent(90),
  tooltipY: 0,
  strokeOpacity: 0,
  templateField: "columnSettings"
});

series.data.setAll(data);


var exporting = am5plugins_exporting.Exporting.new(root, {
  menu: am5plugins_exporting.ExportingMenu.new(root, {})
});


series.appear();
chart.appear(1000, 100);

The internal-export.js file

const {
  Builder
} = require('selenium-webdriver');
const firefox = require('selenium-webdriver/firefox');
require('geckodriver');


module.exports = async (server) => {
  let url = 'http://localhost:3000';
  options = new firefox.Options();

  // Set some conditions for the download manager
  options.setPreference('browser.helperApps.neverAsk.saveToDisk', 'application/pdf');
  options.setPreference('browser.download.folderList', 2);
  options.setPreference('browser.download.manager.showWhenStarting', false);
  options.setPreference('pdfjs.disabled', true);

  // Set the directory to save the exported file
  options.setPreference('browser.download.dir', __dirname);

  // Open the browser headless
  options.addArguments('-headless');

  let driver = await new Builder()
    .setFirefoxOptions(options)
    .forBrowser('firefox')
    .build();

  try {
    await driver.get(url);
    await driver.executeAsyncScript(() => {
      const callback = arguments[arguments.length - 1],
        exportChart = () => {
          exporting.events.on('exportfinished', () => {
            setTimeout(callback, 5000);
          });
          // Start the export
          exporting.download('pdf');
        },

        startExportWhenReady = () => {
          let timeout;
          root.events.on("frameended", waitForFinalFrameEnded);

          function waitForFinalFrameEnded() {
            if (timeout) {
              clearTimeout(timeout);
            }
            timeout = setTimeout(function() {
              root.events.off("frameended", waitForFinalFrameEnded);
              exportChart();
            }, 100);
          }
        };
      if (document.readyState === 'complete')
        startExportWhenReady();
      else
        document.addEventListener('DOMContentLoaded', startExportWhenReady);
    })
  } finally {
    await driver.quit();
  }
}

Now run node server.js to start the server and to generate the chart.

Where to customize the export functionality

You can customize the code calling the export functionality inside the driver.executeAsyncScript method as shown below:

await driver.executeAsyncScript(() => {
  const callback = arguments[arguments.length - 1],
    exportChart = () => {
      exporting.events.on('exportfinished', () => {
        setTimeout(callback, 5000);
      });

      // Start the export
      exporting.download('pdf');
    },
    startExportWhenReady = () => {
      let timeout;
      root.events.on("frameended", waitForFinalFrameEnded);

      function waitForFinalFrameEnded() {
        if (timeout) {
          clearTimeout(timeout);
        }
        timeout = setTimeout(function() {
          root.events.off("frameended", waitForFinalFrameEnded);
          exportChart();
        }, 100);
      }
    };
  if (document.readyState === 'complete')
    startExportWhenReady();
  else
    document.addEventListener('DOMContentLoaded', startExportWhenReady);
})

Download the example

Download the whole example here and run npm i to load the dependencies.

Posted in Uncategorized