pankod/refine

View on GitHub
.github/workflows/e2e-examples.js

Summary

Maintainability
C
1 day
Test Coverage
#!/usr/bin/env node

const fs = require("fs");
const waitOn = require("wait-on");
const pidtree = require("pidtree");
const { join: pathJoin } = require("path");
const { promisify } = require("util");
const { exec } = require("child_process");

const KEY = process.env.KEY;
const CI_BUILD_ID = process.env.CI_BUILD_ID;

const EXAMPLES_DIR = "./examples";
const EXAMPLES = process.env.EXAMPLES ? process.env.EXAMPLES : [];
const BASE_REF = process.env.BASE_REF ? process.env.BASE_REF : "master";

const execPromise = (command) => {
  let commandProcess;
  const promise = new Promise((resolve, reject) => {
    commandProcess = exec(command);

    commandProcess.stdout.on("data", console.log);
    commandProcess.stderr.on("data", console.error);

    commandProcess.on("close", (code) => {
      if (code === 0) {
        resolve();
      } else {
        reject();
      }
    });
  });

  return {
    promise,
    pid: commandProcess.pid,
    process: commandProcess,
  };
};

const getProjectInfo = async (path) => {
  // read package.json
  const pkg = await promisify(fs.readFile)(
    pathJoin(path, "package.json"),
    "utf8",
  );

  // parse package.json
  const packageJson = JSON.parse(pkg);

  const dependencies = Object.keys(packageJson.dependencies || {});
  const devDependencies = Object.keys(packageJson.devDependencies || {});

  let port = 3000;
  let command = "npm run dev";
  let additionalParams = "";

  // check for vite
  if (dependencies.includes("vite") || devDependencies.includes("vite")) {
    port = 5173;
  }

  // check for next and remix
  if (
    dependencies.includes("next") ||
    devDependencies.includes("next") ||
    dependencies.includes("@remix-run/node") ||
    devDependencies.includes("@remix-run/node")
  ) {
    port = 3000;
    command = "npm run build && npm run start:prod";
  }

  if (
    dependencies.includes("react-scripts") ||
    devDependencies.includes("react-scripts")
  ) {
    additionalParams = "-- --host 127.0.0.1";
  }

  return {
    port,
    command,
    additionalParams,
  };
};

const prettyLog = (bg, ...args) => {
  const colors = {
    blue: "\x1b[44m",
    red: "\x1b[41m",
    green: "\x1b[42m",
  };
  const code = colors[bg] || colors.blue;

  console.log(code, ...args, "\x1b[0m");
};

const getProjectsWithE2E = async () => {
  return (
    await Promise.all(
      EXAMPLES.split(",").map(async (path) => {
        const dir = pathJoin(EXAMPLES_DIR, path);
        const isDirectory = (await promisify(fs.stat)(dir)).isDirectory();
        const isConfigExists = await promisify(fs.exists)(
          pathJoin(dir, "cypress.config.ts"),
        );

        if (isDirectory && isConfigExists) {
          return path;
        }
      }),
    )
  ).filter(Boolean);
};

const waitOnFor = async (resource) => {
  try {
    await waitOn({
      resources: [resource],
      log: true,
    });

    return resource;
  } catch (error) {
    if (error) console.log(error);

    return false;
  }
};

const waitForServer = async (port) => {
  // biome-ignore lint/suspicious/noAsyncPromiseExecutor: We have a valid use-case here.
  return new Promise(async (resolve) => {
    setTimeout(() => {
      resolve(false);
    }, 120000);

    try {
      const resolvedResource = await Promise.any([
        waitOnFor(`tcp:${port}`),
        waitOnFor(`http://localhost:${port}`),
        waitOnFor(`http://127.0.0.1:${port}`),
      ]);

      resolve(resolvedResource);
    } catch (error) {
      if (error) console.log(error);

      resolve(false);
    }
  });
};

const waitForClose = (resource) => {
  // biome-ignore lint/suspicious/noAsyncPromiseExecutor: We have a valid use-case here.
  return new Promise(async (resolve) => {
    setTimeout(() => {
      resolve(false);
    }, 120000);

    try {
      await waitOn({
        resources: [resource],
        reverse: true,
        log: true,
      });

      resolve(resource);
    } catch (error) {
      if (error) console.log(error);

      resolve(false);
    }
  });
};

