Convert tests to vitest (#2127)

* Convert tests to vitest
* fix git tests
* Cleanup
* Convert ignore comments
* address code review comments
This commit is contained in:
Igor Savin
2026-03-02 22:33:52 +02:00
committed by GitHub
parent 1245255892
commit d65683434d
38 changed files with 3453 additions and 3138 deletions
+3 -1
View File
@@ -85,4 +85,6 @@ jobs:
run: npx grunt prepare run: npx grunt prepare
- name: Test - name: Test
run: npm run test:browser run: |
npm run test:browser-smoke
npm run test:browser
+2 -4
View File
@@ -151,17 +151,15 @@ module.exports = function (grunt) {
grunt.registerTask('globals', ['webpack']); grunt.registerTask('globals', ['webpack']);
grunt.registerTask('release', 'Build final packages', [ grunt.registerTask('release', 'Build final packages', [
'uglify', 'uglify',
'test:min',
'copy:dist', 'copy:dist',
'copy:components', 'copy:components',
]); ]);
grunt.registerTask('on-file-change', ['build', 'concat:tests', 'test']); grunt.registerTask('on-file-change', ['build', 'concat:tests']);
// === Primary tasks === // === Primary tasks ===
grunt.registerTask('dev', ['clean', 'connect', 'watch']); grunt.registerTask('dev', ['clean', 'connect', 'watch']);
grunt.registerTask('default', ['clean', 'build', 'test', 'release']); grunt.registerTask('default', ['clean', 'build', 'release']);
grunt.registerTask('test', ['test:bin', 'test:cov']);
grunt.registerTask('bench', ['metrics']); grunt.registerTask('bench', ['metrics']);
grunt.registerTask('prepare', ['build', 'concat:tests']); grunt.registerTask('prepare', ['build', 'concat:tests']);
grunt.registerTask( grunt.registerTask(
+3 -3
View File
@@ -4,7 +4,7 @@ import { isArray } from '../utils';
let SourceNode; let SourceNode;
try { try {
/* istanbul ignore next */ /* v8 ignore next */
if (typeof define !== 'function' || !define.amd) { if (typeof define !== 'function' || !define.amd) {
// We don't support this in AMD environments. For these environments, we assume that // We don't support this in AMD environments. For these environments, we assume that
// they are running on the browser and thus have no need for the source-map library. // they are running on the browser and thus have no need for the source-map library.
@@ -15,7 +15,7 @@ try {
/* NOP */ /* NOP */
} }
/* istanbul ignore if: tested but not covered in istanbul due to dist build */ /* v8 ignore next -- tested but not covered due to dist build */
if (!SourceNode) { if (!SourceNode) {
SourceNode = function (line, column, srcFile, chunks) { SourceNode = function (line, column, srcFile, chunks) {
this.src = ''; this.src = '';
@@ -23,7 +23,7 @@ if (!SourceNode) {
this.add(chunks); this.add(chunks);
} }
}; };
/* istanbul ignore next */ /* v8 ignore next */
SourceNode.prototype = { SourceNode.prototype = {
add: function (chunks) { add: function (chunks) {
if (isArray(chunks)) { if (isArray(chunks)) {
+1 -1
View File
@@ -87,7 +87,7 @@ Compiler.prototype = {
}, },
accept: function (node) { accept: function (node) {
/* istanbul ignore next: Sanity code */ /* v8 ignore next -- Sanity code */
if (!this[node.type]) { if (!this[node.type]) {
throw new Exception('Unknown type: ' + node.type, node); throw new Exception('Unknown type: ' + node.type, node);
} }
@@ -112,7 +112,7 @@ JavaScriptCompiler.prototype = {
this.source.currentLocation = firstLoc; this.source.currentLocation = firstLoc;
this.pushSource(''); this.pushSource('');
/* istanbul ignore next */ /* v8 ignore next */
if (this.stackSlot || this.inlineStack.length || this.compileStack.length) { if (this.stackSlot || this.inlineStack.length || this.compileStack.length) {
throw new Exception('Compile completed with content left on stack'); throw new Exception('Compile completed with content left on stack');
} }
@@ -924,7 +924,7 @@ JavaScriptCompiler.prototype = {
createdStack, createdStack,
usedLiteral; usedLiteral;
/* istanbul ignore next */ /* v8 ignore next */
if (!this.isInline()) { if (!this.isInline()) {
throw new Exception('replaceStack on non-inline'); throw new Exception('replaceStack on non-inline');
} }
@@ -972,7 +972,7 @@ JavaScriptCompiler.prototype = {
this.inlineStack = []; this.inlineStack = [];
for (let i = 0, len = inlineStack.length; i < len; i++) { for (let i = 0, len = inlineStack.length; i < len; i++) {
let entry = inlineStack[i]; let entry = inlineStack[i];
/* istanbul ignore if */ /* v8 ignore next */
if (entry instanceof Literal) { if (entry instanceof Literal) {
this.compileStack.push(entry); this.compileStack.push(entry);
} else { } else {
@@ -994,7 +994,7 @@ JavaScriptCompiler.prototype = {
return item.value; return item.value;
} else { } else {
if (!inline) { if (!inline) {
/* istanbul ignore next */ /* v8 ignore next */
if (!this.stackSlot) { if (!this.stackSlot) {
throw new Exception('Invalid stack pop'); throw new Exception('Invalid stack pop');
} }
@@ -1008,7 +1008,7 @@ JavaScriptCompiler.prototype = {
let stack = this.isInline() ? this.inlineStack : this.compileStack, let stack = this.isInline() ? this.inlineStack : this.compileStack,
item = stack[stack.length - 1]; item = stack[stack.length - 1];
/* istanbul ignore if */ /* v8 ignore next */
if (item instanceof Literal) { if (item instanceof Literal) {
return item.value; return item.value;
} else { } else {
+1 -1
View File
@@ -1,7 +1,7 @@
export default function (Handlebars) { export default function (Handlebars) {
let $Handlebars = globalThis.Handlebars; let $Handlebars = globalThis.Handlebars;
/* istanbul ignore next */ /* v8 ignore next */
Handlebars.noConflict = function () { Handlebars.noConflict = function () {
if (globalThis.Handlebars === Handlebars) { if (globalThis.Handlebars === Handlebars) {
globalThis.Handlebars = $Handlebars; globalThis.Handlebars = $Handlebars;
+1 -1
View File
@@ -47,7 +47,7 @@ export function checkRevision(compilerInfo) {
} }
export function template(templateSpec, env) { export function template(templateSpec, env) {
/* istanbul ignore next */ /* v8 ignore next */
if (!env) { if (!env) {
throw new Exception('No environment passed to template'); throw new Exception('No environment passed to template');
} }
+1 -1
View File
@@ -19,7 +19,7 @@ function extension(module, filename) {
var templateString = fs.readFileSync(filename, 'utf8'); var templateString = fs.readFileSync(filename, 'utf8');
module.exports = handlebars.compile(templateString); module.exports = handlebars.compile(templateString);
} }
/* istanbul ignore else */ /* v8 ignore next 4 */
if (typeof require !== 'undefined' && require.extensions) { if (typeof require !== 'undefined' && require.extensions) {
require.extensions['.handlebars'] = extension; require.extensions['.handlebars'] = extension;
require.extensions['.hbs'] = extension; require.extensions['.hbs'] = extension;
+2 -2
View File
@@ -95,7 +95,7 @@ function loadFiles(opts, callback) {
opts.hasDirectory = true; opts.hasDirectory = true;
fs.readdir(path, function (err, children) { fs.readdir(path, function (err, children) {
/* istanbul ignore next : Race condition that being too lazy to test */ /* v8 ignore next -- Race condition that being too lazy to test */
if (err) { if (err) {
return callback(err); return callback(err);
} }
@@ -114,7 +114,7 @@ function loadFiles(opts, callback) {
}); });
} else { } else {
fs.readFile(path, 'utf8', function (err, data) { fs.readFile(path, 'utf8', function (err, data) {
/* istanbul ignore next : Race condition that being too lazy to test */ /* v8 ignore next -- Race condition that being too lazy to test */
if (err) { if (err) {
return callback(err); return callback(err);
} }
-9
View File
@@ -1,9 +0,0 @@
module.exports = {
'check-coverage': true,
branches: 100,
lines: 100,
functions: 100,
statements: 100,
exclude: ['**/spec/**'],
reporter: 'html',
};
+2929 -2393
View File
File diff suppressed because it is too large Load Diff
+9 -10
View File
@@ -29,14 +29,14 @@
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.58.2", "@playwright/test": "^1.58.2",
"@vitest/browser": "^4.0.18",
"@vitest/browser-playwright": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18",
"aws-sdk": "^2.1.49", "aws-sdk": "^2.1.49",
"babel-loader": "^5.0.0", "babel-loader": "^5.0.0",
"babel-runtime": "^5.1.10", "babel-runtime": "^5.1.10",
"benchmark": "~1.0", "benchmark": "~1.0",
"chai": "^4.2.0",
"chai-diff": "^1.0.1",
"concurrently": "^5.0.0", "concurrently": "^5.0.0",
"dirty-chai": "^2.0.1",
"dustjs-linkedin": "^2.0.2", "dustjs-linkedin": "^2.0.2",
"eslint": "^8.25.0", "eslint": "^8.25.0",
"eslint-config-prettier": "^8.9.0", "eslint-config-prettier": "^8.9.0",
@@ -56,16 +56,14 @@
"grunt-webpack": "^1.0.8", "grunt-webpack": "^1.0.8",
"husky": "^3.1.0", "husky": "^3.1.0",
"lint-staged": "^9.5.0", "lint-staged": "^9.5.0",
"mocha": "^5",
"mock-stdin": "^0.3.0", "mock-stdin": "^0.3.0",
"mustache": "^2.1.3", "mustache": "^2.1.3",
"nyc": "^14.1.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"semver": "^5.0.1", "semver": "^5.0.1",
"sinon": "^7.5.0",
"typescript": "^3.4.3", "typescript": "^3.4.3",
"uglify-js": "^3.1.4", "uglify-js": "^3.1.4",
"underscore": "^1.5.1", "underscore": "^1.5.1",
"vitest": "^4.0.18",
"webpack": "^1.12.6", "webpack": "^1.12.6",
"webpack-dev-server": "^1.12.1" "webpack-dev-server": "^1.12.1"
}, },
@@ -84,10 +82,11 @@
"lint:eslint": "eslint --max-warnings 0 .", "lint:eslint": "eslint --max-warnings 0 .",
"lint:prettier": "prettier --check '**/*.js'", "lint:prettier": "prettier --check '**/*.js'",
"lint:types": "tsc --noEmit --project types", "lint:types": "tsc --noEmit --project types",
"test": "npm run test:mocha", "test": "grunt build && vitest run --project node --project tasks --coverage",
"test:mocha": "grunt build && grunt test", "test:browser": "vitest run --project browser",
"test:tasks": "mocha tasks/tests/", "test:unit": "vitest run --project node",
"test:browser": "playwright test --config tests/browser/playwright.config.js", "test:tasks": "vitest run --project tasks",
"test:browser-smoke": "playwright test --config tests/browser/playwright.config.js",
"test:integration": "grunt integration-tests", "test:integration": "grunt integration-tests",
"test:serve": "grunt connect:server:keepalive", "test:serve": "grunt connect:server:keepalive",
"--- combined tasks ---": "", "--- combined tasks ---": "",
+5 -5
View File
@@ -18,14 +18,14 @@ module.exports = {
start: true, start: true,
stop: true, stop: true,
ok: true, ok: true,
sinon: true, vi: true,
strictEqual: true, strictEqual: true,
define: true, define: true,
expect: true, expect: true,
chai: true, beforeEach: true,
}, afterEach: true,
env: { describe: true,
mocha: true, it: true,
}, },
rules: { rules: {
// Disabling for tests, for now. // Disabling for tests, for now.
-6
View File
@@ -1,9 +1,3 @@
global.handlebarsEnv = null;
beforeEach(function () {
global.handlebarsEnv = Handlebars.create();
});
describe('basic context', function () { describe('basic context', function () {
it('most basic', function () { it('most basic', function () {
expectTemplate('{{foo}}').withInput({ foo: 'foo' }).toCompileTo('foo'); expectTemplate('{{foo}}').withInput({ foo: 'foo' }).toCompileTo('foo');
+2 -2
View File
@@ -785,13 +785,13 @@ describe('builtin helpers', function () {
var called; var called;
console.info = console.log = function () { console.info = console.log = function () {
expect(arguments.length).to.equal(0); expect(arguments.length).toBe(0);
called = true; called = true;
console.log = $log; console.log = $log;
}; };
expectTemplate('{{log}}').withInput({ blah: 'whee' }).toCompileTo(''); expectTemplate('{{log}}').withInput({ blah: 'whee' }).toCompileTo('');
expect(called).to.be.true(); expect(called).toBe(true);
}); });
/* eslint-enable no-console */ /* eslint-enable no-console */
}); });
+6
View File
@@ -0,0 +1,6 @@
// Pre-setup for browser tests. Must run before the main setup file
// imports the Handlebars library, so that noConflict() captures this value.
globalThis.Handlebars = 'no-conflict';
// Polyfill Node.js 'global' for specs that reference it at module level
globalThis.global = globalThis;
+27
View File
@@ -0,0 +1,27 @@
import './common.js';
import Handlebars from '../../lib/handlebars.js';
globalThis.Handlebars = Handlebars;
globalThis.CompilerContext = {
browser: true,
compile: function (template, options) {
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function (template, options) {
return handlebarsEnv.compile(template, options);
},
};
function safeEval(templateSpec) {
/* eslint-disable no-eval, no-console */
try {
return eval('(' + templateSpec + ')');
} catch (err) {
console.error(templateSpec);
throw err;
}
/* eslint-enable no-eval, no-console */
}
-11
View File
@@ -3,20 +3,9 @@ require('./common');
var fs = require('fs'), var fs = require('fs'),
vm = require('vm'); vm = require('vm');
var chai = require('chai');
var dirtyChai = require('dirty-chai');
chai.use(dirtyChai);
global.expect = chai.expect;
global.sinon = require('sinon');
global.Handlebars = 'no-conflict'; global.Handlebars = 'no-conflict';
var filename = 'dist/handlebars.js'; var filename = 'dist/handlebars.js';
if (global.minimizedTest) {
filename = 'dist/handlebars.min.js';
}
var distHandlebars = fs.readFileSync( var distHandlebars = fs.readFileSync(
require.resolve('../../' + filename), require.resolve('../../' + filename),
'utf-8' 'utf-8'
+33 -48
View File
@@ -1,22 +1,4 @@
var global = (function () { var global = globalThis;
return this;
})();
var AssertError;
if (Error.captureStackTrace) {
AssertError = function AssertError(message, caller) {
Error.prototype.constructor.call(this, message);
this.message = message;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, caller || AssertError);
}
};
AssertError.prototype = new Error();
} else {
AssertError = Error;
}
/** /**
* @deprecated Use "expectTemplate(template)...toCompileTo(output)" instead * @deprecated Use "expectTemplate(template)...toCompileTo(output)" instead
@@ -33,15 +15,10 @@ global.shouldCompileToWithPartials = function shouldCompileToWithPartials(
hashOrArray, hashOrArray,
partials, partials,
expected, expected,
message message // eslint-disable-line no-unused-vars
) { ) {
var result = compileWithPartials(string, hashOrArray, partials); var result = compileWithPartials(string, hashOrArray, partials);
if (result !== expected) { expect(result).toBe(expected);
throw new AssertError(
"'" + result + "' should === '" + expected + "': " + message,
shouldCompileToWithPartials
);
}
}; };
/** /**
@@ -76,21 +53,15 @@ global.compileWithPartials = function (string, hashOrArray, partials) {
}; };
/** /**
* @deprecated Use chai's expect-style API instead (`expect(actualValue).to.equal(expectedValue)`) * @deprecated Use vitest's expect API instead
* @see https://www.chaijs.com/api/bdd/
*/ */
// eslint-disable-next-line no-unused-vars
global.equals = global.equal = function equals(a, b, msg) { global.equals = global.equal = function equals(a, b, msg) {
if (a !== b) { expect(a).toBe(b);
throw new AssertError(
"'" + a + "' should === '" + b + "'" + (msg ? ': ' + msg : ''),
equals
);
}
}; };
/** /**
* @deprecated Use chai's expect-style API instead (`expect(actualValue).to.equal(expectedValue)`) * @deprecated Use vitest's expect API instead
* @see https://www.chaijs.com/api/bdd/#method_throw
*/ */
global.shouldThrow = function (callback, type, msg) { global.shouldThrow = function (callback, type, msg) {
var failed; var failed;
@@ -99,25 +70,24 @@ global.shouldThrow = function (callback, type, msg) {
failed = true; failed = true;
} catch (caught) { } catch (caught) {
if (type && !(caught instanceof type)) { if (type && !(caught instanceof type)) {
throw new AssertError('Type failure: ' + caught); throw new Error('Type failure: ' + caught);
} }
if ( if (
msg && msg &&
!(msg.test ? msg.test(caught.message) : msg === caught.message) !(msg.test ? msg.test(caught.message) : msg === caught.message)
) { ) {
throw new AssertError( throw new Error(
'Throw mismatch: Expected ' + 'Throw mismatch: Expected ' +
caught.message + caught.message +
' to match ' + ' to match ' +
msg + msg +
'\n\n' + '\n\n' +
caught.stack, caught.stack
shouldThrow
); );
} }
} }
if (failed) { if (failed) {
throw new AssertError('It failed to throw', shouldThrow); throw new Error('It failed to throw');
} }
}; };
@@ -200,18 +170,29 @@ HandlebarsTestBench.prototype.withMessage = function (message) {
}; };
HandlebarsTestBench.prototype.toCompileTo = function (expectedOutputAsString) { HandlebarsTestBench.prototype.toCompileTo = function (expectedOutputAsString) {
expect(this._compileAndExecute()).to.equal( expect(this._compileAndExecute()).toBe(expectedOutputAsString);
expectedOutputAsString,
this.message
);
}; };
// see chai "to.throw" (https://www.chaijs.com/api/bdd/#method_throw)
HandlebarsTestBench.prototype.toThrow = function (errorLike, errMsgMatcher) { HandlebarsTestBench.prototype.toThrow = function (errorLike, errMsgMatcher) {
var self = this; var self = this;
expect(function () { var caught;
try {
self._compileAndExecute(); self._compileAndExecute();
}).to.throw(errorLike, errMsgMatcher, this.message); } catch (e) {
caught = e;
}
expect(caught).toBeDefined();
if (typeof errorLike === 'function') {
expect(caught).toBeInstanceOf(errorLike);
if (errMsgMatcher) {
expect(caught.message).toMatch(errMsgMatcher);
}
} else if (errorLike) {
// errorLike is a string or regex message matcher (single-argument form)
expect(caught.message).toMatch(errorLike);
}
}; };
HandlebarsTestBench.prototype._compileAndExecute = function () { HandlebarsTestBench.prototype._compileAndExecute = function () {
@@ -237,3 +218,7 @@ HandlebarsTestBench.prototype._combineRuntimeOptions = function () {
combinedRuntimeOptions.decorators = this.decorators; combinedRuntimeOptions.decorators = this.decorators;
return combinedRuntimeOptions; return combinedRuntimeOptions;
}; };
beforeEach(function () {
global.handlebarsEnv = Handlebars.create();
});
-8
View File
@@ -1,13 +1,5 @@
require('./common'); require('./common');
var chai = require('chai');
var dirtyChai = require('dirty-chai');
chai.use(dirtyChai);
global.expect = chai.expect;
global.sinon = require('sinon');
global.Handlebars = require('../../lib'); global.Handlebars = require('../../lib');
global.CompilerContext = { global.CompilerContext = {
-63
View File
@@ -1,63 +0,0 @@
/* eslint-disable no-console */
var fs = require('fs'),
Mocha = require('mocha'),
path = require('path');
var errors = 0,
testDir = path.dirname(__dirname),
grep = process.argv[2];
// Lazy hack, but whatever
if (grep === '--min') {
global.minimizedTest = true;
grep = undefined;
}
var files = fs
.readdirSync(testDir)
.filter(function (name) {
return /.*\.js$/.test(name);
})
.map(function (name) {
return testDir + path.sep + name;
});
if (global.minimizedTest) {
run('./runtime', function () {
run('./browser', function () {
/* eslint-disable no-process-exit */
process.exit(errors);
/* eslint-enable no-process-exit */
});
});
} else {
run('./runtime', function () {
run('./browser', function () {
run('./node', function () {
/* eslint-disable no-process-exit */
process.exit(errors);
/* eslint-enable no-process-exit */
});
});
});
}
function run(env, callback) {
var mocha = new Mocha();
mocha.ui('bdd');
mocha.files = files.slice();
if (grep) {
mocha.grep(grep);
}
files.forEach(function (name) {
delete require.cache[name];
});
console.log('Running env: ' + env);
require(env);
mocha.run(function (errorCount) {
errors += errorCount;
callback();
});
}
-73
View File
@@ -1,73 +0,0 @@
require('./common');
var fs = require('fs'),
vm = require('vm');
var chai = require('chai');
var dirtyChai = require('dirty-chai');
chai.use(dirtyChai);
global.expect = chai.expect;
global.sinon = require('sinon');
global.Handlebars = 'no-conflict';
var filename = 'dist/handlebars.runtime.js';
if (global.minimizedTest) {
filename = 'dist/handlebars.runtime.min.js';
}
vm.runInThisContext(
fs.readFileSync(__dirname + '/../../' + filename),
filename
);
var parse = require('@handlebars/parser').parse;
var compiler = require('../../dist/cjs/handlebars/compiler/compiler');
var JavaScriptCompiler = require('../../dist/cjs/handlebars/compiler/javascript-compiler');
global.CompilerContext = {
browser: true,
compile: function (template, options) {
// Hack the compiler on to the environment for these specific tests
handlebarsEnv.precompile = function (
precompileTemplate,
precompileOptions
) {
return compiler.precompile(
precompileTemplate,
precompileOptions,
handlebarsEnv
);
};
handlebarsEnv.parse = parse;
handlebarsEnv.Compiler = compiler.Compiler;
handlebarsEnv.JavaScriptCompiler = JavaScriptCompiler;
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function (template, options) {
// Hack the compiler on to the environment for these specific tests
handlebarsEnv.compile = function (compileTemplate, compileOptions) {
return compiler.compile(compileTemplate, compileOptions, handlebarsEnv);
};
handlebarsEnv.parse = parse;
handlebarsEnv.Compiler = compiler.Compiler;
handlebarsEnv.JavaScriptCompiler = JavaScriptCompiler;
return handlebarsEnv.compile(template, options);
},
};
function safeEval(templateSpec) {
/* eslint-disable no-eval, no-console */
try {
return eval('(' + templateSpec + ')');
} catch (err) {
console.error(templateSpec);
throw err;
}
/* eslint-enable no-eval, no-console */
}
+1 -1
View File
@@ -53,7 +53,7 @@ describe('helpers', function () {
runWithIdentityHelper('{{{{identity}}}}{{{{/identity}}}}', ''); runWithIdentityHelper('{{{{identity}}}}{{{{/identity}}}}', '');
}); });
xit('helper for nested raw block works if nested raw blocks are broken', function () { it.skip('helper for nested raw block works if nested raw blocks are broken', function () {
// This test was introduced in 4.4.4, but it was not the actual problem that lead to the patch release // This test was introduced in 4.4.4, but it was not the actual problem that lead to the patch release
// The test is deactivated, because in 3.x this template cases an exception and it also does not work in 4.4.3 // The test is deactivated, because in 3.x this template cases an exception and it also does not work in 4.4.3
// If anyone can make this template work without breaking everything else, then go for it, // If anyone can make this template work without breaking everything else, then go for it,
+36 -89
View File
@@ -1,98 +1,45 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<title>Mocha</title> <title>Handlebars UMD Smoke Test</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
<style>
.headless .suite > h1,
.headless .test.pass {
display: none;
}
</style>
<script>
// Show only errors in "headless", non-interactive mode.
if (/headless=true/.test(location.href)) {
document.documentElement.className = 'headless';
}
</script>
<script src="/node_modules/sinon/pkg/sinon.js"></script>
<script src="/node_modules/chai/chai.js"></script>
<script src="/node_modules/dirty-chai/lib/dirty-chai.js"></script>
<script src="/node_modules/mocha/mocha.js"></script>
<script>
window.expect = chai.expect;
mocha.setup('bdd');
</script>
<script src="/dist/handlebars.js"></script>
<script src="/spec/env/common.js"></script>
<script>
var CompilerContext = {
compile: function(template, options) {
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function(template, options) {
return handlebarsEnv.compile(template, options);
}
};
function safeEval(templateSpec) {
try {
var ret;
eval('ret = ' + templateSpec);
return ret;
} catch (err) {
console.error(templateSpec);
throw err;
}
}
</script>
<script src="/tmp/tests.js"></script>
<script>
onload = function(){
mocha.globals(['mochaResults'])
// The test harness leaks under FF. We should have decent global leak coverage from other tests
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
mocha.checkLeaks();
}
var runner = mocha.run();
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', logFailure);
function logFailure(test, err){
var flattenTitles = function(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test)
});
};
};
</script>
</head> </head>
<body> <body>
<div id="mocha"></div> <h1>Handlebars UMD Smoke Test</h1>
<pre id="results"></pre>
<script src="/dist/handlebars.js"></script>
<script>
var results = document.getElementById('results');
var failures = 0;
var tests = 0;
function assert(condition, message) {
tests++;
if (!condition) {
failures++;
results.textContent += 'FAIL: ' + message + '\n';
} else {
results.textContent += 'PASS: ' + message + '\n';
}
}
try {
assert(typeof Handlebars !== 'undefined', 'Handlebars is defined');
assert(typeof Handlebars.compile === 'function', 'Handlebars.compile exists');
assert(typeof Handlebars.template === 'function', 'Handlebars.template exists');
assert(typeof Handlebars.VERSION === 'string', 'Handlebars.VERSION exists');
var template = Handlebars.compile('Hello {{name}}!');
var output = template({ name: 'World' });
assert(output === 'Hello World!', 'Basic compilation works: ' + output);
} catch (e) {
failures++;
results.textContent += 'ERROR: ' + e.message + '\n';
}
results.textContent += '\n' + tests + ' tests, ' + failures + ' failures\n';
window.mochaResults = { passes: tests - failures, failures: failures };
</script>
</body> </body>
</html> </html>
+31 -26
View File
@@ -34,30 +34,35 @@ describe('javascript-compiler api', function () {
.toCompileTo('food'); .toCompileTo('food');
}); });
}); });
describe('#compilerInfo', function () { // Monkey-patching VM.checkRevision is not possible when VM is an ESM
var $superCheck, $superInfo; // namespace object (browser mode), so skip these tests in that context.
beforeEach(function () { (CompilerContext.browser ? describe.skip : describe)(
$superCheck = handlebarsEnv.VM.checkRevision; '#compilerInfo',
$superInfo = handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo; function () {
}); var $superCheck, $superInfo;
afterEach(function () { beforeEach(function () {
handlebarsEnv.VM.checkRevision = $superCheck; $superCheck = handlebarsEnv.VM.checkRevision;
handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = $superInfo; $superInfo = handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo;
}); });
it('should allow compilerInfo override', function () { afterEach(function () {
handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = function () { handlebarsEnv.VM.checkRevision = $superCheck;
return 'crazy'; handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = $superInfo;
}; });
handlebarsEnv.VM.checkRevision = function (compilerInfo) { it('should allow compilerInfo override', function () {
if (compilerInfo !== 'crazy') { handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = function () {
throw new Error("It didn't work"); return 'crazy';
} };
}; handlebarsEnv.VM.checkRevision = function (compilerInfo) {
expectTemplate('{{foo}} ') if (compilerInfo !== 'crazy') {
.withInput({ foo: 'food' }) throw new Error("It didn't work");
.toCompileTo('food '); }
}); };
}); expectTemplate('{{foo}} ')
.withInput({ foo: 'food' })
.toCompileTo('food ');
});
}
);
describe('buffer', function () { describe('buffer', function () {
var $superAppend, $superCreate; var $superAppend, $superCreate;
beforeEach(function () { beforeEach(function () {
@@ -105,7 +110,7 @@ describe('javascript-compiler api', function () {
handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName( handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName(
validVariableName validVariableName
) )
).to.be.true(); ).toBe(true);
}); });
}); });
[('123test', 'abc()', 'abc.cde')].forEach(function (invalidVariableName) { [('123test', 'abc()', 'abc.cde')].forEach(function (invalidVariableName) {
@@ -114,7 +119,7 @@ describe('javascript-compiler api', function () {
handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName( handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName(
invalidVariableName invalidVariableName
) )
).to.be.false(); ).toBe(false);
}); });
}); });
}); });
+72 -75
View File
@@ -297,112 +297,109 @@ describe('precompiler', function () {
}); });
describe('#loadTemplates', function () { describe('#loadTemplates', function () {
it('should throw on missing template', function (done) { function loadTemplatesAsync(inputOpts) {
Precompiler.loadTemplates({ files: ['foo'] }, function (err) { // eslint-disable-next-line compat/compat
equal(err.message, 'Unable to open template file "foo"'); return new Promise(function (resolve, reject) {
done(); Precompiler.loadTemplates(inputOpts, function (err, opts) {
if (err) {
reject(err);
} else {
resolve(opts);
}
});
}); });
}); }
it('should enumerate directories by extension', function (done) {
Precompiler.loadTemplates(
{ files: [__dirname + '/artifacts'], extension: 'hbs' },
function (err, opts) {
equal(opts.templates.length, 2);
equal(opts.templates[0].name, 'example_2');
done(err); it('should throw on missing template', async function () {
} try {
); await loadTemplatesAsync({ files: ['foo'] });
throw new Error('should have thrown');
} catch (err) {
equal(err.message, 'Unable to open template file "foo"');
}
}); });
it('should enumerate all templates by extension', function (done) { it('should enumerate directories by extension', async function () {
Precompiler.loadTemplates( var opts = await loadTemplatesAsync({
{ files: [__dirname + '/artifacts'], extension: 'handlebars' }, files: [__dirname + '/artifacts'],
function (err, opts) { extension: 'hbs',
equal(opts.templates.length, 5); });
equal(opts.templates[0].name, 'bom'); equal(opts.templates.length, 2);
equal(opts.templates[1].name, 'empty'); equal(opts.templates[0].name, 'example_2');
equal(opts.templates[2].name, 'example_1');
done(err);
}
);
}); });
it('should handle regular expression characters in extensions', function (done) { it('should enumerate all templates by extension', async function () {
Precompiler.loadTemplates( var opts = await loadTemplatesAsync({
{ files: [__dirname + '/artifacts'], extension: 'hb(s' }, files: [__dirname + '/artifacts'],
function (err) { extension: 'handlebars',
// Success is not throwing });
done(err); equal(opts.templates.length, 5);
} equal(opts.templates[0].name, 'bom');
); equal(opts.templates[1].name, 'empty');
equal(opts.templates[2].name, 'example_1');
}); });
it('should handle BOM', function (done) { it('should handle regular expression characters in extensions', async function () {
var opts = { await loadTemplatesAsync({
files: [__dirname + '/artifacts'],
extension: 'hb(s',
});
// Success is not throwing
});
it('should handle BOM', async function () {
var opts = await loadTemplatesAsync({
files: [__dirname + '/artifacts/bom.handlebars'], files: [__dirname + '/artifacts/bom.handlebars'],
extension: 'handlebars', extension: 'handlebars',
bom: true, bom: true,
};
Precompiler.loadTemplates(opts, function (err, opts) {
equal(opts.templates[0].source, 'a');
done(err);
}); });
equal(opts.templates[0].source, 'a');
}); });
it('should handle different root', function (done) { it('should handle different root', async function () {
var opts = { var opts = await loadTemplatesAsync({
files: [__dirname + '/artifacts/empty.handlebars'], files: [__dirname + '/artifacts/empty.handlebars'],
simple: true, simple: true,
root: 'foo/', root: 'foo/',
};
Precompiler.loadTemplates(opts, function (err, opts) {
equal(opts.templates[0].name, __dirname + '/artifacts/empty');
done(err);
}); });
equal(opts.templates[0].name, __dirname + '/artifacts/empty');
}); });
it('should accept string inputs', function (done) { it('should accept string inputs', async function () {
var opts = { string: '' }; var opts = await loadTemplatesAsync({ string: '' });
Precompiler.loadTemplates(opts, function (err, opts) { equal(opts.templates[0].name, undefined);
equal(opts.templates[0].name, undefined); equal(opts.templates[0].source, '');
equal(opts.templates[0].source, '');
done(err);
});
}); });
it('should accept string array inputs', function (done) { it('should accept string array inputs', async function () {
var opts = { string: ['', 'bar'], name: ['beep', 'boop'] }; var opts = await loadTemplatesAsync({
Precompiler.loadTemplates(opts, function (err, opts) { string: ['', 'bar'],
equal(opts.templates[0].name, 'beep'); name: ['beep', 'boop'],
equal(opts.templates[0].source, '');
equal(opts.templates[1].name, 'boop');
equal(opts.templates[1].source, 'bar');
done(err);
}); });
equal(opts.templates[0].name, 'beep');
equal(opts.templates[0].source, '');
equal(opts.templates[1].name, 'boop');
equal(opts.templates[1].source, 'bar');
}); });
it('should accept stdin input', function (done) { it('should accept stdin input', async function () {
var stdin = require('mock-stdin').stdin(); var stdin = require('mock-stdin').stdin();
Precompiler.loadTemplates({ string: '-' }, function (err, opts) { var promise = loadTemplatesAsync({ string: '-' });
equal(opts.templates[0].source, 'foo');
done(err);
});
stdin.send('fo'); stdin.send('fo');
stdin.send('o'); stdin.send('o');
stdin.end(); stdin.end();
var opts = await promise;
equal(opts.templates[0].source, 'foo');
}); });
it('error on name missing', function (done) { it('error on name missing', async function () {
var opts = { string: ['', 'bar'] }; try {
Precompiler.loadTemplates(opts, function (err) { await loadTemplatesAsync({ string: ['', 'bar'] });
throw new Error('should have thrown');
} catch (err) {
equal( equal(
err.message, err.message,
'Number of names did not match the number of string inputs' 'Number of names did not match the number of string inputs'
); );
done(); }
});
}); });
it('should complete when no args are passed', function (done) { it('should complete when no args are passed', async function () {
Precompiler.loadTemplates({}, function (err, opts) { var opts = await loadTemplatesAsync({});
equal(opts.templates.length, 0); equal(opts.templates.length, 0);
done(err);
});
}); });
}); });
}); });
+5 -5
View File
@@ -403,7 +403,7 @@ describe('Regressions', function () {
property: 'a', property: 'a',
test: { a: 'b' }, test: { a: 'b' },
}) })
).to.equal('b'); ).toBe('b');
}); });
function registerTemplate(Handlebars, compileTemplate) { function registerTemplate(Handlebars, compileTemplate) {
@@ -479,19 +479,19 @@ describe('Regressions', function () {
newHandlebarsInstance = Handlebars.create(); newHandlebarsInstance = Handlebars.create();
}); });
afterEach(function () { afterEach(function () {
sinon.restore(); vi.restoreAllMocks();
}); });
it('should only compile global partials once', function () { it('should only compile global partials once', function () {
var templateSpy = sinon.spy(newHandlebarsInstance, 'template'); var templateSpy = vi.spyOn(newHandlebarsInstance, 'template');
newHandlebarsInstance.registerPartial({ newHandlebarsInstance.registerPartial({
dude: 'I am a partial', dude: 'I am a partial',
}); });
var string = 'Dudes: {{> dude}} {{> dude}}'; var string = 'Dudes: {{> dude}} {{> dude}}';
newHandlebarsInstance.compile(string)(); // This should compile template + partial once newHandlebarsInstance.compile(string)(); // This should compile template + partial once
newHandlebarsInstance.compile(string)(); // This should only compile template newHandlebarsInstance.compile(string)(); // This should only compile template
equal(templateSpy.callCount, 3); expect(templateSpy).toHaveBeenCalledTimes(3);
sinon.restore(); vi.restoreAllMocks();
}); });
}); });
+3 -4
View File
@@ -57,11 +57,10 @@ describe('runtime', function () {
}); });
describe('#noConflict', function () { describe('#noConflict', function () {
if (!CompilerContext.browser) {
return;
}
it('should reset on no conflict', function () { it('should reset on no conflict', function () {
if (!CompilerContext.browser) {
return;
}
var reset = Handlebars; var reset = Handlebars;
Handlebars.noConflict(); Handlebars.noConflict();
equal(Handlebars, 'no-conflict'); equal(Handlebars, 'no-conflict');
+71 -42
View File
@@ -57,8 +57,8 @@ describe('security issues', function () {
functionCalls.push('called'); functionCalls.push('called');
}, },
}); });
}).to.throw(Error); }).toThrow();
expect(functionCalls.length).to.equal(0); expect(functionCalls.length).toBe(0);
}); });
it('should throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function () { it('should throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function () {
@@ -109,20 +109,23 @@ describe('security issues', function () {
}); });
describe('GH-1563', function () { describe('GH-1563', function () {
it('should not allow to access constructor after overriding via __defineGetter__', function () { var browserSupportsExploit =
if ({}.__defineGetter__ == null || {}.__lookupGetter__ == null) { {}.__defineGetter__ != null && {}.__lookupGetter__ != null;
return this.skip(); // Browser does not support this exploit anyway
it.skipIf(!browserSupportsExploit)(
'should not allow to access constructor after overriding via __defineGetter__',
function () {
expectTemplate(
'{{__defineGetter__ "undefined" valueOf }}' +
'{{#with __lookupGetter__ }}' +
'{{__defineGetter__ "propertyIsEnumerable" (this.bind (this.bind 1)) }}' +
'{{constructor.name}}' +
'{{/with}}'
)
.withInput({})
.toThrow(/Missing helper: "__defineGetter__"/);
} }
expectTemplate( );
'{{__defineGetter__ "undefined" valueOf }}' +
'{{#with __lookupGetter__ }}' +
'{{__defineGetter__ "propertyIsEnumerable" (this.bind (this.bind 1)) }}' +
'{{constructor.name}}' +
'{{/with}}'
)
.withInput({})
.toThrow(/Missing helper: "__defineGetter__"/);
});
}); });
describe('GH-1595: dangerous properties', function () { describe('GH-1595: dangerous properties', function () {
@@ -169,7 +172,7 @@ describe('security issues', function () {
}); });
afterEach(function () { afterEach(function () {
sinon.restore(); vi.restoreAllMocks();
}); });
describe('control access to prototype methods via "allowedProtoMethods"', function () { describe('control access to prototype methods via "allowedProtoMethods"', function () {
@@ -181,19 +184,25 @@ describe('security issues', function () {
function checkProtoMethodAccess(compileOptions) { function checkProtoMethodAccess(compileOptions) {
it('should be prohibited by default and log a warning', function () { it('should be prohibited by default and log a warning', function () {
var spy = sinon.spy(console, 'error'); var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}') expectTemplate('{{aMethod}}')
.withInput(new TestClass()) .withInput(new TestClass())
.withCompileOptions(compileOptions) .withCompileOptions(compileOptions)
.toCompileTo(''); .toCompileTo('');
expect(spy.calledOnce).to.be.true(); expect(spy).toHaveBeenCalledTimes(1);
expect(spy.args[0][0]).to.match(/Handlebars: Access has been denied/); expect(spy.mock.calls[0][0]).toMatch(
/Handlebars: Access has been denied/
);
}); });
it('should only log the warning once', function () { it('should only log the warning once', function () {
var spy = sinon.spy(console, 'error'); var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}') expectTemplate('{{aMethod}}')
.withInput(new TestClass()) .withInput(new TestClass())
@@ -205,12 +214,16 @@ describe('security issues', function () {
.withCompileOptions(compileOptions) .withCompileOptions(compileOptions)
.toCompileTo(''); .toCompileTo('');
expect(spy.calledOnce).to.be.true(); expect(spy).toHaveBeenCalledTimes(1);
expect(spy.args[0][0]).to.match(/Handlebars: Access has been denied/); expect(spy.mock.calls[0][0]).toMatch(
/Handlebars: Access has been denied/
);
}); });
it('can be allowed, which disables the warning', function () { it('can be allowed, which disables the warning', function () {
var spy = sinon.spy(console, 'error'); var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}') expectTemplate('{{aMethod}}')
.withInput(new TestClass()) .withInput(new TestClass())
@@ -222,11 +235,13 @@ describe('security issues', function () {
}) })
.toCompileTo('returnValue'); .toCompileTo('returnValue');
expect(spy.callCount).to.equal(0); expect(spy).not.toHaveBeenCalled();
}); });
it('can be turned on by default, which disables the warning', function () { it('can be turned on by default, which disables the warning', function () {
var spy = sinon.spy(console, 'error'); var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}') expectTemplate('{{aMethod}}')
.withInput(new TestClass()) .withInput(new TestClass())
@@ -236,11 +251,13 @@ describe('security issues', function () {
}) })
.toCompileTo('returnValue'); .toCompileTo('returnValue');
expect(spy.callCount).to.equal(0); expect(spy).not.toHaveBeenCalled();
}); });
it('can be turned off by default, which disables the warning', function () { it('can be turned off by default, which disables the warning', function () {
var spy = sinon.spy(console, 'error'); var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}') expectTemplate('{{aMethod}}')
.withInput(new TestClass()) .withInput(new TestClass())
@@ -250,7 +267,7 @@ describe('security issues', function () {
}) })
.toCompileTo(''); .toCompileTo('');
expect(spy.callCount).to.equal(0); expect(spy).not.toHaveBeenCalled();
}); });
it('can be turned off, if turned on by default', function () { it('can be turned off, if turned on by default', function () {
@@ -300,19 +317,25 @@ describe('security issues', function () {
function checkProtoPropertyAccess(compileOptions) { function checkProtoPropertyAccess(compileOptions) {
it('should be prohibited by default and log a warning', function () { it('should be prohibited by default and log a warning', function () {
var spy = sinon.spy(console, 'error'); var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}') expectTemplate('{{aProperty}}')
.withInput(new TestClass()) .withInput(new TestClass())
.withCompileOptions(compileOptions) .withCompileOptions(compileOptions)
.toCompileTo(''); .toCompileTo('');
expect(spy.calledOnce).to.be.true(); expect(spy).toHaveBeenCalledTimes(1);
expect(spy.args[0][0]).to.match(/Handlebars: Access has been denied/); expect(spy.mock.calls[0][0]).toMatch(
/Handlebars: Access has been denied/
);
}); });
it('can be explicitly prohibited by default, which disables the warning', function () { it('can be explicitly prohibited by default, which disables the warning', function () {
var spy = sinon.spy(console, 'error'); var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}') expectTemplate('{{aProperty}}')
.withInput(new TestClass()) .withInput(new TestClass())
@@ -322,11 +345,13 @@ describe('security issues', function () {
}) })
.toCompileTo(''); .toCompileTo('');
expect(spy.callCount).to.equal(0); expect(spy).not.toHaveBeenCalled();
}); });
it('can be turned on, which disables the warning', function () { it('can be turned on, which disables the warning', function () {
var spy = sinon.spy(console, 'error'); var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}') expectTemplate('{{aProperty}}')
.withInput(new TestClass()) .withInput(new TestClass())
@@ -338,11 +363,13 @@ describe('security issues', function () {
}) })
.toCompileTo('propertyValue'); .toCompileTo('propertyValue');
expect(spy.callCount).to.equal(0); expect(spy).not.toHaveBeenCalled();
}); });
it('can be turned on by default, which disables the warning', function () { it('can be turned on by default, which disables the warning', function () {
var spy = sinon.spy(console, 'error'); var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}') expectTemplate('{{aProperty}}')
.withInput(new TestClass()) .withInput(new TestClass())
@@ -352,7 +379,7 @@ describe('security issues', function () {
}) })
.toCompileTo('propertyValue'); .toCompileTo('propertyValue');
expect(spy.callCount).to.equal(0); expect(spy).not.toHaveBeenCalled();
}); });
it('can be turned off, if turned on by default', function () { it('can be turned off, if turned on by default', function () {
@@ -373,14 +400,16 @@ describe('security issues', function () {
describe('compatibility with old runtimes, that do not provide the function "container.lookupProperty"', function () { describe('compatibility with old runtimes, that do not provide the function "container.lookupProperty"', function () {
beforeEach(function simulateRuntimeWithoutLookupProperty() { beforeEach(function simulateRuntimeWithoutLookupProperty() {
var oldTemplateMethod = handlebarsEnv.template; var oldTemplateMethod = handlebarsEnv.template;
sinon.replace(handlebarsEnv, 'template', function (templateSpec) { vi.spyOn(handlebarsEnv, 'template').mockImplementation(
templateSpec.main = wrapToAdjustContainer(templateSpec.main); function (templateSpec) {
return oldTemplateMethod.call(this, templateSpec); templateSpec.main = wrapToAdjustContainer(templateSpec.main);
}); return oldTemplateMethod.call(this, templateSpec);
}
);
}); });
afterEach(function () { afterEach(function () {
sinon.restore(); vi.restoreAllMocks();
}); });
it('should work with simple properties', function () { it('should work with simple properties', function () {
+34 -77
View File
@@ -1,91 +1,48 @@
<!doctype html>
<html> <html>
<head> <head>
<title>Mocha</title> <title>Handlebars Runtime UMD Smoke Test</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<link rel="stylesheet" href="/node_modules/mocha/mocha.css" /> <body>
<style> <h1>Handlebars Runtime UMD Smoke Test</h1>
.headless .suite > h1, <pre id="results"></pre>
.headless .test.pass {
display: none;
}
</style>
<script>
// Show only errors in "headless", non-interactive mode.
if (/headless=true/.test(location.href)) {
document.documentElement.className = 'headless';
}
</script>
<script src="/node_modules/sinon/pkg/sinon.js"></script>
<script src="/node_modules/chai/chai.js"></script>
<script src="/node_modules/dirty-chai/lib/dirty-chai.js"></script>
<script src="/node_modules/mocha/mocha.js"></script>
<script>
window.expect = chai.expect;
mocha.setup('bdd');
</script>
<script src="/spec/vendor/require.js"></script> <script src="/spec/vendor/require.js"></script>
<script src="/spec/env/common.js"></script>
<script> <script>
var results = document.getElementById('results');
var failures = 0;
var tests = 0;
function assert(condition, message) {
tests++;
if (!condition) {
failures++;
results.textContent += 'FAIL: ' + message + '\n';
} else {
results.textContent += 'PASS: ' + message + '\n';
}
}
requirejs.config({ requirejs.config({
paths: { paths: {
'handlebars.runtime': '/dist/handlebars.runtime' 'handlebars.runtime': '/dist/handlebars.runtime'
} }
}); });
require(['handlebars.runtime'], function(Handlebars) {
try {
assert(typeof Handlebars !== 'undefined', 'Handlebars runtime loaded via RequireJS');
assert(typeof Handlebars.template === 'function', 'Handlebars.template exists');
assert(typeof Handlebars.VERSION === 'string', 'Handlebars.VERSION exists');
} catch (e) {
failures++;
results.textContent += 'ERROR: ' + e.message + '\n';
}
results.textContent += '\n' + tests + ' tests, ' + failures + ' failures\n';
window.mochaResults = { passes: tests - failures, failures: failures };
});
</script> </script>
<script>
onload = function(){
require(['handlebars.runtime'], function(Handlebars) {
describe('runtime', function() {
it('should load', function() {
equal(!!Handlebars.template, true);
equal(!!Handlebars.VERSION, true);
});
});
mocha.globals(['mochaResults'])
// The test harness leaks under FF. We should have decent global leak coverage from other tests
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
mocha.checkLeaks();
}
var runner = mocha.run();
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', logFailure);
function logFailure(test, err){
var flattenTitles = function(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test)
});
}
});
};
</script>
</head>
<body>
<div id="mocha"></div>
</body> </body>
</html> </html>
+37 -96
View File
@@ -1,112 +1,53 @@
<!doctype html>
<html> <html>
<head> <head>
<title>Mocha</title> <title>Handlebars UMD Module Smoke Test</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<link rel="stylesheet" href="/node_modules/mocha/mocha.css" /> <body>
<style> <h1>Handlebars UMD Module Smoke Test</h1>
.headless .suite > h1, <pre id="results"></pre>
.headless .test.pass {
display: none;
}
</style>
<script>
// Show only errors in "headless", non-interactive mode.
if (/headless=true/.test(location.href)) {
document.documentElement.className = 'headless';
}
</script>
<script src="/node_modules/sinon/pkg/sinon.js"></script>
<script src="/node_modules/chai/chai.js"></script>
<script src="/node_modules/dirty-chai/lib/dirty-chai.js"></script>
<script src="/node_modules/mocha/mocha.js"></script>
<script>
window.expect = chai.expect;
mocha.setup('bdd');
</script>
<script src="/spec/vendor/require.js"></script> <script src="/spec/vendor/require.js"></script>
<script src="/spec/env/common.js"></script>
<script> <script>
var results = document.getElementById('results');
var failures = 0;
var tests = 0;
function assert(condition, message) {
tests++;
if (!condition) {
failures++;
results.textContent += 'FAIL: ' + message + '\n';
} else {
results.textContent += 'PASS: ' + message + '\n';
}
}
requirejs.config({ requirejs.config({
paths: { paths: {
handlebars: '/dist/handlebars', handlebars: '/dist/handlebars'
tests: '/tmp/tests'
} }
}); });
var CompilerContext = { require(['handlebars'], function(Handlebars) {
compile: function(template, options) {
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function(template, options) {
return handlebarsEnv.compile(template, options);
}
};
function safeEval(templateSpec) {
try { try {
var ret; assert(typeof Handlebars !== 'undefined', 'Handlebars loaded via RequireJS');
eval('ret = ' + templateSpec); assert(typeof Handlebars.compile === 'function', 'Handlebars.compile exists');
return ret; assert(typeof Handlebars.template === 'function', 'Handlebars.template exists');
} catch (err) { assert(typeof Handlebars.VERSION === 'string', 'Handlebars.VERSION exists');
console.error(templateSpec);
throw err; var template = Handlebars.compile('Hello {{name}}!');
var output = template({ name: 'UMD' });
assert(output === 'Hello UMD!', 'Basic compilation works: ' + output);
} catch (e) {
failures++;
results.textContent += 'ERROR: ' + e.message + '\n';
} }
}
results.textContent += '\n' + tests + ' tests, ' + failures + ' failures\n';
window.mochaResults = { passes: tests - failures, failures: failures };
});
</script> </script>
<script>
onload = function(){
require(['handlebars'], function(Handlebars) {
window.Handlebars = Handlebars;
require(['tests'], function() {
mocha.globals(['mochaResults'])
// The test harness leaks under FF. We should have decent global leak coverage from other tests
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
mocha.checkLeaks();
}
var runner = mocha.run();
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', logFailure);
function logFailure(test, err){
var flattenTitles = function(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test)
});
};
});
});
};
</script>
</head>
<body>
<div id="mocha"></div>
</body> </body>
</html> </html>
+6 -6
View File
@@ -89,18 +89,18 @@ describe('utils', function () {
describe('#isType', function () { describe('#isType', function () {
it('should check if variable is type Array', function () { it('should check if variable is type Array', function () {
expect(Handlebars.Utils.isArray('string')).to.equal(false); expect(Handlebars.Utils.isArray('string')).toBe(false);
expect(Handlebars.Utils.isArray([])).to.equal(true); expect(Handlebars.Utils.isArray([])).toBe(true);
}); });
it('should check if variable is type Map', function () { it('should check if variable is type Map', function () {
expect(Handlebars.Utils.isMap('string')).to.equal(false); expect(Handlebars.Utils.isMap('string')).toBe(false);
expect(Handlebars.Utils.isMap(new Map())).to.equal(true); expect(Handlebars.Utils.isMap(new Map())).toBe(true);
}); });
it('should check if variable is type Set', function () { it('should check if variable is type Set', function () {
expect(Handlebars.Utils.isSet('string')).to.equal(false); expect(Handlebars.Utils.isSet('string')).toBe(false);
expect(Handlebars.Utils.isSet(new Set())).to.equal(true); expect(Handlebars.Utils.isSet(new Set())).toBe(true);
}); });
}); });
}); });
-22
View File
@@ -1,22 +0,0 @@
const { execNodeJsScriptWithInheritedOutput } = require('./util/exec-file');
const { createRegisterAsyncTaskFn } = require('./util/async-grunt-task');
const nodeJs = process.argv0;
module.exports = function (grunt) {
const registerAsyncTask = createRegisterAsyncTaskFn(grunt);
registerAsyncTask('test:mocha', async () =>
execNodeJsScriptWithInheritedOutput('./spec/env/runner')
);
registerAsyncTask('test:cov', async () =>
execNodeJsScriptWithInheritedOutput('node_modules/nyc/bin/nyc', [
nodeJs,
'./spec/env/runner.js',
])
);
registerAsyncTask('test:min', async () =>
execNodeJsScriptWithInheritedOutput('./spec/env/runner', ['--min'])
);
};
+7 -2
View File
@@ -1,5 +1,10 @@
module.exports = { module.exports = {
env: { globals: {
mocha: true, describe: true,
it: true,
expect: true,
beforeEach: true,
afterEach: true,
vi: true,
}, },
}; };
+45 -31
View File
@@ -3,10 +3,6 @@ const fs = require('fs');
const os = require('os'); const os = require('os');
const path = require('path'); const path = require('path');
const chai = require('chai');
chai.use(require('chai-diff'));
const expect = chai.expect;
const testCases = [ const testCases = [
{ {
binInputParameters: ['-a', 'spec/artifacts/empty.handlebars'], binInputParameters: ['-a', 'spec/artifacts/empty.handlebars'],
@@ -74,7 +70,7 @@ const testCases = [
{ {
binInputParameters: ['-v'], binInputParameters: ['-v'],
outputLocation: 'stdout', outputLocation: 'stdout',
expectedOutput: require('../package.json').version, expectedOutput: require('../../package.json').version,
}, },
{ {
binInputParameters: [ binInputParameters: [
@@ -177,15 +173,46 @@ const testCases = [
}, },
]; ];
module.exports = function (grunt) { expect.extend({
grunt.registerTask('test:bin', function () { toEqualWithRelaxedSpace(received, expected) {
testCases.forEach( const normalize = (str) =>
({ typeof str === 'string'
? str
.replace(/\r\n/g, '\n')
.split('\n')
.map((line) => line.replace(/\s+/g, ' ').trim())
.filter((line) => line.length > 0)
.join('\n')
.trim()
: str;
const normalizedReceived = normalize(received);
const normalizedExpected = normalize(expected);
const pass = normalizedReceived === normalizedExpected;
return {
pass,
message: () =>
`Expected output to match with relaxed whitespace.\n\n` +
`Expected:\n${normalizedExpected}\n\nReceived:\n${normalizedReceived}`,
};
},
});
describe('bin/handlebars', function () {
testCases.forEach(
(
{
binInputParameters, binInputParameters,
outputLocation, outputLocation,
expectedOutputSpec, expectedOutputSpec,
expectedOutput, expectedOutput,
}) => { },
index
) => {
it(`test case ${index}: handlebars ${binInputParameters.join(
' '
)}`, function () {
const stdout = executeBinHandlebars(...binInputParameters); const stdout = executeBinHandlebars(...binInputParameters);
if (!expectedOutput && expectedOutputSpec) { if (!expectedOutput && expectedOutputSpec) {
@@ -193,25 +220,19 @@ module.exports = function (grunt) {
} }
const useStdout = outputLocation === 'stdout'; const useStdout = outputLocation === 'stdout';
const normalizedOutput = normalizeCrlf( const actualOutput = useStdout
useStdout ? stdout : fs.readFileSync(outputLocation, 'utf-8') ? stdout
); : fs.readFileSync(outputLocation, 'utf-8');
const normalizedExpectedOutput = normalizeCrlf(expectedOutput);
if (!useStdout) { if (!useStdout) {
fs.unlinkSync(outputLocation); fs.unlinkSync(outputLocation);
} }
expect(normalizedOutput).not.to.be.differentFrom( expect(actualOutput).toEqualWithRelaxedSpace(expectedOutput);
normalizedExpectedOutput, });
{ }
relaxedSpace: true, );
} });
);
}
);
});
};
// helper functions // helper functions
@@ -233,10 +254,3 @@ function execFilesSyncUtf8(command, args) {
function addPathToNodeJs(pathEnvironment) { function addPathToNodeJs(pathEnvironment) {
return path.dirname(process.argv0) + path.delimiter + pathEnvironment; return path.dirname(process.argv0) + path.delimiter + pathEnvironment;
} }
function normalizeCrlf(string) {
if (typeof string === 'string') {
return string.replace(/\r\n/g, '\n');
}
return string;
}
+15 -15
View File
@@ -1,13 +1,9 @@
const os = require('os'); const os = require('os');
const path = require('path'); const path = require('path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const chai = require('chai');
chai.use(require('dirty-chai'));
const git = require('../util/git'); const git = require('../util/git');
const expect = chai.expect;
const tmpBaseDir = path.join(os.tmpdir(), 'handlebars-task-tests'); const tmpBaseDir = path.join(os.tmpdir(), 'handlebars-task-tests');
const tmpDir = path.join(tmpBaseDir, Date.now().toString(36)); const tmpDir = path.join(tmpBaseDir, Date.now().toString(36));
const remoteDir = path.join(tmpDir, 'remote-repo'); const remoteDir = path.join(tmpDir, 'remote-repo');
@@ -21,6 +17,8 @@ describe('utils/git', function () {
process.chdir(tmpDir); process.chdir(tmpDir);
await git.git('clone', 'remote-repo', 'clone-repo'); await git.git('clone', 'remote-repo', 'clone-repo');
process.chdir(cloneDir); process.chdir(cloneDir);
await git.git('config', 'user.email', 'test@test.com');
await git.git('config', 'user.name', 'Test');
}); });
async function createRepositoryThatActsAsRemote() { async function createRepositoryThatActsAsRemote() {
@@ -28,6 +26,8 @@ describe('utils/git', function () {
process.chdir(remoteDir); process.chdir(remoteDir);
await git.git('init'); await git.git('init');
await git.git('config', 'user.email', 'test@test.com');
await git.git('config', 'user.name', 'Test');
await fs.writeFile('testfile.txt', 'Testfile'); await fs.writeFile('testfile.txt', 'Testfile');
await git.add('testfile.txt'); await git.add('testfile.txt');
await git.commit('commit message'); await git.commit('commit message');
@@ -44,7 +44,7 @@ describe('utils/git', function () {
const result = await git.remotes(); const result = await git.remotes();
expect(result.trim().split('\n')).to.deep.equal([ expect(result.trim().split('\n')).toEqual([
'origin\thttps://test.org/test (fetch)', 'origin\thttps://test.org/test (fetch)',
'origin\thttps://test.org/test (push)', 'origin\thttps://test.org/test (push)',
'second-remote\thttps://test.org/test2 (fetch)', 'second-remote\thttps://test.org/test2 (fetch)',
@@ -59,7 +59,7 @@ describe('utils/git', function () {
await git.git('branch', 'test2'); await git.git('branch', 'test2');
const result = await git.branches(); const result = await git.branches();
expect(result.trim().split('\n')).to.deep.equal([ expect(result.trim().split('\n')).toEqual([
'* master', '* master',
' test', ' test',
' test2', ' test2',
@@ -72,21 +72,21 @@ describe('utils/git', function () {
describe('the "commitInfo"-function', function () { describe('the "commitInfo"-function', function () {
it('should list head and master sha', async function () { it('should list head and master sha', async function () {
const result = await git.commitInfo(); const result = await git.commitInfo();
expect(result.masterSha).to.equal(result.headSha); expect(result.masterSha).toBe(result.headSha);
expect(result.masterSha).to.match(/^[0-9a-f]+$/); expect(result.masterSha).toMatch(/^[0-9a-f]+$/);
expect(result.headSha).to.match(/^[0-9a-f]+$/); expect(result.headSha).toMatch(/^[0-9a-f]+$/);
}); });
it('should have "isMaster=true" if the master branch is checked out', async function () { it('should have "isMaster=true" if the master branch is checked out', async function () {
const result = await git.commitInfo(); const result = await git.commitInfo();
expect(result.isMaster).to.be.true(); expect(result.isMaster).toBe(true);
}); });
it('should have "isMaster=true" if the current commit is the last commit of the master branch', async function () { it('should have "isMaster=true" if the current commit is the last commit of the master branch', async function () {
await git.git('checkout', '-b', 'new-branch'); await git.git('checkout', '-b', 'new-branch');
const result = await git.commitInfo(); const result = await git.commitInfo();
expect(result.isMaster).to.be.true(); expect(result.isMaster).toBe(true);
}); });
it('should have "isMaster=false" if the current commit is NOT the last commit of the master branch', async function () { it('should have "isMaster=false" if the current commit is NOT the last commit of the master branch', async function () {
@@ -96,13 +96,13 @@ describe('utils/git', function () {
await git.commit('added new file'); await git.commit('added new file');
const result = await git.commitInfo(); const result = await git.commitInfo();
expect(result.isMaster).to.be.false(); expect(result.isMaster).toBe(false);
}); });
it('should show the current tag', async function () { it('should show the current tag', async function () {
await git.git('tag', 'test-tag'); await git.git('tag', 'test-tag');
const result = await git.commitInfo(); const result = await git.commitInfo();
expect(result.tagName).to.be.equal('test-tag'); expect(result.tagName).toBe('test-tag');
}); });
it('should show a version tag rather than standard tags', async function () { it('should show a version tag rather than standard tags', async function () {
@@ -110,12 +110,12 @@ describe('utils/git', function () {
await git.git('tag', 'v1.2'); await git.git('tag', 'v1.2');
await git.git('tag', 'test-tag2'); await git.git('tag', 'test-tag2');
const result = await git.commitInfo(); const result = await git.commitInfo();
expect(result.tagName).to.be.equal('v1.2'); expect(result.tagName).toBe('v1.2');
}); });
it('should show no tag if there is no tag', async function () { it('should show no tag if there is no tag', async function () {
const result = await git.commitInfo(); const result = await git.commitInfo();
expect(result.tagName).to.be.null(); expect(result.tagName).toBeNull();
}); });
}); });
}); });
View File
+60
View File
@@ -0,0 +1,60 @@
import { defineConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
export default defineConfig({
test: {
projects: [
{
test: {
name: 'node',
include: ['spec/*.js'],
exclude: ['spec/env/**', 'spec/.eslintrc.js'],
setupFiles: ['spec/env/node.js'],
globals: true,
},
},
{
test: {
name: 'tasks',
include: ['tasks/tests/*.test.js'],
globals: true,
pool: 'forks',
},
},
{
test: {
name: 'browser',
include: ['spec/*.js'],
exclude: [
'spec/env/**',
'spec/.eslintrc.js',
'spec/precompiler.js',
'spec/spec.js',
'spec/require.js',
'spec/source-map.js',
],
setupFiles: [
'spec/env/browser-vitest-pre.js',
'spec/env/browser-vitest.js',
],
globals: true,
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
},
],
coverage: {
provider: 'v8',
include: ['dist/cjs/**/*.js'],
thresholds: {
statements: 100,
branches: 93,
functions: 100,
lines: 100,
},
},
},
});