test.js

const puppeteer = require('puppeteer');
const fs = require('fs');
const { spawn } = require('child_process');
const { computeFileFormat } = require('@ud-viz/utils_shared');

/**
 * Test browser scripts by opening a new page on a puppeteer browser and evaluating the script, script should have a structure like below
 *
 * @example () => {
 *  return new Promise((resolve)=>{
 *    //do test
 *    resolve(); // test succeed
 *  }};
 * @param {string} testFolderPath - path of the folder where scripts belong
 * @param {string} bundlePath - path of the bundle needed for scripts to work
 * @param {boolean} disableThirdParty - disable third party or not in browser
 * @todo goto necessary ?
 * @returns {Promise} - promise resolving when test have passed
 */
function browserScripts(testFolderPath, bundlePath, disableThirdParty = false) {
  return folderInBrowserPage(testFolderPath, async (page, currentFile) => {
    console.log('\n\nstart testing script ', currentFile.name);

    if (disableThirdParty) {
      // hack to disable third party cookies => https://github.com/puppeteer/puppeteer/issues/3998
      // to read in localStorage without error
      await page.goto('chrome://settings/cookies');
      await page.evaluate(() =>
        document
          .querySelector('body > settings-ui')
          .shadowRoot.querySelector('#main')
          .shadowRoot.querySelector('settings-basic-page')
          .shadowRoot.querySelector(
            '#basicPage > settings-section.expanded > settings-privacy-page'
          )
          .shadowRoot.querySelector(
            '#trackingProtection > settings-cookies-page'
          )
          .shadowRoot.querySelector('#blockThirdPartyToggle')
          .shadowRoot.querySelector('#control')
          .shadowRoot.querySelector('#bar')
          .click()
      );
    }

    await page.goto('https://example.com/');
    // import bundle.js
    if (bundlePath) await page.evaluate(fs.readFileSync(bundlePath, 'utf8'));

    console.log(currentFile.name, ' has imported bundle');
    // test script
    await page.evaluate(
      eval(fs.readFileSync(testFolderPath + '/' + currentFile.name, 'utf8'))
    ); // without the eval here console.log are not catch

    console.log(currentFile.name, ' test succeed');
  });
}

/**
 * Test scripts by spawning them and waiting the process to exit, script should have a structure like below
 *
 * @example
 * //do test
 * process.exit(0);// test succeed
 * @param {string} folderPath - path of the folder where scripts belong
 * @returns {Promise} - a promise resolving when test have passed
 */
function scripts(folderPath) {
  return new Promise((resolve) => {
    const test = (folderPath, file) => {
      return new Promise((resolveTest, rejectTest) => {
        const pathFile = folderPath + '/' + file.name;

        const child = spawn('node', [pathFile], {
          shell: true,
        });

        child.stdout.on('data', (data) => {
          console.log(`${data}`);
        });
        child.stderr.on('data', (data) => {
          console.error('\x1b[31m', file.name + ` ERROR :\n${data}`);
        });

        child.on('close', (error) => {
          if (error) {
            console.log(file.name, ' failed');
            rejectTest(error);
            return;
          }
          console.log(file.name, ' succeed');
          resolveTest();
        });
      });
    };

    fs.readdir(folderPath, { withFileTypes: true }, (err, files) => {
      if (!files || !files.length) return;

      let index = 0;

      const process = async () => {
        const file = files[index];

        if (!file) console.log(index, files);

        if (file.isFile()) {
          console.log('\n\n' + file.name, ' start');
          await test(folderPath, file);
        }

        if (index < files.length - 1) {
          index++;
          process();
        } else {
          resolve();
        }
      };

      try {
        process();
      } catch (error) {
        throw new Error(error);
      }
    });
  });
}

/**
 * Open html files in a page of a puppeteer browser in order to catch error
 *
 * @param {string} folderPath - path of the folder where html files belong
 * @param {number} port - port of the server http listening
 * @returns {Promise} - promise resolving when test have passed
 */
function html(folderPath, port) {
  return folderInBrowserPage(folderPath, (page, currentFile) => {
    // check if file is an html file
    if (computeFileFormat(currentFile.name) != 'html') return Promise.resolve();

    return new Promise((resolve) => {
      console.log('\n\nstart testing html ', currentFile.name);

      // page connect to html file
      page
        .goto(
          'http://localhost:' + port + '/' + folderPath + '/' + currentFile.name
        )
        .then(() => {
          // wait 1 sec
          setTimeout(() => {
            console.log(currentFile.name, ' test succeed');
            resolve();
          }, 3000);
        });
    });
  });
}

/**
 * @callback PageTestFile
 * @param {puppeteer.Page} page - page where the test is going to be done
 * @param {fs.Dirent} currentFile - file being tested
 * @returns {Promise} - promise resolving when test has passed
 */

/**
 *
 * @param {string} testFolderPath - path of the folder where belong files to test
 * @param {PageTestFile} pageTest - description of the test
 * @returns {Promise} - a promise resolving when test have passed
 */
function folderInBrowserPage(testFolderPath, pageTest) {
  return new Promise((resolve, reject) => {
    fs.readdir(testFolderPath, { withFileTypes: true }, async (err, files) => {
      if (err) {
        reject(err);
        return;
      }

      if (files.length) {
        // launch a headless browser
        const browser = await puppeteer.launch({
          headless: 'new',
          args: [
            '--disable-gpu',
            '--disable-dev-shm-usage',
            '--disable-setuid-sandbox',
            '--no-first-run',
            '--no-sandbox',
            '--no-zygote',
            '--deterministic-fetch',
            '--disable-features=IsolateOrigins',
            '--disable-site-isolation-trials',
            '--disable-web-security', // allow cross request origin'
          ],
        });
        // console.log('browser opened');
        // Create a new browser context

        let index = 0;

        const process = async () => {
          const currentFile = files[index];
          if (currentFile.isFile()) {
            // open a new page
            const page = await browser.newPage();

            // console log of the page are print in console.log of this process
            // https://pptr.dev/api/puppeteer.pageeventobject
            page
              .on('console', (message) =>
                console.log(`${message.type().toUpperCase()} ${message.text()}`)
              )
              .on('error', ({ message }) => {
                throw new Error(currentFile.name + ' ERROR: ' + message);
              })
              .on('pageerror', ({ message }) => {
                throw new Error(currentFile.name + ' ERROR: ' + message);
              })
              .on('response', (response) => {
                const log = `${response.status()} ${response.url()}`;
                if (response.status() >= 400 && response.status() < 500) {
                  if (!response.url().includes('http://localhost')) {
                    console.warn(`ERROR SKIP: ${log}`);
                  } else {
                    throw new Error(log);
                  }
                } else {
                  console.log(log);
                }
              })
              .on('requestfailed', (request) => {
                const url = new URL(request.url());

                if (!url.host.includes('http://localhost'))
                  console.warn(
                    `ERROR SKIP: ${
                      request.failure().errorText
                    } ${request.url()}`
                  );
                else
                  throw new Error(
                    `${request.failure().errorText} ${request.url()}`
                  );
              });

            await pageTest(page, currentFile);

            // close
            await page.close();

            // console.log(currentFile.name, ' close page');
          }

          if (index < files.length - 1) {
            index++;
            await process();
          }
        };

        await process();

        await browser.close();
        // console.log('browser closed');
      }

      resolve();
    });
  });
}

module.exports = {
  scripts: scripts,
  browserScripts: browserScripts,
  html: html,
};