const runTests = async () => {
  const examplesToRun = await getProjectsWithE2E();

  if (examplesToRun.length === 0) {
    return { success: true, empty: true };
  }

  prettyLog("blue", `Running Tests for ${examplesToRun.length} Examples`);
  prettyLog("blue", `Examples: ${examplesToRun.join(", ")}`);
  console.log("\n");

  const failedExamples = []; // { name: string; error: any };

  for await (const path of examplesToRun) {
    console.log(`::group::Example ${path}`);

    const { port, command, additionalParams } = await getProjectInfo(
      `${EXAMPLES_DIR}/${path}`,
    );
    console.log("port", port);
    console.log("command", command);
    console.log("additionalParams", additionalParams);

    prettyLog("blue", `Running for ${path} at port ${port}`);

    prettyLog("blue", "Starting the dev server");

    let start;

    let failed = false;

    // starting the dev server
    try {
      start = exec(
        `cd ${pathJoin(EXAMPLES_DIR, path)} && ${command} ${additionalParams}`,
      );

      start.stdout.on("data", console.log);
      start.stderr.on("data", console.error);
    } catch (error) {
      prettyLog("red", "Error occured on starting the dev server");
      failed = true;
    }

    let respondedUrl = false;

    try {
      prettyLog("blue", `Waiting for the server to start at port ${port}`);

      const status = await waitForServer(port);
      if (!status) {
        prettyLog("red", "Error occured on waiting for the server to start");
        failed = true;
      } else {
        respondedUrl = status;
        prettyLog("green", `Server started at ${status}`);
      }
    } catch (error) {
      prettyLog("red", "Error occured on waiting for the server to start");
      if (error) console.log(error);

      failed = true;
    }

    try {
      if (!failed) {
        const params = `-- --record --key ${KEY} --group ${path}`;
        const runner = `npm run lerna run cypress:run -- --scope ${path} ${params}`;

        prettyLog("blue", `Running tests for ${path}`);

        const { promise } = execPromise(runner);

        await promise;

        prettyLog("green", `Tests for ${path} finished`);
      }
    } catch (error) {
      prettyLog("red", `Error occured on tests for ${path}`);
      if (error) console.log(error);

      failed = true;
    } finally {
      prettyLog("blue", "Killing the dev server");

      try {
        if (start.pid) {
          const pidsOfStart = await pidtree(start.pid, {
            root: true,
          });

          pidsOfStart.forEach((pid) => {
            process.kill(pid, "SIGINT");
          });

          await waitForClose(respondedUrl);

          prettyLog("green", "Killed the dev server");
        } else {
          failed = true;
        }
      } catch (error) {
        prettyLog("red", "Error occured on killing the dev server");
        if (error) console.log(error);
        failed = true;
      }
    }

    if (!failed) {
      prettyLog("green", `Tests for ${path} finished successfully`);
    } else {
      failedExamples.push({ name: path });
      prettyLog("red", `Tests for ${path} failed.`);
    }

    console.log("::endgroup::");
  }

  if (failedExamples.length > 0) {
    return { success: false, failedExamples };
  }

  return { success: true };
};

runTests()
  .then(({ error, success, empty, failedExamples }) => {
    if (success) {
      prettyLog("green", empty ? "No Examples To Run" : "All Tests Passed");
      process.exitCode = 0;
      process.exit(0);
    } else {
      prettyLog("red", "Tests Failed or an Error Occured");
      if (error) console.log(error);

      if (failedExamples)
        prettyLog(
          "red",
          `Failed Examples: \n${failedExamples
            .map(({ name }) => `  |-- ${name}`)
            .join("\n")}`,
        );

      process.exitCode = 1;
      process.exit(1);
    }
  })
  .catch((error) => {
    prettyLog("red", "Tests Failed or an Error Occured");
    if (error) console.log(error);
    process.exitCode = 1;
    process.exit(1);
  });