-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: add test case for JsSdkTest-1 (#2720)
Co-authored-by: hung-nguyen <hung-nguyen@cybozu.vn>
- Loading branch information
1 parent
aa4d2b3
commit 57af1c7
Showing
8 changed files
with
303 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()}-`), | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters