Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] feat(textlint-tester): implement snapshot testing #548

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/textlint-tester/README.md
Expand Up @@ -237,6 +237,17 @@ tester.run("no-todo", rule, {

See [`textlint-tester-test.ts`](./test/textlint-tester-test.ts) or [`textlint-tester-plugin.ts`](./test/textlint-tester-plugin.ts) for concrete examples.

### Snapshot Testing

> textlint-tester 5.1+

Snapshot Testing easy to test a rule.

It record from input to

- Input Text
- Output Text(Fixed text)
- Lint messages

## Contributing

Expand Down
4 changes: 3 additions & 1 deletion packages/textlint-tester/package.json
Expand Up @@ -31,12 +31,14 @@
"clean": "rimraf lib/ out/",
"prepublish": "npm run --if-present build",
"test": "mocha \"test/**/*.{js,ts}\"",
"test:updateSnapshot": "cross-env SNAPSHOT_UPDATE=1 mocha \"test/**/*.{js,ts}\"",
"watch": "tsc -p . --watch"
},
"dependencies": {
"@textlint/feature-flag": "^3.0.5",
"@textlint/kernel": "^3.0.0",
"textlint": "^11.0.1"
"textlint": "^11.0.1",
"snap-shot-core": "^6.0.1"
},
"devDependencies": {
"@types/mocha": "^5.2.0",
Expand Down
187 changes: 187 additions & 0 deletions packages/textlint-tester/src/formatter.ts
@@ -0,0 +1,187 @@
// LICENSE : MIT

// Original code is https://github.com/azer/prettify-error
// Author : azer
"use strict";
import { TextlintFixResult, TextlintMessage, TextlintResult } from "@textlint/kernel";

export interface TextLintFormatterOption {
formatterName: string;
color?: boolean;
}

const format = require("@azu/format-text");
const chalk = require("chalk");
const padStart = require("string.prototype.padstart");
const style = require("@azu/style-format");
const stripAnsi = require("strip-ansi");
const pluralize = require("pluralize");
// width is 2
const widthOfString = require("string-width");
// color set
let summaryColor = "yellow";
let greenColor = "green";
const template = style(
"{grey}{ruleId}: {red}{title}{reset}\n" +
"{grey}{filename}{reset}\n" +
" {red}{paddingForLineNo} {v}{reset}\n" +
" {grey}{previousLineNo}. {previousLine}{reset}\n" +
" {reset}{failingLineNo}. {failingLine}{reset}\n" +
" {grey}{nextLineNo}. {nextLine}{reset}\n" +
" {red}{paddingForLineNo} {^}{reset}\n" +
""
);

/**
*
* @param {string} code
* @param {TextLintMessage} message
* @returns {*}
*/
function failingCode(code: string, message: TextlintMessage): any {
let result = [];
const lines = code.split("\n");
let i = message.line - 3;
while (++i < message.line + 1) {
if (i + 1 !== message.line) {
result.push({
line: message.line - (message.line - i - 1),
code: lines[i]
});
continue;
}

result.push({
line: message.line,
col: message.column,
code: lines[i],
failed: true
});
}

return result;
}

function showColumn(codes: string, ch: string): string {
let result = "";
const codeObject: any = codes[1];
const sliced = codeObject.code.slice(0, codeObject.col);
const width = widthOfString(sliced);
if (width <= 0) {
return "";
}
let i = width - 1;

while (i--) {
result += " ";
}

return result + ch;
}

/**
*
* @param {string} code
* @param {string} filePath
* @param {TextLintMessage} message
* @returns {*}
*/
function prettyError(code: string, filePath: string, message: TextlintMessage): any {
if (!code) {
return;
}
const parsed = failingCode(code, message);
const previousLineNo = String(parsed[0].line);
const failingLineNo = String(parsed[1].line);
const nextLineNo = String(parsed[2].line);
const linumlen = Math.max(previousLineNo.length, failingLineNo.length, nextLineNo.length);
return format(template, {
ruleId: message.ruleId,
title: message.message,
filename: filePath + ":" + message.line + ":" + message.column,
previousLine: parsed[0].code ? parsed[0].code : "",
previousLineNo: padStart(previousLineNo, linumlen),
previousColNo: parsed[0].col,
failingLine: parsed[1].code,
failingLineNo: padStart(failingLineNo, linumlen),
failingColNo: parsed[1].col,
nextLine: parsed[2].code ? parsed[2].code : "",
nextLineNo: padStart(nextLineNo, linumlen),
nextColNo: parsed[2].col,
paddingForLineNo: padStart("", linumlen),
"^": showColumn(parsed, "^"),
v: showColumn(parsed, "v")
});
}

export function formatOutput(text: string, formatterOutput?: string, output?: string) {
return `${text}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
${output ? output : "[[NO OUTPUT]]"}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
${formatterOutput ? formatterOutput : "[[NO LINT MESSAGE]]"}`;
}

export function testerFormatter(
{ text, result, fixResult }: { text: string; result: TextlintResult; fixResult?: TextlintFixResult },
options: TextLintFormatterOption
) {
// default: true
const useColor = options.color !== undefined ? options.color : true;
let output = "";
let total = 0;
let errors = 0;
let warnings = 0;
let totalFixable = 0;
const code = text || require("fs").readFileSync(result.filePath, "utf-8");
const messages = result.messages;
if (messages.length === 0) {
return formatOutput(text);
}
total += messages.length;
messages.forEach(function(message) {
// fixable
const fixableIcon = message.fix ? chalk[greenColor].bold("\u2713 ") : "";
if (message.fix) {
totalFixable++;
}
if ((message as any).fatal || message.severity === 2) {
errors++;
} else {
warnings++;
}
const r = fixableIcon + prettyError(code, result.filePath, message);
if (r) {
output += r + "\n";
}
});

if (total > 0) {
output += chalk[summaryColor].bold(
[
"\u2716 ",
total,
pluralize(" problem", total),
" (",
errors,
pluralize(" error", errors),
", ",
warnings,
pluralize(" warning", warnings),
")\n"
].join("")
);
}

if (totalFixable > 0) {
output += chalk[greenColor].bold(
"✓ " + totalFixable + " fixable " + pluralize("problem", totalFixable) + ".\n"
);
output += "Try to run: $ " + chalk.underline("textlint --fix [file]") + "\n";
}

if (!useColor) {
return stripAnsi(formatOutput(text, output, fixResult ? fixResult.output : undefined));
}
return output;
}
93 changes: 93 additions & 0 deletions packages/textlint-tester/src/snapshot-test.ts
@@ -0,0 +1,93 @@
import { testerFormatter } from "./formatter";
import * as path from "path";
import * as assert from "assert";
import { TextLintCore } from "textlint";
import { TextlintFixResult, TextlintResult } from "@textlint/kernel";
import { getTestText } from "./test-util";

const snapShot = require("snap-shot-core");
export type TesterSnapshot = {
text: string;
ext?: string;
inputPath?: string;
options?: any;
};

export interface snapshotTestOptions {
snapshotFileName: string;
fix: boolean;
}

const escapeTemplateStringContent = (text: string) => {
return text.replace(/`/g, "\\`").replace(/\${/g, "\\${");
};
export const snapshotTest = (textlint: TextLintCore, state: TesterSnapshot, options: snapshotTestOptions) => {
const text = typeof state === "object" ? state.text : state;
const inputPath = typeof state === "object" ? state.inputPath : undefined;
const ext = typeof state === "object" && state.ext !== undefined ? state.ext : ".md";
const actualText = getTestText({ text, inputPath });
const singleName = actualText.split(/\n/g).join("_");
it(singleName || "No Name", () => {
const lintPromise: Promise<TextlintResult> = textlint.lintText(actualText, ext);
const fixPromise: Promise<TextlintFixResult | undefined> = options.fix
? textlint.fixText(actualText, ext)
: Promise.resolve(undefined);
return Promise.all([lintPromise, fixPromise]).then(([lintResult, fixResult]) => {
const output = testerFormatter(
{
text: actualText,
result: lintResult,
fixResult
},
{
color: false,
formatterName: "tester"
}
);
// if (output.length === 0) {
// throw new Error(`Snapshot should not be empty result.
// If you want to test the text is valid, please add it to "valid"`);
// }
// change current dir for snapshot
// https://github.com/bahmutov/snap-shot-core/pull/51
const cwd = process.cwd();
const snapshotDir = path.dirname(options.snapshotFileName);
try {
process.chdir(snapshotDir);
} catch (_error) {
return Promise.reject(_error);
}
try {
snapShot({
// Workaround*1: https://github.com/bahmutov/snap-shot-core/issues/117
what: output,
file: options.snapshotFileName, // aliases: file, __filename
specName: singleName, // or whatever name you want to give,
store: (text: string) => escapeTemplateStringContent(text),
raiser: ({
value, // current value
expected // loaded value
}: {
value: any;
expected: any;
}) => {
// Workaround*1: https://github.com/bahmutov/snap-shot-core/issues/117
// load value should be escaped with stored value
assert.deepStrictEqual(value, expected);
},
ext: ".snap",
opts: {
show: Boolean(process.env.SNAPSHOT_SHOW),
dryRun: Boolean(process.env.SNAPSHOT_DRY),
update: Boolean(process.env.SNAPSHOT_UPDATE)
}
});
} catch (error) {
return Promise.reject(error);
} finally {
process.chdir(cwd);
}
return Promise.resolve();
});
});
};
4 changes: 2 additions & 2 deletions packages/textlint-tester/src/test-util.ts
Expand Up @@ -11,14 +11,14 @@ import { TextlintResult } from "@textlint/kernel";
* @param {string} [inputPath]
* @returns {string}
*/
function getTestText({ text, inputPath }: { text?: string; inputPath?: string }) {
export function getTestText({ text, inputPath }: { text?: string; inputPath?: string }) {
if (typeof inputPath === "string") {
return fs.readFileSync(inputPath, "utf-8");
}
if (typeof text === "string") {
return text;
}
throw new Error("should be defined { text } or { inputPath }");
throw new Error("should define either one of { text } or { inputPath }");
}

export type InvalidPattern = {
Expand Down