chore: replace eslint & prettier w/ biomejs (#21)
* chore: replace eslint & prettier w/ biomejs * fix syntax error in ci.yml workflow * ensure that build CI jobs only run if check job succeeds to save resources
This commit is contained in:
parent
73135dd4b5
commit
5118a8174b
44 changed files with 2408 additions and 5691 deletions
|
|
@ -3,149 +3,149 @@
|
|||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { promises as fs } from "fs";
|
||||
import * as path from "path";
|
||||
import { promises as fs } from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
import {
|
||||
npmCmd,
|
||||
runCommand,
|
||||
WEBSNACKS_BIN_PATH,
|
||||
WEBSNACKS_REPO_ROOT,
|
||||
withTempDir,
|
||||
WEBSNACKS_BIN_PATH,
|
||||
WEBSNACKS_REPO_ROOT,
|
||||
npmCmd,
|
||||
runCommand,
|
||||
withTempDir,
|
||||
} from "../helpers/e2e";
|
||||
import { testSuite } from "../lib";
|
||||
|
||||
testSuite("build command", ({ test }) => {
|
||||
test("runs without throwing error", async () => {
|
||||
await withTempDir(async (tempDirPath) => {
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "tsconfig.json"),
|
||||
JSON.stringify({
|
||||
compilerOptions: {
|
||||
esModuleInterop: true,
|
||||
module: "CommonJS",
|
||||
moduleResolution: "node",
|
||||
jsx: "react",
|
||||
jsxFactory: "createElement",
|
||||
target: "ES2018",
|
||||
lib: ["ES2018"],
|
||||
strict: true,
|
||||
noUnusedLocals: true,
|
||||
noUnusedParameters: true,
|
||||
noImplicitReturns: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
},
|
||||
include: ["components/**/*", "pages/**/*"],
|
||||
}),
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "websnacks.ts"),
|
||||
`
|
||||
test("runs without throwing error", async () => {
|
||||
await withTempDir(async (tempDirPath) => {
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "tsconfig.json"),
|
||||
JSON.stringify({
|
||||
compilerOptions: {
|
||||
esModuleInterop: true,
|
||||
module: "CommonJS",
|
||||
moduleResolution: "node",
|
||||
jsx: "react",
|
||||
jsxFactory: "createElement",
|
||||
target: "ES2018",
|
||||
lib: ["ES2018"],
|
||||
strict: true,
|
||||
noUnusedLocals: true,
|
||||
noUnusedParameters: true,
|
||||
noImplicitReturns: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
},
|
||||
include: ["components/**/*", "pages/**/*"],
|
||||
}),
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "websnacks.ts"),
|
||||
`
|
||||
import { Config } from "websnacks";
|
||||
const config: Config = {
|
||||
watch: [],
|
||||
};
|
||||
export = config;
|
||||
`,
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
const pagesPath = path.join(tempDirPath, "pages");
|
||||
await fs.mkdir(pagesPath);
|
||||
await fs.writeFile(
|
||||
path.join(pagesPath, "index.tsx"),
|
||||
`
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
const pagesPath = path.join(tempDirPath, "pages");
|
||||
await fs.mkdir(pagesPath);
|
||||
await fs.writeFile(
|
||||
path.join(pagesPath, "index.tsx"),
|
||||
`
|
||||
import { createElement } from "websnacks";
|
||||
export const page = () => <html />;
|
||||
`,
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "package.json"),
|
||||
JSON.stringify({
|
||||
devDependencies: {
|
||||
websnacks: `file:${WEBSNACKS_REPO_ROOT}`,
|
||||
},
|
||||
}),
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
await runCommand(npmCmd, ["install", "--silent"], {
|
||||
cwd: tempDirPath,
|
||||
}).complete;
|
||||
const cmd = runCommand(
|
||||
"node",
|
||||
[WEBSNACKS_BIN_PATH, "-r", "ts-node/register", "build"],
|
||||
{
|
||||
cwd: tempDirPath,
|
||||
},
|
||||
);
|
||||
await cmd.complete;
|
||||
});
|
||||
});
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "package.json"),
|
||||
JSON.stringify({
|
||||
devDependencies: {
|
||||
websnacks: `file:${WEBSNACKS_REPO_ROOT}`,
|
||||
},
|
||||
}),
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
await runCommand(npmCmd, ["install", "--silent"], {
|
||||
cwd: tempDirPath,
|
||||
}).complete;
|
||||
const cmd = runCommand(
|
||||
"node",
|
||||
[WEBSNACKS_BIN_PATH, "-r", "ts-node/register", "build"],
|
||||
{
|
||||
cwd: tempDirPath,
|
||||
},
|
||||
);
|
||||
await cmd.complete;
|
||||
});
|
||||
});
|
||||
|
||||
test("works without config file", async () => {
|
||||
await withTempDir(async (tempDirPath) => {
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "tsconfig.json"),
|
||||
JSON.stringify({
|
||||
compilerOptions: {
|
||||
esModuleInterop: true,
|
||||
module: "CommonJS",
|
||||
moduleResolution: "node",
|
||||
jsx: "react",
|
||||
jsxFactory: "createElement",
|
||||
target: "ES2018",
|
||||
lib: ["ES2018"],
|
||||
strict: true,
|
||||
noUnusedLocals: true,
|
||||
noUnusedParameters: true,
|
||||
noImplicitReturns: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
},
|
||||
include: ["components/**/*", "pages/**/*"],
|
||||
}),
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
const pagesPath = path.join(tempDirPath, "pages");
|
||||
await fs.mkdir(pagesPath);
|
||||
await fs.writeFile(
|
||||
path.join(pagesPath, "index.tsx"),
|
||||
`
|
||||
test("works without config file", async () => {
|
||||
await withTempDir(async (tempDirPath) => {
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "tsconfig.json"),
|
||||
JSON.stringify({
|
||||
compilerOptions: {
|
||||
esModuleInterop: true,
|
||||
module: "CommonJS",
|
||||
moduleResolution: "node",
|
||||
jsx: "react",
|
||||
jsxFactory: "createElement",
|
||||
target: "ES2018",
|
||||
lib: ["ES2018"],
|
||||
strict: true,
|
||||
noUnusedLocals: true,
|
||||
noUnusedParameters: true,
|
||||
noImplicitReturns: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
},
|
||||
include: ["components/**/*", "pages/**/*"],
|
||||
}),
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
const pagesPath = path.join(tempDirPath, "pages");
|
||||
await fs.mkdir(pagesPath);
|
||||
await fs.writeFile(
|
||||
path.join(pagesPath, "index.tsx"),
|
||||
`
|
||||
import { createElement } from "websnacks";
|
||||
export const page = () => <html />;
|
||||
`,
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "package.json"),
|
||||
JSON.stringify({
|
||||
devDependencies: {
|
||||
websnacks: `file:${WEBSNACKS_REPO_ROOT}`,
|
||||
},
|
||||
}),
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
await runCommand(npmCmd, ["install", "--silent"], {
|
||||
cwd: tempDirPath,
|
||||
}).complete;
|
||||
const cmd = runCommand(
|
||||
"node",
|
||||
[WEBSNACKS_BIN_PATH, "-r", "ts-node/register", "build"],
|
||||
{
|
||||
cwd: tempDirPath,
|
||||
},
|
||||
);
|
||||
await cmd.complete;
|
||||
});
|
||||
});
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "package.json"),
|
||||
JSON.stringify({
|
||||
devDependencies: {
|
||||
websnacks: `file:${WEBSNACKS_REPO_ROOT}`,
|
||||
},
|
||||
}),
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
await runCommand(npmCmd, ["install", "--silent"], {
|
||||
cwd: tempDirPath,
|
||||
}).complete;
|
||||
const cmd = runCommand(
|
||||
"node",
|
||||
[WEBSNACKS_BIN_PATH, "-r", "ts-node/register", "build"],
|
||||
{
|
||||
cwd: tempDirPath,
|
||||
},
|
||||
);
|
||||
await cmd.complete;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
270
test/e2e/dev.tsx
270
test/e2e/dev.tsx
|
|
@ -3,158 +3,158 @@
|
|||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { promises as fs } from "fs";
|
||||
import * as path from "path";
|
||||
import { promises as fs } from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
import {
|
||||
npmCmd,
|
||||
runCommand,
|
||||
wait,
|
||||
WEBSNACKS_BIN_PATH,
|
||||
WEBSNACKS_REPO_ROOT,
|
||||
withTempDir,
|
||||
WEBSNACKS_BIN_PATH,
|
||||
WEBSNACKS_REPO_ROOT,
|
||||
npmCmd,
|
||||
runCommand,
|
||||
wait,
|
||||
withTempDir,
|
||||
} from "../helpers/e2e";
|
||||
import { testSuite } from "../lib";
|
||||
|
||||
testSuite("dev command", ({ test, expect }) => {
|
||||
test("starts without throwing error", async () => {
|
||||
await withTempDir(async (tempDirPath) => {
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "tsconfig.json"),
|
||||
JSON.stringify({
|
||||
compilerOptions: {
|
||||
esModuleInterop: true,
|
||||
module: "CommonJS",
|
||||
moduleResolution: "node",
|
||||
jsx: "react",
|
||||
jsxFactory: "createElement",
|
||||
target: "ES2018",
|
||||
lib: ["ES2018"],
|
||||
strict: true,
|
||||
noUnusedLocals: true,
|
||||
noUnusedParameters: true,
|
||||
noImplicitReturns: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
},
|
||||
include: ["components/**/*", "pages/**/*"],
|
||||
}),
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "websnacks.ts"),
|
||||
`
|
||||
test("starts without throwing error", async () => {
|
||||
await withTempDir(async (tempDirPath) => {
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "tsconfig.json"),
|
||||
JSON.stringify({
|
||||
compilerOptions: {
|
||||
esModuleInterop: true,
|
||||
module: "CommonJS",
|
||||
moduleResolution: "node",
|
||||
jsx: "react",
|
||||
jsxFactory: "createElement",
|
||||
target: "ES2018",
|
||||
lib: ["ES2018"],
|
||||
strict: true,
|
||||
noUnusedLocals: true,
|
||||
noUnusedParameters: true,
|
||||
noImplicitReturns: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
},
|
||||
include: ["components/**/*", "pages/**/*"],
|
||||
}),
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "websnacks.ts"),
|
||||
`
|
||||
import { Config } from "websnacks";
|
||||
const config: Config = {
|
||||
watch: [],
|
||||
};
|
||||
export = config;
|
||||
`,
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
const pagesPath = path.join(tempDirPath, "pages");
|
||||
await fs.mkdir(pagesPath);
|
||||
await fs.writeFile(
|
||||
path.join(pagesPath, "index.tsx"),
|
||||
`
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
const pagesPath = path.join(tempDirPath, "pages");
|
||||
await fs.mkdir(pagesPath);
|
||||
await fs.writeFile(
|
||||
path.join(pagesPath, "index.tsx"),
|
||||
`
|
||||
import { createElement } from "websnacks";
|
||||
export const page = () => <html />;
|
||||
`,
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "package.json"),
|
||||
JSON.stringify({
|
||||
devDependencies: {
|
||||
websnacks: `file:${WEBSNACKS_REPO_ROOT}`,
|
||||
},
|
||||
}),
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
await runCommand(npmCmd, ["install", "--silent"], {
|
||||
cwd: tempDirPath,
|
||||
}).complete;
|
||||
const cmd = runCommand(
|
||||
"node",
|
||||
[WEBSNACKS_BIN_PATH, "-r", "ts-node/register", "dev"],
|
||||
{
|
||||
cwd: tempDirPath,
|
||||
},
|
||||
);
|
||||
// FIXME: This test is a bit brittle due to relying on timeouts.
|
||||
await wait(10_000);
|
||||
cmd.process.kill();
|
||||
const stdout = await cmd.complete;
|
||||
expect(stdout).toStartWith("Listening at");
|
||||
});
|
||||
});
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "package.json"),
|
||||
JSON.stringify({
|
||||
devDependencies: {
|
||||
websnacks: `file:${WEBSNACKS_REPO_ROOT}`,
|
||||
},
|
||||
}),
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
await runCommand(npmCmd, ["install", "--silent"], {
|
||||
cwd: tempDirPath,
|
||||
}).complete;
|
||||
const cmd = runCommand(
|
||||
"node",
|
||||
[WEBSNACKS_BIN_PATH, "-r", "ts-node/register", "dev"],
|
||||
{
|
||||
cwd: tempDirPath,
|
||||
},
|
||||
);
|
||||
// FIXME: This test is a bit brittle due to relying on timeouts.
|
||||
await wait(10_000);
|
||||
cmd.process.kill();
|
||||
const stdout = await cmd.complete;
|
||||
expect(stdout).toStartWith("Listening at");
|
||||
});
|
||||
});
|
||||
|
||||
test("works without config file", async () => {
|
||||
await withTempDir(async (tempDirPath) => {
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "tsconfig.json"),
|
||||
JSON.stringify({
|
||||
compilerOptions: {
|
||||
esModuleInterop: true,
|
||||
module: "CommonJS",
|
||||
moduleResolution: "node",
|
||||
jsx: "react",
|
||||
jsxFactory: "createElement",
|
||||
target: "ES2018",
|
||||
lib: ["ES2018"],
|
||||
strict: true,
|
||||
noUnusedLocals: true,
|
||||
noUnusedParameters: true,
|
||||
noImplicitReturns: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
},
|
||||
include: ["components/**/*", "pages/**/*"],
|
||||
}),
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
const pagesPath = path.join(tempDirPath, "pages");
|
||||
await fs.mkdir(pagesPath);
|
||||
await fs.writeFile(
|
||||
path.join(pagesPath, "index.tsx"),
|
||||
`
|
||||
test("works without config file", async () => {
|
||||
await withTempDir(async (tempDirPath) => {
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "tsconfig.json"),
|
||||
JSON.stringify({
|
||||
compilerOptions: {
|
||||
esModuleInterop: true,
|
||||
module: "CommonJS",
|
||||
moduleResolution: "node",
|
||||
jsx: "react",
|
||||
jsxFactory: "createElement",
|
||||
target: "ES2018",
|
||||
lib: ["ES2018"],
|
||||
strict: true,
|
||||
noUnusedLocals: true,
|
||||
noUnusedParameters: true,
|
||||
noImplicitReturns: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
},
|
||||
include: ["components/**/*", "pages/**/*"],
|
||||
}),
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
const pagesPath = path.join(tempDirPath, "pages");
|
||||
await fs.mkdir(pagesPath);
|
||||
await fs.writeFile(
|
||||
path.join(pagesPath, "index.tsx"),
|
||||
`
|
||||
import { createElement } from "websnacks";
|
||||
export const page = () => <html />;
|
||||
`,
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "package.json"),
|
||||
JSON.stringify({
|
||||
devDependencies: {
|
||||
websnacks: `file:${WEBSNACKS_REPO_ROOT}`,
|
||||
},
|
||||
}),
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
await runCommand(npmCmd, ["install", "--silent"], {
|
||||
cwd: tempDirPath,
|
||||
}).complete;
|
||||
const cmd = runCommand(
|
||||
"node",
|
||||
[WEBSNACKS_BIN_PATH, "-r", "ts-node/register", "dev"],
|
||||
{
|
||||
cwd: tempDirPath,
|
||||
},
|
||||
);
|
||||
// FIXME: This test is a bit brittle due to relying on timeouts.
|
||||
await wait(10_000);
|
||||
cmd.process.kill();
|
||||
const stdout = await cmd.complete;
|
||||
expect(stdout).toStartWith("Listening at");
|
||||
});
|
||||
});
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempDirPath, "package.json"),
|
||||
JSON.stringify({
|
||||
devDependencies: {
|
||||
websnacks: `file:${WEBSNACKS_REPO_ROOT}`,
|
||||
},
|
||||
}),
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
await runCommand(npmCmd, ["install", "--silent"], {
|
||||
cwd: tempDirPath,
|
||||
}).complete;
|
||||
const cmd = runCommand(
|
||||
"node",
|
||||
[WEBSNACKS_BIN_PATH, "-r", "ts-node/register", "dev"],
|
||||
{
|
||||
cwd: tempDirPath,
|
||||
},
|
||||
);
|
||||
// FIXME: This test is a bit brittle due to relying on timeouts.
|
||||
await wait(10_000);
|
||||
cmd.process.kill();
|
||||
const stdout = await cmd.complete;
|
||||
expect(stdout).toStartWith("Listening at");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { ChildProcess, spawn } from "child_process";
|
||||
import { promises as fs } from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import { type ChildProcess, spawn } from "node:child_process";
|
||||
import { promises as fs } from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
/**
|
||||
* Set a timeout and wait for at least the specified number of milliseconds,
|
||||
|
|
@ -15,9 +15,9 @@ import * as path from "path";
|
|||
* @param timeMs Time in milliseconds to wait.
|
||||
*/
|
||||
export const wait = async (timeMs: number): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(), timeMs);
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(), timeMs);
|
||||
});
|
||||
};
|
||||
|
||||
const TEMP_PATH = path.resolve(__dirname, "..", "..", ".temp");
|
||||
|
|
@ -36,15 +36,15 @@ const TEMP_PATH = path.resolve(__dirname, "..", "..", ".temp");
|
|||
* directory path as its only argument.
|
||||
*/
|
||||
export const withTempDir = async (
|
||||
op: (tempDirPath: string) => Promise<void> | void,
|
||||
op: (tempDirPath: string) => Promise<void> | void,
|
||||
): Promise<void> => {
|
||||
await fs.mkdir(TEMP_PATH, { recursive: true });
|
||||
const tempDirPath = await fs.mkdtemp(`${TEMP_PATH}/`);
|
||||
try {
|
||||
await op(tempDirPath);
|
||||
} catch (error) {
|
||||
throw new Error(`(${tempDirPath}): ${error}`);
|
||||
}
|
||||
await fs.mkdir(TEMP_PATH, { recursive: true });
|
||||
const tempDirPath = await fs.mkdtemp(`${TEMP_PATH}/`);
|
||||
try {
|
||||
await op(tempDirPath);
|
||||
} catch (error) {
|
||||
throw new Error(`(${tempDirPath}): ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -55,47 +55,47 @@ export const WEBSNACKS_REPO_ROOT = path.resolve(__dirname, "..", "..");
|
|||
* Fully resolved path to the websnacks CLI script in this repository.
|
||||
*/
|
||||
export const WEBSNACKS_BIN_PATH = path.join(
|
||||
WEBSNACKS_REPO_ROOT,
|
||||
"bin",
|
||||
"websnacks.js",
|
||||
WEBSNACKS_REPO_ROOT,
|
||||
"bin",
|
||||
"websnacks.js",
|
||||
);
|
||||
|
||||
/**
|
||||
* A handle to an asynchronous shell command run in a subprocess.
|
||||
*/
|
||||
export interface AsyncCommand {
|
||||
/**
|
||||
* Promise that resolves with the stdout of the subprocess once the
|
||||
* subprocess exits with a zero-code.
|
||||
*
|
||||
* The promise rejects if the subprocess exits with a non-zero code, the
|
||||
* subprocess writes to its stderr, or the command failed to spawn.
|
||||
*/
|
||||
complete: Promise<string>;
|
||||
/**
|
||||
* Handle to to child process for event-based process manipulation.
|
||||
*/
|
||||
process: ChildProcess;
|
||||
/**
|
||||
* Promise that resolves with the stdout of the subprocess once the
|
||||
* subprocess exits with a zero-code.
|
||||
*
|
||||
* The promise rejects if the subprocess exits with a non-zero code, the
|
||||
* subprocess writes to its stderr, or the command failed to spawn.
|
||||
*/
|
||||
complete: Promise<string>;
|
||||
/**
|
||||
* Handle to to child process for event-based process manipulation.
|
||||
*/
|
||||
process: ChildProcess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used to configure {@link runCommand}.
|
||||
*/
|
||||
export interface CliOptions {
|
||||
/**
|
||||
* Working directory where the command should be run. Defaults to the
|
||||
* current working directory.
|
||||
*/
|
||||
cwd?: string;
|
||||
/**
|
||||
* Timeout in milliseconds after which a command that hasn't exited will
|
||||
* reject the promise and kill the subprocess.
|
||||
*/
|
||||
timeoutMs?: number;
|
||||
/**
|
||||
* Working directory where the command should be run. Defaults to the
|
||||
* current working directory.
|
||||
*/
|
||||
cwd?: string;
|
||||
/**
|
||||
* Timeout in milliseconds after which a command that hasn't exited will
|
||||
* reject the promise and kill the subprocess.
|
||||
*/
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_CLI_OPTIONS = {
|
||||
timeoutMs: 15_000,
|
||||
timeoutMs: 15_000,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -111,60 +111,58 @@ const DEFAULT_CLI_OPTIONS = {
|
|||
* @returns Command object for handling in client code.
|
||||
*/
|
||||
export const runCommand = (
|
||||
command: string,
|
||||
args: string[] = [],
|
||||
options?: CliOptions,
|
||||
command: string,
|
||||
args: string[] = [],
|
||||
options?: CliOptions,
|
||||
): AsyncCommand => {
|
||||
const optionsWithDefaults = { ...DEFAULT_CLI_OPTIONS, ...options };
|
||||
const process = spawn(command, args, {
|
||||
...optionsWithDefaults,
|
||||
stdio: "pipe",
|
||||
});
|
||||
const complete = new Promise<string>((resolve, reject) => {
|
||||
let threwError = false;
|
||||
const optionsWithDefaults = { ...DEFAULT_CLI_OPTIONS, ...options };
|
||||
const process = spawn(command, args, {
|
||||
...optionsWithDefaults,
|
||||
stdio: "pipe",
|
||||
});
|
||||
const complete = new Promise<string>((resolve, reject) => {
|
||||
let threwError = false;
|
||||
|
||||
let stdout = "";
|
||||
process.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
process.stderr.on("data", (data) => {
|
||||
threwError = true;
|
||||
process.kill();
|
||||
reject(new Error(`command output to stderr: ${data.toString()}`));
|
||||
});
|
||||
let stdout = "";
|
||||
process.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
process.stderr.on("data", (data) => {
|
||||
threwError = true;
|
||||
process.kill();
|
||||
reject(new Error(`command output to stderr: ${data.toString()}`));
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
threwError = true;
|
||||
process.kill();
|
||||
reject(
|
||||
new Error(
|
||||
`max timeout of ${optionsWithDefaults.timeoutMs}ms reached`,
|
||||
),
|
||||
);
|
||||
}, optionsWithDefaults.timeoutMs);
|
||||
process.on("exit", (code) => {
|
||||
if (threwError) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
if (code !== null && code !== 0) {
|
||||
reject(new Error(`command exited with non-zero code: ${code}`));
|
||||
return;
|
||||
}
|
||||
resolve(stdout);
|
||||
});
|
||||
process.on("error", (error) => {
|
||||
clearTimeout(timer);
|
||||
if (!threwError) {
|
||||
reject(new Error(`command errored: ${error}`));
|
||||
threwError = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
return {
|
||||
complete,
|
||||
process,
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
threwError = true;
|
||||
process.kill();
|
||||
reject(
|
||||
new Error(`max timeout of ${optionsWithDefaults.timeoutMs}ms reached`),
|
||||
);
|
||||
}, optionsWithDefaults.timeoutMs);
|
||||
process.on("exit", (code) => {
|
||||
if (threwError) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
if (code !== null && code !== 0) {
|
||||
reject(new Error(`command exited with non-zero code: ${code}`));
|
||||
return;
|
||||
}
|
||||
resolve(stdout);
|
||||
});
|
||||
process.on("error", (error) => {
|
||||
clearTimeout(timer);
|
||||
if (!threwError) {
|
||||
reject(new Error(`command errored: ${error}`));
|
||||
threwError = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
return {
|
||||
complete,
|
||||
process,
|
||||
};
|
||||
};
|
||||
|
||||
export const npmCmd = os.platform() === "win32" ? "npm.cmd" : "npm";
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@
|
|||
import { areEqual, displayValue, matches } from "./utils";
|
||||
|
||||
class ExpectError extends Error {
|
||||
public constructor(reason: string, expected: unknown, actual: unknown) {
|
||||
super(
|
||||
`${reason}\n` +
|
||||
`\texpected: ${displayValue(expected)}\n` +
|
||||
`\tactual : ${displayValue(actual)}`,
|
||||
);
|
||||
}
|
||||
public constructor(reason: string, expected: unknown, actual: unknown) {
|
||||
super(
|
||||
`${reason}\n` +
|
||||
`\texpected: ${displayValue(expected)}\n` +
|
||||
`\tactual : ${displayValue(actual)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -24,138 +24,134 @@ class ExpectError extends Error {
|
|||
* Expect.
|
||||
*/
|
||||
export class Expect<T> {
|
||||
protected readonly value: T;
|
||||
protected readonly value: T;
|
||||
|
||||
/**
|
||||
* Create a new expectation around a value.
|
||||
*
|
||||
* @param value Value to place assertions upon.
|
||||
*/
|
||||
public constructor(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
/**
|
||||
* Create a new expectation around a value.
|
||||
*
|
||||
* @param value Value to place assertions upon.
|
||||
*/
|
||||
public constructor(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect the value to equal an expected value.
|
||||
*
|
||||
* Note that strict equality checking is used for primitives and structural
|
||||
* equality is used for objects.
|
||||
*
|
||||
* @param expected Expected value.
|
||||
*
|
||||
* @throws ExpectError If the actual value does not equal the expected value.
|
||||
*/
|
||||
public toEqual(expected: T): void {
|
||||
if (!areEqual(this.value, expected)) {
|
||||
throw new ExpectError(
|
||||
`value does not equal expected`,
|
||||
expected,
|
||||
this.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Expect the value to equal an expected value.
|
||||
*
|
||||
* Note that strict equality checking is used for primitives and structural
|
||||
* equality is used for objects.
|
||||
*
|
||||
* @param expected Expected value.
|
||||
*
|
||||
* @throws ExpectError If the actual value does not equal the expected value.
|
||||
*/
|
||||
public toEqual(expected: T): void {
|
||||
if (!areEqual(this.value, expected)) {
|
||||
throw new ExpectError(
|
||||
"value does not equal expected",
|
||||
expected,
|
||||
this.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* String-specific Expect assertions.
|
||||
*/
|
||||
export class StringExpect extends Expect<string> {
|
||||
/**
|
||||
* Expect the string value to match a RegExp pattern.
|
||||
*
|
||||
* @param pattern Regular expression to match against.
|
||||
*
|
||||
* @throws ExpectError If the actual value does not match the expected
|
||||
* RegExp pattern.
|
||||
*/
|
||||
public toMatch(pattern: RegExp): void {
|
||||
if (!this.value.match(pattern)) {
|
||||
throw new ExpectError(
|
||||
`value does not match expected pattern`,
|
||||
pattern,
|
||||
this.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Expect the string value to match a RegExp pattern.
|
||||
*
|
||||
* @param pattern Regular expression to match against.
|
||||
*
|
||||
* @throws ExpectError If the actual value does not match the expected
|
||||
* RegExp pattern.
|
||||
*/
|
||||
public toMatch(pattern: RegExp): void {
|
||||
if (!this.value.match(pattern)) {
|
||||
throw new ExpectError(
|
||||
"value does not match expected pattern",
|
||||
pattern,
|
||||
this.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect the string value to start with a particular prefix.
|
||||
*
|
||||
* @param prefix Prefix that the string is expected to start with.
|
||||
*
|
||||
* @throws ExpectError If the actual value does not start with the expected
|
||||
* prefix.
|
||||
*/
|
||||
public toStartWith(prefix: string): void {
|
||||
if (!this.value.startsWith(prefix)) {
|
||||
throw new ExpectError(
|
||||
`value does not start with expected prefix`,
|
||||
prefix,
|
||||
this.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Expect the string value to start with a particular prefix.
|
||||
*
|
||||
* @param prefix Prefix that the string is expected to start with.
|
||||
*
|
||||
* @throws ExpectError If the actual value does not start with the expected
|
||||
* prefix.
|
||||
*/
|
||||
public toStartWith(prefix: string): void {
|
||||
if (!this.value.startsWith(prefix)) {
|
||||
throw new ExpectError(
|
||||
"value does not start with expected prefix",
|
||||
prefix,
|
||||
this.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect the string value to end with a particular suffix.
|
||||
*
|
||||
* @param suffix Suffix that the string is expected to end with.
|
||||
*
|
||||
* @throws ExpectError If the actual value does not end with the expected
|
||||
* suffix.
|
||||
*/
|
||||
public toEndWith(suffix: string): void {
|
||||
if (!this.value.endsWith(suffix)) {
|
||||
throw new ExpectError(
|
||||
`value does not end with expected suffix`,
|
||||
suffix,
|
||||
this.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Expect the string value to end with a particular suffix.
|
||||
*
|
||||
* @param suffix Suffix that the string is expected to end with.
|
||||
*
|
||||
* @throws ExpectError If the actual value does not end with the expected
|
||||
* suffix.
|
||||
*/
|
||||
public toEndWith(suffix: string): void {
|
||||
if (!this.value.endsWith(suffix)) {
|
||||
throw new ExpectError(
|
||||
"value does not end with expected suffix",
|
||||
suffix,
|
||||
this.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function-specific Expect assertions.
|
||||
*/
|
||||
export class FunctionExpect<T> extends Expect<() => T> {
|
||||
/**
|
||||
* Expect the function to throw an Error with error message matching a
|
||||
* string or pattern.
|
||||
*
|
||||
* @param pattern String that exactly matches the error message or RegExp
|
||||
* that should match the error message.
|
||||
*
|
||||
* @throws ExpectError If the function does not throw an error, throws a
|
||||
* non-Error value, or throws an Error whose message does not match
|
||||
* the expected pattern.
|
||||
*/
|
||||
public toThrowErrorMatching(pattern: string | RegExp): void {
|
||||
try {
|
||||
this.value();
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error)) {
|
||||
throw new ExpectError(
|
||||
`function threw non-Error value`,
|
||||
pattern,
|
||||
error,
|
||||
);
|
||||
}
|
||||
if (!matches(error.message, pattern)) {
|
||||
throw new ExpectError(
|
||||
`thrown Error's message does not match pattern`,
|
||||
pattern,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw new ExpectError(
|
||||
`function did not throw expected error`,
|
||||
pattern,
|
||||
null,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Expect the function to throw an Error with error message matching a
|
||||
* string or pattern.
|
||||
*
|
||||
* @param pattern String that exactly matches the error message or RegExp
|
||||
* that should match the error message.
|
||||
*
|
||||
* @throws ExpectError If the function does not throw an error, throws a
|
||||
* non-Error value, or throws an Error whose message does not match
|
||||
* the expected pattern.
|
||||
*/
|
||||
public toThrowErrorMatching(pattern: string | RegExp): void {
|
||||
try {
|
||||
this.value();
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error)) {
|
||||
throw new ExpectError("function threw non-Error value", pattern, error);
|
||||
}
|
||||
if (!matches(error.message, pattern)) {
|
||||
throw new ExpectError(
|
||||
`thrown Error's message does not match pattern`,
|
||||
pattern,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw new ExpectError(
|
||||
"function did not throw expected error",
|
||||
pattern,
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -202,11 +198,11 @@ export function expect<T>(fn: () => T): FunctionExpect<T>;
|
|||
* @param value Value to place expectations upon.
|
||||
*/
|
||||
export function expect(value: unknown): Expect<unknown> {
|
||||
if (typeof value === "string") {
|
||||
return new StringExpect(value);
|
||||
}
|
||||
if (typeof value === "function") {
|
||||
return new FunctionExpect(value as () => unknown);
|
||||
}
|
||||
return new Expect(value);
|
||||
if (typeof value === "string") {
|
||||
return new StringExpect(value);
|
||||
}
|
||||
if (typeof value === "function") {
|
||||
return new FunctionExpect(value as () => unknown);
|
||||
}
|
||||
return new Expect(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,68 +7,66 @@ import { expect } from "./expect";
|
|||
import { displayValue, shuffle } from "./utils";
|
||||
|
||||
interface Test {
|
||||
readonly name: string;
|
||||
readonly name: string;
|
||||
|
||||
runTest(): void | Promise<void>;
|
||||
runTest(): void | Promise<void>;
|
||||
}
|
||||
|
||||
type TestResult = {
|
||||
testName: string;
|
||||
testName: string;
|
||||
} & (
|
||||
| {
|
||||
result: "pass";
|
||||
}
|
||||
| {
|
||||
result: "fail";
|
||||
error: Error;
|
||||
}
|
||||
| {
|
||||
result: "pass";
|
||||
}
|
||||
| {
|
||||
result: "fail";
|
||||
error: Error;
|
||||
}
|
||||
);
|
||||
|
||||
const runTest = async (test: Test): Promise<TestResult> => {
|
||||
let result: TestResult;
|
||||
try {
|
||||
await test.runTest();
|
||||
result = {
|
||||
testName: test.name,
|
||||
result: "pass",
|
||||
};
|
||||
} catch (error) {
|
||||
result = {
|
||||
testName: test.name,
|
||||
result: "fail",
|
||||
error:
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error(
|
||||
`threw non-error object: ${displayValue(error)}`,
|
||||
),
|
||||
};
|
||||
}
|
||||
return result;
|
||||
let result: TestResult;
|
||||
try {
|
||||
await test.runTest();
|
||||
result = {
|
||||
testName: test.name,
|
||||
result: "pass",
|
||||
};
|
||||
} catch (error) {
|
||||
result = {
|
||||
testName: test.name,
|
||||
result: "fail",
|
||||
error:
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error(`threw non-error object: ${displayValue(error)}`),
|
||||
};
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Context object that is passed into a test suite definition.
|
||||
*/
|
||||
export interface TestSuiteContext {
|
||||
/**
|
||||
* Define a test in this test suite.
|
||||
*
|
||||
* Tests are functions that pass if they are executed and don't throw (or
|
||||
* that resolve for async tests), and that fail if they throw an error (or
|
||||
* reject for async tests).
|
||||
*
|
||||
* Note that tests are executed in a random order within a test suite in
|
||||
* order to prevent accidentally creating order dependencies between tests,
|
||||
* which can result in brittle tests and is a code smell that might indicate
|
||||
* that the code under test is also brittle.
|
||||
*/
|
||||
test: (name: string, def: () => void | Promise<void>) => void;
|
||||
/**
|
||||
* Expectation builder function used to build human-readable assertions and
|
||||
* errors.
|
||||
*/
|
||||
expect: typeof expect;
|
||||
/**
|
||||
* Define a test in this test suite.
|
||||
*
|
||||
* Tests are functions that pass if they are executed and don't throw (or
|
||||
* that resolve for async tests), and that fail if they throw an error (or
|
||||
* reject for async tests).
|
||||
*
|
||||
* Note that tests are executed in a random order within a test suite in
|
||||
* order to prevent accidentally creating order dependencies between tests,
|
||||
* which can result in brittle tests and is a code smell that might indicate
|
||||
* that the code under test is also brittle.
|
||||
*/
|
||||
test: (name: string, def: () => void | Promise<void>) => void;
|
||||
/**
|
||||
* Expectation builder function used to build human-readable assertions and
|
||||
* errors.
|
||||
*/
|
||||
expect: typeof expect;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -85,36 +83,36 @@ export interface TestSuiteContext {
|
|||
* @param def Function used to declare the tests
|
||||
*/
|
||||
export const testSuite = (
|
||||
suiteName: string,
|
||||
def: (ctx: TestSuiteContext) => void,
|
||||
suiteName: string,
|
||||
def: (ctx: TestSuiteContext) => void,
|
||||
): void => {
|
||||
const tests: Test[] = [];
|
||||
const test = (name: string, runTest: () => void | Promise<void>): void => {
|
||||
tests.push({ name, runTest });
|
||||
};
|
||||
def({ test, expect });
|
||||
const tests: Test[] = [];
|
||||
const test = (name: string, runTest: () => void | Promise<void>): void => {
|
||||
tests.push({ name, runTest });
|
||||
};
|
||||
def({ test, expect });
|
||||
|
||||
// Randomly shuffle the tests so that we can catch accidental order
|
||||
// dependencies.
|
||||
shuffle(tests);
|
||||
(async () => {
|
||||
const results = await Promise.all(tests.map((test) => runTest(test)));
|
||||
let passed = 0;
|
||||
for (const testResult of results) {
|
||||
if (testResult.result === "fail") {
|
||||
console.error(
|
||||
`[TEST FAILURE] "${suiteName}": "${testResult.testName}": ` +
|
||||
`${testResult.error.stack}\n`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
passed += 1;
|
||||
}
|
||||
console.info(
|
||||
`[TEST] suite "${suiteName}": ${passed} of ${tests.length} succeeded\n\n`,
|
||||
);
|
||||
if (passed < tests.length) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
})();
|
||||
// Randomly shuffle the tests so that we can catch accidental order
|
||||
// dependencies.
|
||||
shuffle(tests);
|
||||
(async () => {
|
||||
const results = await Promise.all(tests.map((test) => runTest(test)));
|
||||
let passed = 0;
|
||||
for (const testResult of results) {
|
||||
if (testResult.result === "fail") {
|
||||
console.error(
|
||||
`[TEST FAILURE] "${suiteName}": "${testResult.testName}": ` +
|
||||
`${testResult.error.stack}\n`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
passed += 1;
|
||||
}
|
||||
console.info(
|
||||
`[TEST] suite "${suiteName}": ${passed} of ${tests.length} succeeded\n\n`,
|
||||
);
|
||||
if (passed < tests.length) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,43 +9,43 @@
|
|||
* @param arr Array to shuffle.
|
||||
*/
|
||||
export const shuffle = <T>(arr: T[]): void => {
|
||||
let j: number;
|
||||
let x: T;
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
j = Math.floor(Math.random() * (i + 1));
|
||||
x = arr[i];
|
||||
arr[i] = arr[j];
|
||||
arr[j] = x;
|
||||
}
|
||||
let j: number;
|
||||
let x: T;
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
j = Math.floor(Math.random() * (i + 1));
|
||||
x = arr[i];
|
||||
arr[i] = arr[j];
|
||||
arr[j] = x;
|
||||
}
|
||||
};
|
||||
|
||||
const areArraysEqual = <T>(a: T[], b: T[]): boolean => {
|
||||
if (a.length != b.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (!areEqual(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (!areEqual(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const areObjectsEqual = <T extends Record<string, unknown>>(
|
||||
a: T,
|
||||
b: T,
|
||||
a: T,
|
||||
b: T,
|
||||
): boolean => {
|
||||
const aKeys = Object.keys(a) as Array<keyof T>;
|
||||
const bKeys = Object.keys(b) as Array<keyof T>;
|
||||
if (aKeys.length !== bKeys.length) {
|
||||
return false;
|
||||
}
|
||||
for (const key of aKeys) {
|
||||
if (!areEqual(a[key], b[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
const aKeys = Object.keys(a) as Array<keyof T>;
|
||||
const bKeys = Object.keys(b) as Array<keyof T>;
|
||||
if (aKeys.length !== bKeys.length) {
|
||||
return false;
|
||||
}
|
||||
for (const key of aKeys) {
|
||||
if (!areEqual(a[key], b[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -58,19 +58,19 @@ const areObjectsEqual = <T extends Record<string, unknown>>(
|
|||
* @return Whether the two values are structurally equal.
|
||||
*/
|
||||
export const areEqual = <T>(a: T, b: T): boolean => {
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
return areArraysEqual(a, b);
|
||||
}
|
||||
if (a instanceof RegExp && b instanceof RegExp) {
|
||||
return a.source === b.source;
|
||||
}
|
||||
if (typeof a === "object" && typeof b === "object") {
|
||||
return areObjectsEqual(
|
||||
a as Record<string, unknown>,
|
||||
b as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
return a === b;
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
return areArraysEqual(a, b);
|
||||
}
|
||||
if (a instanceof RegExp && b instanceof RegExp) {
|
||||
return a.source === b.source;
|
||||
}
|
||||
if (typeof a === "object" && typeof b === "object") {
|
||||
return areObjectsEqual(
|
||||
a as Record<string, unknown>,
|
||||
b as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
return a === b;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -84,10 +84,10 @@ export const areEqual = <T>(a: T, b: T): boolean => {
|
|||
* @param pattern String or RegExp pattern to match value against.
|
||||
*/
|
||||
export const matches = (value: string, pattern: string | RegExp): boolean => {
|
||||
if (typeof pattern === "string") {
|
||||
return value === pattern;
|
||||
}
|
||||
return pattern.test(value);
|
||||
if (typeof pattern === "string") {
|
||||
return value === pattern;
|
||||
}
|
||||
return pattern.test(value);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -102,11 +102,11 @@ export const matches = (value: string, pattern: string | RegExp): boolean => {
|
|||
* @return Rendered value to display.
|
||||
*/
|
||||
export const displayValue = (value: unknown): string => {
|
||||
if (value === undefined) {
|
||||
return "undefined";
|
||||
}
|
||||
if (value instanceof RegExp) {
|
||||
return value.toString();
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
if (value === undefined) {
|
||||
return "undefined";
|
||||
}
|
||||
if (value instanceof RegExp) {
|
||||
return value.toString();
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { fork } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { fork } from "node:child_process";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { shuffle } from "./lib/utils";
|
||||
|
||||
|
|
@ -14,10 +14,10 @@ const files = fs.readdirSync(TEST_SUITES_DIR);
|
|||
// Shuffle test suites to detect ordering dependencies between them.
|
||||
shuffle(files);
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(TEST_SUITES_DIR, file);
|
||||
fork(path.relative(process.cwd(), fullPath)).on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
const fullPath = path.join(TEST_SUITES_DIR, file);
|
||||
fork(path.relative(process.cwd(), fullPath)).on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { fork } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { fork } from "node:child_process";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { shuffle } from "./lib/utils";
|
||||
|
||||
|
|
@ -14,10 +14,10 @@ const files = fs.readdirSync(TEST_SUITES_DIR);
|
|||
// Shuffle test suites to detect ordering dependencies between them.
|
||||
shuffle(files);
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(TEST_SUITES_DIR, file);
|
||||
fork(path.relative(process.cwd(), fullPath)).on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
const fullPath = path.join(TEST_SUITES_DIR, file);
|
||||
fork(path.relative(process.cwd(), fullPath)).on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,151 +3,150 @@
|
|||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, createElement, Fragment } from "../../dist";
|
||||
import { type Component, Fragment, createElement } from "../../dist";
|
||||
import { renderPage } from "../../dist/render";
|
||||
import { testSuite } from "../lib";
|
||||
|
||||
testSuite("renderPage", ({ test, expect }) => {
|
||||
test("throws an Error when root elem is not html tag", () => {
|
||||
expect(() => renderPage(<div />)).toThrowErrorMatching(
|
||||
"attempted to render page with non-HTML root element div",
|
||||
);
|
||||
});
|
||||
test("throws an Error when root elem is not html tag", () => {
|
||||
expect(() => renderPage(<div />)).toThrowErrorMatching(
|
||||
"attempted to render page with non-HTML root element div",
|
||||
);
|
||||
});
|
||||
|
||||
test("outputs a HTML5 DOCTYPE declaration", () => {
|
||||
const html = renderPage(<html />);
|
||||
expect(html).toStartWith("<!DOCTYPE html>");
|
||||
});
|
||||
test("outputs a HTML5 DOCTYPE declaration", () => {
|
||||
const html = renderPage(<html />);
|
||||
expect(html).toStartWith("<!DOCTYPE html>");
|
||||
});
|
||||
|
||||
test("escapes HTML in tag names", () => {
|
||||
const html = renderPage(
|
||||
<html>{createElement("div></div", null)}</html>,
|
||||
);
|
||||
expect(html).toEqual(
|
||||
"<!DOCTYPE html><html><div></div></div></div></html>",
|
||||
);
|
||||
});
|
||||
test("escapes HTML in tag names", () => {
|
||||
const html = renderPage(<html>{createElement("div></div", null)}</html>);
|
||||
expect(html).toEqual(
|
||||
"<!DOCTYPE html><html><div></div></div></div></html>",
|
||||
);
|
||||
});
|
||||
|
||||
test("renders html attributes", () => {
|
||||
const html = renderPage(
|
||||
<html>
|
||||
<div className="test" id="1" />
|
||||
</html>,
|
||||
);
|
||||
expect(html).toEqual(
|
||||
'<!DOCTYPE html><html><div class="test" id="1"></div></html>',
|
||||
);
|
||||
});
|
||||
test("renders html attributes", () => {
|
||||
const html = renderPage(
|
||||
<html>
|
||||
<div className="test" id="1" />
|
||||
</html>,
|
||||
);
|
||||
expect(html).toEqual(
|
||||
'<!DOCTYPE html><html><div class="test" id="1"></div></html>',
|
||||
);
|
||||
});
|
||||
|
||||
test("renders common html tags", () => {
|
||||
const html = renderPage(
|
||||
<html>
|
||||
<head>
|
||||
<title />
|
||||
</head>
|
||||
<body>
|
||||
<div />
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
expect(html).toEqual(
|
||||
"<!DOCTYPE html><html><head><title></title></head><body><div></div></body></html>",
|
||||
);
|
||||
});
|
||||
test("renders common html tags", () => {
|
||||
const html = renderPage(
|
||||
<html>
|
||||
<head>
|
||||
<title />
|
||||
</head>
|
||||
<body>
|
||||
<div />
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
expect(html).toEqual(
|
||||
"<!DOCTYPE html><html><head><title></title></head><body><div></div></body></html>",
|
||||
);
|
||||
});
|
||||
|
||||
test("renders text nodes", () => {
|
||||
const html = renderPage(<html>There are three lights!</html>);
|
||||
expect(html).toEqual(
|
||||
"<!DOCTYPE html><html>There are three lights!</html>",
|
||||
);
|
||||
});
|
||||
test("renders text nodes", () => {
|
||||
const html = renderPage(<html>There are three lights!</html>);
|
||||
expect(html).toEqual("<!DOCTYPE html><html>There are three lights!</html>");
|
||||
});
|
||||
|
||||
test("renders spliced number nodes", () => {
|
||||
const nLights = 3;
|
||||
const html = renderPage(<html>There are {nLights} lights!</html>);
|
||||
expect(html).toEqual("<!DOCTYPE html><html>There are 3 lights!</html>");
|
||||
});
|
||||
test("renders spliced number nodes", () => {
|
||||
const nLights = 3;
|
||||
const html = renderPage(<html>There are {nLights} lights!</html>);
|
||||
expect(html).toEqual("<!DOCTYPE html><html>There are 3 lights!</html>");
|
||||
});
|
||||
|
||||
test("renders spliced arrays", () => {
|
||||
const Light: Component<{ lightN: number }> = ({ lightN }) => (
|
||||
<div>{lightN}</div>
|
||||
);
|
||||
const lights = [1, 2, 3];
|
||||
const html = renderPage(
|
||||
<html>
|
||||
There are{" "}
|
||||
{lights.map((lightN) => (
|
||||
<Light lightN={lightN} />
|
||||
))}{" "}
|
||||
lights!
|
||||
</html>,
|
||||
);
|
||||
expect(html).toEqual(
|
||||
"<!DOCTYPE html><html>There are <div>1</div><div>2</div><div>3</div> lights!</html>",
|
||||
);
|
||||
});
|
||||
test("renders spliced arrays", () => {
|
||||
const Light: Component<{ lightN: number }> = ({ lightN }) => (
|
||||
<div>{lightN}</div>
|
||||
);
|
||||
const lights = [1, 2, 3];
|
||||
const html = renderPage(
|
||||
<html>
|
||||
There are{" "}
|
||||
{lights.map((lightN) => (
|
||||
<Light lightN={lightN} />
|
||||
))}{" "}
|
||||
lights!
|
||||
</html>,
|
||||
);
|
||||
expect(html).toEqual(
|
||||
"<!DOCTYPE html><html>There are <div>1</div><div>2</div><div>3</div> lights!</html>",
|
||||
);
|
||||
});
|
||||
|
||||
test("renders components w/ custom properties", () => {
|
||||
interface LightProps {
|
||||
nLights: number;
|
||||
}
|
||||
const Light: Component<LightProps> = ({ nLights }) => (
|
||||
<div>{nLights} lights</div>
|
||||
);
|
||||
const html = renderPage(
|
||||
<html>
|
||||
There are <Light nLights={3} />!
|
||||
</html>,
|
||||
);
|
||||
expect(html).toEqual(
|
||||
"<!DOCTYPE html><html>There are <div>3 lights</div>!</html>",
|
||||
);
|
||||
});
|
||||
test("renders components w/ custom properties", () => {
|
||||
interface LightProps {
|
||||
nLights: number;
|
||||
}
|
||||
const Light: Component<LightProps> = ({ nLights }) => (
|
||||
<div>{nLights} lights</div>
|
||||
);
|
||||
const html = renderPage(
|
||||
<html>
|
||||
There are <Light nLights={3} />!
|
||||
</html>,
|
||||
);
|
||||
expect(html).toEqual(
|
||||
"<!DOCTYPE html><html>There are <div>3 lights</div>!</html>",
|
||||
);
|
||||
});
|
||||
|
||||
test("renders fragment children only", () => {
|
||||
const html = renderPage(
|
||||
<html>
|
||||
<Fragment>
|
||||
<div>test of</div>
|
||||
<div>fragments</div>
|
||||
</Fragment>
|
||||
</html>,
|
||||
);
|
||||
expect(html).toEqual(
|
||||
"<!DOCTYPE html><html><div>test of</div><div>fragments</div></html>",
|
||||
);
|
||||
});
|
||||
test("renders fragment children only", () => {
|
||||
const html = renderPage(
|
||||
<html>
|
||||
<Fragment>
|
||||
<div>test of</div>
|
||||
<div>fragments</div>
|
||||
</Fragment>
|
||||
</html>,
|
||||
);
|
||||
expect(html).toEqual(
|
||||
"<!DOCTYPE html><html><div>test of</div><div>fragments</div></html>",
|
||||
);
|
||||
});
|
||||
|
||||
test("renders unescaped HTML via dangerouslySetInnerHTML", () => {
|
||||
const html = renderPage(
|
||||
<html>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: "<div>red alert!</div>",
|
||||
}}
|
||||
/>
|
||||
</html>,
|
||||
);
|
||||
expect(html).toEqual(
|
||||
"<!DOCTYPE html><html><div><div>red alert!</div></div></html>",
|
||||
);
|
||||
});
|
||||
test("renders unescaped HTML via dangerouslySetInnerHTML", () => {
|
||||
const html = renderPage(
|
||||
<html>
|
||||
<div
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: explicit test
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: "<div>red alert!</div>",
|
||||
}}
|
||||
/>
|
||||
</html>,
|
||||
);
|
||||
expect(html).toEqual(
|
||||
"<!DOCTYPE html><html><div><div>red alert!</div></div></html>",
|
||||
);
|
||||
});
|
||||
|
||||
test("throws error when both dangerouslySetInnerHTML and children prop present", () => {
|
||||
expect(() =>
|
||||
renderPage(
|
||||
<html>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: "<div>set phasers to kill</div>",
|
||||
}}
|
||||
>
|
||||
<div>set phasers to stun</div>
|
||||
</div>
|
||||
</html>,
|
||||
),
|
||||
).toThrowErrorMatching(
|
||||
'An element with children may not have a "dangerouslySetInnerHTML" prop since children would be overriden',
|
||||
);
|
||||
});
|
||||
test("throws error when both dangerouslySetInnerHTML and children prop present", () => {
|
||||
expect(() =>
|
||||
renderPage(
|
||||
<html>
|
||||
<div
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: explicit test
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtmlWithChildren: explicit test
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: "<div>set phasers to kill</div>",
|
||||
}}
|
||||
>
|
||||
<div>set phasers to stun</div>
|
||||
</div>
|
||||
</html>,
|
||||
),
|
||||
).toThrowErrorMatching(
|
||||
'An element with children may not have a "dangerouslySetInnerHTML" prop since children would be overriden',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"extends": "../tsconfig-base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"jsxFactory": "createElement"
|
||||
}
|
||||
"extends": "../tsconfig-base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"jsxFactory": "createElement"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue