Skip to content

Commit

Permalink
test: add test case for JsSdkTest-1 (#2720)
Browse files Browse the repository at this point in the history
Co-authored-by: hung-nguyen <hung-nguyen@cybozu.vn>
  • Loading branch information
tuanphamcybozu and hung-cybo authored May 3, 2024
1 parent aa4d2b3 commit 57af1c7
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 1 deletion.
84 changes: 84 additions & 0 deletions packages/create-plugin/__e2e__/e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import assert from "assert";
import type { QuestionInput } from "./utils/executeCommand";
import { executeCommandWithInteractiveInput } from "./utils/executeCommand";
import {
CREATE_PLUGIN_COMMAND,
DEFAULT_ANSWER,
ANSWER_NO,
} from "./utils/constants";
import path from "path";
import { generateWorkingDir } from "./utils/generateWorkingDir";
import fs from "fs";
import { rimrafSync } from "rimraf";
import {
assertObjectIncludes,
readPluginManifestJson,
} from "./utils/verification";
import { getBoundMessage } from "../src/messages";

describe("create-plugin", function () {
let workingDir: string;
beforeEach(() => {
workingDir = generateWorkingDir();
console.log(`Working directory: ${workingDir}`);
});

it("#JsSdkTest-1 Should able to create a plugin with specified output directory and required options successfully", async () => {
const m = getBoundMessage("en");
const outputDir = "test1";
const questionsInput: QuestionInput[] = [
{
question: m("Q_NameEn"),
answer: "test1-name",
},
{
question: m("Q_DescriptionEn"),
answer: "test1-description",
},
{
question: m("Q_SupportJa"),
answer: DEFAULT_ANSWER,
},
{
question: m("Q_SupportZh"),
answer: DEFAULT_ANSWER,
},
{
question: m("Q_websiteUrlEn"),
answer: DEFAULT_ANSWER,
},
{
question: m("Q_MobileSupport"),
answer: ANSWER_NO,
},
{
question: m("Q_enablePluginUploader"),
answer: ANSWER_NO,
},
];

const response = await executeCommandWithInteractiveInput({
command: CREATE_PLUGIN_COMMAND,
workingDir,
outputDir,
questionsInput,
});

assert(response.status === 0, "Failed to create plugin");

const pluginDir = path.resolve(workingDir, outputDir);
assert.ok(fs.existsSync(pluginDir), "plugin dir is not created.");

const actualManifestJson = readPluginManifestJson(pluginDir);
const expectedManifestJson = {
name: { en: "test1-name" },
description: { en: "test1-description" },
};
assertObjectIncludes(actualManifestJson, expectedManifestJson);
});

afterEach(() => {
rimrafSync(workingDir);
console.log(`Working directory ${workingDir} has been removed`);
});
});
6 changes: 6 additions & 0 deletions packages/create-plugin/__e2e__/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const DEFAULT_ANSWER = "";
export const ANSWER_YES = "Yes";
export const ANSWER_NO = "No";

export const CREATE_PLUGIN_COMMAND = "create-plugin";
export const CREATE_KINTONE_PLUGIN_COMMAND = "create-kintone-plugin";
147 changes: 147 additions & 0 deletions packages/create-plugin/__e2e__/utils/executeCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { spawn } from "child_process";
import fs from "fs";
import path from "path";

export type Response = {
status: number;
stdout: Buffer;
stderr: Buffer;
error?: Error;
};

export type QuestionInput = {
question: string;
answer: string;
};

export const getCommands = (): { [key: string]: string } => {
const packageJson = JSON.parse(
fs.readFileSync(path.resolve("package.json"), "utf8"),
);
return Object.fromEntries(
Object.entries(packageJson.bin).map(([command, relativePath]) => {
return [command, path.resolve(relativePath as string)];
}),
);
};

export const execCommand = (
command: string,
args: string,
options: {
env?: { [key: string]: string };
cwd?: string;
},
) => {
return spawn(command, parseArgs(args, options?.env), {
stdio: ["pipe", "pipe", "pipe"],
env: options?.env ?? process.env,
cwd: options?.cwd ?? process.cwd(),
shell: true,
});
};

const parseArgs = (args: string, envVars?: { [key: string]: string }) => {
const replacedArgs = replaceTokenWithEnvVars(args, envVars).match(
/(?:[^\s'"]+|"[^"]*"|'[^']*')+/g,
);

if (!replacedArgs) {
throw new Error("Failed to parse command arguments.");
}

return replacedArgs.map((arg) => arg.replace(/^['"]|['"]$/g, ""));
};

const replaceTokenWithEnvVars = (
input: string,
envVars: { [key: string]: string } | undefined,
) =>
input
.replace(/\$\$[a-zA-Z0-9_]+/g, processEnvReplacer)
.replace(/\$[a-zA-Z0-9_]+/g, inputEnvReplacer(envVars));

export const executeCommandWithInteractiveInput = async (options: {
command: string;
workingDir: string;
outputDir: string;
questionsInput: QuestionInput[];
commandArguments?: string;
}) => {
const { command, workingDir, outputDir, questionsInput, commandArguments } =
options;
const commands = getCommands();
if (!commands[command]) {
throw new Error(`Command ${command} not found.`);
}

const commandString = `${commands[command]} ${commandArguments || ""} ${outputDir}`;
const cliProcess = execCommand("node", commandString, {
cwd: workingDir,
});

let stdout: Buffer;
let stderr: Buffer;

const cliExitPromise = new Promise<Response>((resolve, reject) => {
cliProcess.on("exit", (code: number) => {
resolve({
status: code,
stdout: stdout ?? Buffer.from(""),
stderr: stderr ?? Buffer.from(""),
});
});
cliProcess.on("error", (error) => {
reject({
error,
});
});
});

let currentStep = 0;
cliProcess.stdout.on("data", async (data: Buffer) => {
const output = data.toString();
stdout = stdout ? Buffer.concat([stdout, data]) : data;
if (currentStep === questionsInput.length || !questionsInput[currentStep]) {
cliProcess.stdin.end();
return;
}

if (output.includes(questionsInput[currentStep].question)) {
cliProcess.stdin.write(questionsInput[currentStep].answer);
cliProcess.stdin.write("\n");
currentStep++;
}
});

cliProcess.stderr.on("data", async (data: Buffer) => {
stderr = data;
cliProcess.stdin.end();
});

return cliExitPromise;
};

const processEnvReplacer = (substring: string) => {
const key = substring.replace("$$", "");
const value = process.env[key];
if (value === undefined) {
throw new Error(`The env variable in process.env is missing: ${key}`);
}
return value;
};

const inputEnvReplacer = (envVars: { [key: string]: string } | undefined) => {
return (substring: string) => {
if (!envVars) {
return substring;
}

const key = substring.replace("$", "");
const value = envVars[key];
if (value === undefined) {
throw new Error(`The env variable in input parameter is missing: ${key}`);
}
return value;
};
};
9 changes: 9 additions & 0 deletions packages/create-plugin/__e2e__/utils/generateWorkingDir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import fs from "fs";
import path from "path";
import os from "os";

export const generateWorkingDir = (): string => {
return fs.mkdtempSync(
path.join(os.tmpdir(), `create-plugin-e2e-test-${new Date().valueOf()}-`),
);
};
12 changes: 12 additions & 0 deletions packages/create-plugin/__e2e__/utils/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const generateRandomString = (length: number) => {
let result = "";
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
let counter = 0;
while (counter < length) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
counter += 1;
}
return result;
};
35 changes: 35 additions & 0 deletions packages/create-plugin/__e2e__/utils/verification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import fs from "fs";
import path from "path";
import assert from "assert";

type PluginTemplate = "minimum" | "modern";

export const readPluginManifestJson = (
pluginDir: string,
template: PluginTemplate = "minimum",
) => {
try {
const manifestJsonPath = path.resolve(
pluginDir,
template === "modern" ? "plugin" : "src",
"manifest.json",
);

const fileContent = fs.readFileSync(manifestJsonPath, "utf8");
return JSON.parse(fileContent);
} catch (e) {
throw new Error(`Failed to read manifest.json\n${e}`);
}
};

export const assertObjectIncludes = (
actual: { [key: PropertyKey]: unknown },
expected: { [key: PropertyKey]: unknown },
message?: string | Error,
) => {
for (const key in expected) {
if (Object.prototype.hasOwnProperty.call(expected, key)) {
assert.deepStrictEqual(actual[key], expected[key], message);
}
}
};
8 changes: 8 additions & 0 deletions packages/create-plugin/jest.e2e.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('@jest/types').Config.InitialOptions} */
const config = {
roots: ["<rootDir>"],
testRegex: "/__e2e__/.*\\.test\\.ts$",
testEnvironment: "node",
testTimeout: 120000,
};
module.exports = config;
3 changes: 2 additions & 1 deletion packages/create-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
"e2e": "cross-env NODE_ENV=e2e jest --testPathPattern=/__tests__/e2e\\.test\\.ts$",
"test": "pnpm unittest",
"test:all": "run-p test e2e",
"test:ci": "pnpm test:all"
"test:ci": "pnpm test:all",
"test:e2e": "cross-env NODE_ENV=e2e jest --config=jest.e2e.config.js"
},
"keywords": [
"kintone"
Expand Down

0 comments on commit 57af1c7

Please sign in to comment.