Switch from jasmine to jest (#747)

Summary:
The reasons for switching to jest:
- easy snapshot testing so that we can easily verify the structure of the parse tree and MathML tree
- easy compilation of ES6 features for tests as we continue to expand our use of ES6

Test Plan:
- npm test
This commit is contained in:
Kevin Barabash
2017-07-11 14:16:26 -04:00
committed by Erik Demaine
parent 4480f2c987
commit 361c500a7f
12 changed files with 228 additions and 275 deletions

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ diff.png
/test/symgroups.log /test/symgroups.log
/test/symgroups.pdf /test/symgroups.pdf
/test/screenshotter/unicode-fonts /test/screenshotter/unicode-fonts
coverage/

View File

@@ -9,9 +9,9 @@ help solving a problem, feel free to stop by our [gitter channel](https://gitter
If you'd like to contribute, try contributing new symbols or functions that If you'd like to contribute, try contributing new symbols or functions that
KaTeX doesn't currently support. The wiki has a page which lists [all of the KaTeX doesn't currently support. The wiki has a page which lists [all of the
supported supported
functions](https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX) as functions](https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX) as
well as a page that describes how to [examine TeX commands and where to find well as a page that describes how to [examine TeX commands and where to find
rules](https://github.com/Khan/KaTeX/wiki/Examining-TeX) which can be quite rules](https://github.com/Khan/KaTeX/wiki/Examining-TeX) which can be quite
useful when adding new commands. There's also a user-contributed [preview page] useful when adding new commands. There's also a user-contributed [preview page]
(http://utensil-site.github.io/available-in-katex/) (http://utensil-site.github.io/available-in-katex/)
showing how KaTeX would render a series of symbols/functions (including the ones showing how KaTeX would render a series of symbols/functions (including the ones
@@ -60,18 +60,22 @@ This will host an interactive editor at
[http://localhost:7936/](http://localhost:7936/) to play around with and test [http://localhost:7936/](http://localhost:7936/) to play around with and test
changes. changes.
#### Jasmine tests #### Jest tests
The JavaScript parser and some of the tree The JavaScript parser and some of the HTML and MathML tree
builder is tested with Jasmine. These tests can be run either using node with builders are tested with Jest. These tests can be run using node with
`make test`, or in the browser by visiting `make test`. If you need to debug the tests see
[http://localhost:7936/test/test.html](http://localhost:7936/test/test.html). [https://facebook.github.io/jest/docs/troubleshooting.html](https://facebook.github.io/jest/docs/troubleshooting.html)
The Jasmine tests should be run after every change, even the addition of small The interactive editor can also be used for debugging tests in the browser by
copy/pasting the test case to be debugged into the editor. The permalink option
can come in really useful when doing repeated runs of the same test case.
The Jest tests should be run after every change, even the addition of small
symbols. However, [Travis](https://travis-ci.org/Khan/KaTeX/) will run these symbols. However, [Travis](https://travis-ci.org/Khan/KaTeX/) will run these
tests when you submit a pull request, in case you forget. tests when you submit a pull request, in case you forget.
If you make any changes to Parser.js, add Jasmine tests to ensure they work. If you make any changes to Parser.js, add Jest tests to ensure they work.
#### Screenshot tests #### Screenshot tests
@@ -117,7 +121,7 @@ Code
In general, try to make your code blend in with the surrounding code. In general, try to make your code blend in with the surrounding code.
## Pull Requests ## Pull Requests
- link back to the original issue(s) whenever possible - link back to the original issue(s) whenever possible
- new commands should be added to the [wiki](https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX) - new commands should be added to the [wiki](https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX)
- commits should be squashed before merging - commits should be squashed before merging

View File

@@ -1,4 +1,4 @@
.PHONY: build dist lint setup copy serve clean metrics test zip contrib .PHONY: build dist lint setup copy serve clean metrics test coverage zip contrib
build: lint build/katex.min.js build/katex.min.css contrib zip compress build: lint build/katex.min.js build/katex.min.css contrib zip compress
ifeq ($(KATEX_DIST),skip) ifeq ($(KATEX_DIST),skip)
@@ -101,7 +101,10 @@ serve: $(NIS)
$(NODE) server.js $(NODE) server.js
test: $(NIS) test: $(NIS)
JASMINE_CONFIG_PATH=test/jasmine.json node_modules/.bin/jasmine node_modules/.bin/jest
coverage: $(NIS)
node_modules/.bin/jest --coverage
PERL=perl PERL=perl
PYTHON=$(shell python2 --version >/dev/null 2>&1 && echo python2 || echo python) PYTHON=$(shell python2 --version >/dev/null 2>&1 && echo python2 || echo python)

View File

@@ -1,5 +1,4 @@
/* global beforeEach: false */ /* global beforeEach: false */
/* global jasmine: false */
/* global expect: false */ /* global expect: false */
/* global it: false */ /* global it: false */
/* global describe: false */ /* global describe: false */
@@ -7,60 +6,56 @@
const splitAtDelimiters = require("./splitAtDelimiters"); const splitAtDelimiters = require("./splitAtDelimiters");
beforeEach(function() { beforeEach(function() {
jasmine.addMatchers({ expect.extend({
toSplitInto: function() { toSplitInto: function(actual, left, right, result) {
return { const message = {
compare: function(actual, left, right, result) { pass: true,
const message = { message: "'" + actual + "' split correctly",
pass: true,
message: "'" + actual + "' split correctly",
};
const startData = [{type: "text", data: actual}];
const split =
splitAtDelimiters(startData, left, right, false);
if (split.length !== result.length) {
message.pass = false;
message.message = "Different number of splits: " +
split.length + " vs. " + result.length + " (" +
JSON.stringify(split) + " vs. " +
JSON.stringify(result) + ")";
return message;
}
for (let i = 0; i < split.length; i++) {
const real = split[i];
const correct = result[i];
let good = true;
let diff;
if (real.type !== correct.type) {
good = false;
diff = "type";
} else if (real.data !== correct.data) {
good = false;
diff = "data";
} else if (real.display !== correct.display) {
good = false;
diff = "display";
}
if (!good) {
message.pass = false;
message.message = "Difference at split " +
(i + 1) + ": " + JSON.stringify(real) +
" vs. " + JSON.stringify(correct) +
" (" + diff + " differs)";
break;
}
}
return message;
},
}; };
const startData = [{type: "text", data: actual}];
const split =
splitAtDelimiters(startData, left, right, false);
if (split.length !== result.length) {
message.pass = false;
message.message = "Different number of splits: " +
split.length + " vs. " + result.length + " (" +
JSON.stringify(split) + " vs. " +
JSON.stringify(result) + ")";
return message;
}
for (let i = 0; i < split.length; i++) {
const real = split[i];
const correct = result[i];
let good = true;
let diff;
if (real.type !== correct.type) {
good = false;
diff = "type";
} else if (real.data !== correct.data) {
good = false;
diff = "data";
} else if (real.display !== correct.display) {
good = false;
diff = "display";
}
if (!good) {
message.pass = false;
message.message = "Difference at split " +
(i + 1) + ": " + JSON.stringify(real) +
" vs. " + JSON.stringify(correct) +
" (" + diff + " differs)";
break;
}
}
return message;
}, },
}); });
}); });

View File

@@ -4,6 +4,3 @@
build/** build/**
node_modules/** node_modules/**
dist/** dist/**
# Third party code
test/jasmine/**

View File

@@ -16,6 +16,7 @@
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"babel-eslint": "^7.2.0", "babel-eslint": "^7.2.0",
"babel-jest": "^20.0.3",
"babel-plugin-transform-class-properties": "^6.23.0", "babel-plugin-transform-class-properties": "^6.23.0",
"babel-plugin-transform-runtime": "^6.15.0", "babel-plugin-transform-runtime": "^6.15.0",
"babel-preset-es2015": "^6.18.0", "babel-preset-es2015": "^6.18.0",
@@ -26,8 +27,7 @@
"eslint": "^3.13.0", "eslint": "^3.13.0",
"express": "^4.14.0", "express": "^4.14.0",
"glob": "^7.1.1", "glob": "^7.1.1",
"jasmine": "^2.3.2", "jest": "^20.0.4",
"jasmine-core": "^2.3.4",
"js-yaml": "^3.3.1", "js-yaml": "^3.3.1",
"jspngopt": "^0.2.0", "jspngopt": "^0.2.0",
"less": "~2.7.1", "less": "~2.7.1",
@@ -46,5 +46,13 @@
}, },
"dependencies": { "dependencies": {
"match-at": "^0.1.0" "match-at": "^0.1.0"
},
"jest": {
"testMatch": [
"**/test/*-spec.js"
],
"transform": {
"^.+\\.js$": "babel-jest"
}
} }
} }

View File

@@ -54,8 +54,6 @@ function getStatic(url, file) {
} }
browserified("/katex.js", "katex", "katex"); browserified("/katex.js", "katex", "katex");
app.use("/test/jasmine", express.static(path.dirname(
require.resolve("jasmine-core/lib/jasmine-core/jasmine.js"))));
browserified("/test/katex-spec.js", "test/*[Ss]pec.js"); browserified("/test/katex-spec.js", "test/*[Ss]pec.js");
browserified( browserified(
"/contrib/auto-render/auto-render.js", "/contrib/auto-render/auto-render.js",

View File

@@ -1,5 +1,4 @@
/* global beforeEach: false */ /* global beforeEach: false */
/* global jasmine: false */
/* global expect: false */ /* global expect: false */
/* global it: false */ /* global it: false */
/* global describe: false */ /* global describe: false */
@@ -10,50 +9,47 @@ import Settings from "../src/Settings";
const defaultSettings = new Settings({}); const defaultSettings = new Settings({});
beforeEach(function() { beforeEach(function() {
jasmine.addMatchers({ const prefix = "KaTeX parse error: ";
toFailWithParseError: function(util, customEqualityTesters) {
const prefix = "KaTeX parse error: "; expect.extend({
return { toFailWithParseError: function(actual, expected) {
compare: function(actual, expected) { try {
try { parseTree(actual, defaultSettings);
parseTree(actual, defaultSettings); return {
return { pass: false,
pass: false, message: "'" + actual + "' parsed without error",
message: "'" + actual + "' parsed without error", };
}; } catch (e) {
} catch (e) { if (expected === undefined) {
if (expected === undefined) { return {
return { pass: true,
pass: true, message: "'" + actual + "' parsed with error",
message: "'" + actual + "' parsed with error", };
}; }
} const msg = e.message;
const msg = e.message; const exp = prefix + expected;
const exp = prefix + expected; if (msg === exp) {
if (msg === exp) { return {
return { pass: true,
pass: true, message: "'" + actual + "'" +
message: "'" + actual + "'" + " parsed with error '" + expected + "'",
" parsed with error '" + expected + "'", };
}; } else if (msg.slice(0, 19) === prefix) {
} else if (msg.slice(0, 19) === prefix) { return {
return { pass: false,
pass: false, message: "'" + actual + "'" +
message: "'" + actual + "'" + " parsed with error '" + msg.slice(19) +
" parsed with error '" + msg.slice(19) + "' but expected '" + expected + "'",
"' but expected '" + expected + "'", };
}; } else {
} else { return {
return { pass: false,
pass: false, message: "'" + actual + "'" +
message: "'" + actual + "'" + " caused error '" + msg +
" caused error '" + msg + "' but expected '" + exp + "'",
"' but expected '" + exp + "'", };
}; }
} }
}
},
};
}, },
}); });
}); });

View File

@@ -1,11 +0,0 @@
{
"spec_dir": ".",
"spec_files": [
"test/**/*[sS]pec.js",
"contrib/**/*[sS]pec.js"
],
"helpers": [
"helpers/**/*.js",
"node_modules/babel-core/register.js"
]
}

View File

@@ -1,6 +1,5 @@
/* eslint max-len:0 */ /* eslint max-len:0 */
/* global beforeEach: false */ /* global beforeEach: false */
/* global jasmine: false */
/* global expect: false */ /* global expect: false */
/* global it: false */ /* global it: false */
/* global describe: false */ /* global describe: false */
@@ -88,115 +87,99 @@ const parseAndSetResult = function(expr, result, settings) {
}; };
beforeEach(function() { beforeEach(function() {
jasmine.addMatchers({ expect.extend({
toParse: function(actual, settings) {
const usedSettings = settings ? settings : defaultSettings;
toParse: function() { const result = {
return { pass: true,
compare: function(actual, settings) { message: "'" + actual + "' succeeded parsing",
const usedSettings = settings ? settings : defaultSettings;
const result = {
pass: true,
message: "'" + actual + "' succeeded parsing",
};
parseAndSetResult(actual, result, usedSettings);
return result;
},
}; };
parseAndSetResult(actual, result, usedSettings);
return result;
}, },
toNotParse: function() { toNotParse: function(actual, settings) {
return { const usedSettings = settings ? settings : defaultSettings;
compare: function(actual, settings) {
const usedSettings = settings ? settings : defaultSettings;
const result = { const result = {
pass: false, pass: false,
message: "Expected '" + actual + "' to fail " + message: "Expected '" + actual + "' to fail " +
"parsing, but it succeeded", "parsing, but it succeeded",
};
try {
parseTree(actual, usedSettings);
} catch (e) {
if (e instanceof ParseError) {
result.pass = true;
result.message = "'" + actual + "' correctly " +
"didn't parse with error: " + e.message;
} else {
result.message = "'" + actual + "' failed " +
"parsing with unknown error: " + e.message;
}
}
return result;
},
}; };
try {
parseTree(actual, usedSettings);
} catch (e) {
if (e instanceof ParseError) {
result.pass = true;
result.message = "'" + actual + "' correctly " +
"didn't parse with error: " + e.message;
} else {
result.message = "'" + actual + "' failed " +
"parsing with unknown error: " + e.message;
}
}
return result;
}, },
toBuild: function() { toBuild: function(actual, settings) {
return { const usedSettings = settings ? settings : defaultSettings;
compare: function(actual, settings) {
const usedSettings = settings ? settings : defaultSettings;
const result = { const result = {
pass: true, pass: true,
message: "'" + actual + "' succeeded in building", message: "'" + actual + "' succeeded in building",
};
expect(actual).toParse(usedSettings);
try {
_getBuilt(actual, settings);
} catch (e) {
result.pass = false;
if (e instanceof ParseError) {
result.message = "'" + actual + "' failed to " +
"build with error: " + e.message;
} else {
result.message = "'" + actual + "' failed " +
"building with unknown error: " + e.message;
}
}
return result;
},
}; };
expect(actual).toParse(usedSettings);
try {
_getBuilt(actual, settings);
} catch (e) {
result.pass = false;
if (e instanceof ParseError) {
result.message = "'" + actual + "' failed to " +
"build with error: " + e.message;
} else {
result.message = "'" + actual + "' failed " +
"building with unknown error: " + e.message;
}
}
return result;
}, },
toParseLike: function(util, baton) { toParseLike: function(actual, expected, settings) {
return { const usedSettings = settings ? settings : defaultSettings;
compare: function(actual, expected, settings) {
const usedSettings = settings ? settings : defaultSettings;
const result = { const result = {
pass: true, pass: true,
message: "Parse trees of '" + actual + message: "Parse trees of '" + actual +
"' and '" + expected + "' are equivalent", "' and '" + expected + "' are equivalent",
};
const actualTree = parseAndSetResult(actual, result,
usedSettings);
if (!actualTree) {
return result;
}
const expectedTree = parseAndSetResult(expected, result,
usedSettings);
if (!expectedTree) {
return result;
}
stripPositions(actualTree);
stripPositions(expectedTree);
if (!util.equals(actualTree, expectedTree, baton)) {
result.pass = false;
result.message = "Parse trees of '" + actual +
"' and '" + expected + "' are not equivalent";
}
return result;
},
}; };
},
const actualTree = parseAndSetResult(actual, result,
usedSettings);
if (!actualTree) {
return result;
}
const expectedTree = parseAndSetResult(expected, result,
usedSettings);
if (!expectedTree) {
return result;
}
stripPositions(actualTree);
stripPositions(expectedTree);
if (JSON.stringify(actualTree) !== JSON.stringify(expectedTree)) {
result.pass = false;
result.message = "Parse trees of '" + actual +
"' and '" + expected + "' are not equivalent";
}
return result;
},
}); });
}); });

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script src="jasmine/jasmine.js"></script>
<script src="jasmine/jasmine-html.js"></script>
<script src="jasmine/boot.js"></script>
<link rel="stylesheet" href="jasmine/jasmine.css">
<script src="katex-spec.js"></script>
</head>
<body>
</body>
</html>

View File

@@ -1,6 +1,5 @@
/* eslint max-len:0 */ /* eslint max-len:0 */
/* global beforeEach: false */ /* global beforeEach: false */
/* global jasmine: false */
/* global expect: false */ /* global expect: false */
/* global it: false */ /* global it: false */
/* global describe: false */ /* global describe: false */
@@ -27,50 +26,42 @@ const parseAndSetResult = function(expr, result, settings) {
describe("unicode", function() { describe("unicode", function() {
beforeEach(function() { beforeEach(function() {
jasmine.addMatchers({ expect.extend({
toParse: function() { toParse: function(actual, settings) {
return { const usedSettings = settings ? settings : defaultSettings;
compare: function(actual, settings) {
const usedSettings = settings ? settings : defaultSettings;
const result = { const result = {
pass: true, pass: true,
message: "'" + actual + "' succeeded parsing", message: "'" + actual + "' succeeded parsing",
};
parseAndSetResult(actual, result, usedSettings);
return result;
},
}; };
parseAndSetResult(actual, result, usedSettings);
return result;
}, },
toNotParse: function() { toNotParse: function(actual, settings) {
return { const usedSettings = settings ? settings : defaultSettings;
compare: function(actual, settings) {
const usedSettings = settings ? settings : defaultSettings;
const result = { const result = {
pass: false, pass: false,
message: "Expected '" + actual + "' to fail " + message: "Expected '" + actual + "' to fail " +
"parsing, but it succeeded", "parsing, but it succeeded",
};
try {
parseTree(actual, usedSettings);
} catch (e) {
if (e instanceof ParseError) {
result.pass = true;
result.message = "'" + actual + "' correctly " +
"didn't parse with error: " + e.message;
} else {
result.message = "'" + actual + "' failed " +
"parsing with unknown error: " + e.message;
}
}
return result;
},
}; };
try {
parseTree(actual, usedSettings);
} catch (e) {
if (e instanceof ParseError) {
result.pass = true;
result.message = "'" + actual + "' correctly " +
"didn't parse with error: " + e.message;
} else {
result.message = "'" + actual + "' failed " +
"parsing with unknown error: " + e.message;
}
}
return result;
}, },
}); });
}); });