blob: 953ef8ebf35792694b1a6f64e754443cd5109df7 [file] [log] [blame]
//-------------------------------------------------------------------------------------------------------
// Copyright (C) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
//-------------------------------------------------------------------------------------------------------
// ES6 String Template tests -- verifies the API shape and basic functionality
WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js");
function ReturnString(str) {
return str;
}
function GetCallsite(callsite) {
return callsite;
}
function GetExpectedCachedCallsite() {
return GetCallsite`some string template ${''} with replacements ${''}`;
}
function GetRawStringValue(callsite, idx) {
return callsite.raw[idx];
}
function GetStringValue(callsite, idx) {
return callsite[idx];
}
function tagReturnConstructor(callsite) {
switch(callsite[0]) {
case 'string':
return String;
case 'symbol':
return Symbol;
case 'array':
return Array;
case 'number':
return Number;
}
return function() {
return {
name: 'constructed object'
}
};
}
function tagReturnConstructorWrapper() {
return tagReturnConstructor;
}
var ObjectWithToString = {
toString: function() { return "ObjectWithToString.toString() called!"; },
value: 'ObjectWithToString.value (should not show up)'
};
var tests = [
{
name: "String constructor should have spec defined built-ins with correct lengths",
body: function () {
assert.isTrue(String.hasOwnProperty('raw'), "String constructor should have a raw method");
assert.isTrue(String.raw.length === 1, "String.raw method takes 1 arguments");
}
},
{
name: "String.raw takes a callsite object and replacement values and returns a string that is the combination of the callsite raw strings and replacement values",
body: function () {
assert.throws(function () { String.raw.call(); }, TypeError, "String.raw throws TypeError if it is given no arguments", "String.raw: argument is not an Object");
assert.throws(function () { String.raw.call('non-object'); }, TypeError, "String.raw throws TypeError if it is given non-object parameter", "String.raw: argument is not an Object");
assert.throws(function () { String.raw.call({}); }, TypeError, "String.raw throws TypeError if it is given object which doesn't contain a raw property", "String.raw: argument is not an Object");
assert.throws(function () { String.raw.call({ raw: undefined }); }, TypeError, "String.raw throws TypeError if it is given object which contains a raw property which is undefined", "String.raw: argument is not an Object");
assert.throws(function () { String.raw.call({ raw: 'non-object' }); }, TypeError, "String.raw throws TypeError if it is given object which contains a raw property which is not an object", "String.raw: argument is not an Object");
assert.areEqual("", String.raw``, "String.raw of empty string is the empty string");
assert.areEqual("no replacement", String.raw`no replacement`, "no replacement expressions");
assert.areEqual("\\u1234", String.raw`\u1234`, "unicode escape sequence");
assert.areEqual("str\\ning1 simple replacement... \\r\\nstring end", String.raw`str\ning1 ${'simple replacement...'} \r\nstring end`, "string with replacement and escape sequences");
assert.areEqual("str1\\\\ str2 str3\\\\ str4 str5\\\\ str6 str7\\\\", String.raw`str1\\ ${'str2'} str3\\ ${'str4'} str5\\ ${'str6'} str7\\`, "several replacements and escape sequences");
assert.areEqual("begin (nested template here) end", String.raw`begin ${`(nested ${ReturnString('template')} here)`} end`, "nested string template literal");
assert.areEqual("call toString... ObjectWithToString.toString() called! success...?", String.raw`call toString... ${ObjectWithToString} success...?`, "replacement expression is object with toString");
assert.areEqual("simple raw string", String.raw({ raw: ["simple raw string"] }), "pass a custom object with raw property");
assert.areEqual("string1 replacement string2", String.raw({ raw: ["string1 ", " string2"] }, ReturnString('replacement')), "pass a custom object with raw property and replacement parameter");
assert.areEqual("string1 r1 string2", String.raw({ raw: ["string1 ", " string2"] }, 'r1', 'r2'), "pass object manually with more replacement parameters than expected");
assert.areEqual("string1 string2", String.raw({ raw: ["string1 ", " string2"] }), "pass object manually with fewer replacement parameters than expected");
assert.areEqual("string1 string2 string3", String.raw({ raw: ["string1 ", " string2 ", " string3"] }), "pass object manually with fewer replacement parameters than expected");
assert.areEqual("", String.raw({ raw: [] }), "pass object manually containing with no raw strings");
}
},
{
name: "Callsite object passed to tag functions should have correct attributes",
body: function() {
var callsite = GetCallsite`simple template ${'with'} some ${'replacement'} expressions`;
var descriptor;
for (var i = 0; i < callsite.length; i++) {
descriptor = Object.getOwnPropertyDescriptor(callsite, i);
assert.isFalse(descriptor.writable, `callsite[${i}].descriptor.writable == false`);
assert.isTrue(descriptor.enumerable, `callsite[${i}].descriptor.enumerable == true`);
assert.isFalse(descriptor.configurable, `callsite[${i}].descriptor.configurable == false`);
descriptor = Object.getOwnPropertyDescriptor(callsite.raw, i);
assert.isFalse(descriptor.writable, `callsite.raw[${i}].descriptor.writable == false`);
assert.isTrue(descriptor.enumerable, `callsite.raw[${i}].descriptor.enumerable == true`);
assert.isFalse(descriptor.configurable, `callsite.raw[${i}].descriptor.configurable == false`);
}
descriptor = Object.getOwnPropertyDescriptor(callsite, 'raw');
assert.isFalse(descriptor.writable, `callsite.raw.descriptor.writable == false`);
assert.isFalse(descriptor.enumerable, `callsite.raw.descriptor.enumerable == false`);
assert.isFalse(descriptor.configurable, `callsite.raw.descriptor.configurable == false`);
assert.isTrue(Object.isFrozen(callsite), "callsite object is frozen");
assert.isTrue(Object.isFrozen(callsite.raw), "callsite.raw object is frozen");
}
},
{
name: "Each string template literal corresponds to exactly one (cached) callsite object",
body: function() {
var callsite1 = GetCallsite`simple template literal 1`;
var callsite2 = GetCallsite`simple template literal 2`;
assert.isFalse(callsite1 === callsite2, "different string template literals create different callsite objects");
var callsite3 = GetCallsite`simple template literal 3`;
var callsite4 = GetCallsite`simple template literal 3`;
assert.isTrue(callsite3 === callsite4, "different string template literals with the same string literal value create identical callsite objects");
var loopCallsite = undefined;
for (var i = 0; i < 10; i++) {
var c = GetCallsite`loop template literal ${i}`;
if (loopCallsite === undefined) {
loopCallsite = c;
} else {
assert.isTrue(loopCallsite === c, "string template literal used in a loop reuses the same callsite object.");
}
assert.areEqual(2, c.length, "loop callsite has expected count of string literals");
assert.areEqual("loop template literal ", c[0], "loop callsite has expected first string literal value");
assert.areEqual("", c[1], "loop callsite has expected second string literal value");
assert.areEqual(2, c.raw.length, "loop callsite.raw has expected count of string literals");
assert.areEqual("loop template literal ", c.raw[0], "loop callsite.raw has expected first string literal value");
assert.areEqual("", c.raw[1], "loop callsite.raw has expected second string literal value");
}
loopCallsite = undefined
for (var i = 0; i < 10; i++) {
var c = GetExpectedCachedCallsite();
if (loopCallsite === undefined) {
loopCallsite = c;
} else {
assert.isTrue(loopCallsite === c, "string template declared in other function returns same callsite object when function called.");
}
assert.areEqual(3, c.length, "loop callsite has expected count of string literals");
assert.areEqual("some string template ", c[0], "loop callsite has expected first string literal value");
assert.areEqual(" with replacements ", c[1], "loop callsite has expected second string literal value");
assert.areEqual("", c[2], "loop callsite has expected third string literal value");
assert.areEqual(3, c.raw.length, "loop callsite.raw has expected count of string literals");
assert.areEqual("some string template ", c.raw[0], "loop callsite.raw has expected first string literal value");
assert.areEqual(" with replacements ", c.raw[1], "loop callsite.raw has expected second string literal value");
assert.areEqual("", c.raw[2], "loop callsite.raw has expected third string literal value");
}
}
},
{
name: "BLUE: 490848 - string templates do not allow substitution expressions which contain '}' characters",
body: function() {
var foo;
assert.areEqual("function () { return 'foo called'; }", `${foo = function() { return 'foo called'; }}`, "Function declaration (+assignment) in string template");
assert.areEqual('function', typeof foo, "Assignment inside string template substitution expression");
assert.areEqual('foo called', foo(), "Function declared in template expression can be called later");
assert.areEqual("ObjectLiteral.toString() called", `${{toString:function(){return 'ObjectLiteral.toString() called';}}}`, "Object literal declared in string template expression");
foo = undefined;
assert.areEqual("foo: comma syntax", `${(foo = function() { return 'foo: comma syntax'; }, foo())}`, "Comma syntax in string template");
assert.areEqual('function', typeof foo, "Assignment inside string template substitution expression");
assert.areEqual('foo: comma syntax', foo(), "Function declared in template expression can be called later");
assert.areEqual('{toString}', `{${{toString:function(){return 'toString';}}}}`, "Test all the permutations of '}' characters in a string template");
}
},
{
name: "BLUE: 525727 - using a let declaration inside a string template causes an assertion",
body: function() {
assert.throws(function () { eval('`${let x = 5}`;'); }, SyntaxError, "Let returns an identifier - can't be a substitution expression", "Expected '}'");
assert.throws(function () { eval('`${a a}`;'); }, SyntaxError, "ParseExpr returns the first word as an identifier.", "Expected '}'");
}
},
{
name: "BLUE: 557210 - string templates do not check for escaped uses of substitution expressions",
body: function() {
// None of these should cause AV
new Function("function z() {}; `z`;")();
new Function("function z() {}; `${z}`;")();
new Function("function z() {}; `${z}${z}`;")();
new Function("function z() {}; `${z}${z}${z}`;")();
new Function("function z() {}; `${'z'}${z}${z}`;")();
new Function("function z() {}; `${'z'}${'z'}${z}`;")();
new Function("function z() {}; '' + z + '';")();
new Function("function z() {}; z`${`${z}`}`;")();
new Function("function z() {}; z``;")();
new Function("function z() {}; ``;")();
new Function("(`${function(id) { return id }}`);")();
new Function("function y() {} y`${`${'z'}${`${function(id) { return id }})${ /x/g >= 'c'}`}`}`;")();
}
},
{
name: "BLUE: 560073 - parsing a tagged string template does not respect the fInNew flag",
body: function() {
assert.throws(function() { new tagReturnConstructor`symbol`; }, TypeError, "Calling 'new Symbol()' throws so we should see a throw if we try to new the return from the tag function if it is Symbol", "Function is not a constructor");
assert.isTrue(Array.isArray(new tagReturnConstructor`array`), "Simple case of calling tagged string template as constructor");
assert.areEqual(6, new tagReturnConstructor`array${`string1`}${`string2`}`(6).length, "Calling tagged string template as constructor with replacements and arguments");
assert.areEqual(`string`, new tagReturnConstructor`string`(`STRING`).toLowerCase(), "Create a string and pass parameters via String constructor returned from tag function");
assert.throws(function() { new tagReturnConstructorWrapper`unused``symbol`; }, TypeError, "Calling 'new Symbol()' throws so we should see a throw if we try to new the return from the tag function if it is Symbol", "Function is not a constructor");
assert.areEqual(new Number(0), new tagReturnConstructorWrapper`unused``number`, "Create a boxed number object via Number constructor returned from previous string template tag function");
assert.areEqual(Object(`actual string value`), new tagReturnConstructorWrapper`unused``string`(`actual string value`), "Create a string via String constructor returned from previous string template tag function");
}
},
{
name: "String template tag function returning a constructor function should be constructable in a new expression",
body: function() {
function tagReturnNestedConstructorFunction(name) {
return tagReturnConstructor([name]);
}
function tagReturnNestedConstructorObject(callsite) {
return {
constructor_array : [ Symbol, String, Array, Number ],
constructor_tag_function : tagReturnConstructor,
constructor_ordinary_function : tagReturnNestedConstructorFunction,
constructor_object : { symbol : Symbol, string : String, array : Array, number : Number },
constructor_nested : function() { return tagReturnNestedConstructorObject; }
};
}
function tagReturnNestedConstructorArray() {
return tagReturnNestedConstructorObject().constructor_array;
}
function tagReturnNestedConstructorArrayWrapper() {
return [function() { return tagReturnNestedConstructorArray(); }];
}
assert.throws(function() { new tagReturnNestedConstructorObject`ignored`.constructor_array[0]; }, TypeError, "Calling 'new Symbol()' throws so we should see a throw if we try to new the return from the tag function if it is Symbol", "Function is not a constructor");
assert.throws(function() { new tagReturnNestedConstructorObject`ignored`.constructor_tag_function`symbol`; }, TypeError, "Calling 'new Symbol()' throws so we should see a throw if we try to new the return from the tag function if it is Symbol", "Function is not a constructor");
assert.isTrue(Symbol === new tagReturnNestedConstructorObject`ignored`.constructor_ordinary_function(`symbol`), "Mixing MemberExpression token types");
assert.throws(function() { new new tagReturnNestedConstructorObject`ignored`.constructor_ordinary_function(`symbol`); }, TypeError, "new constructor_ordinary_function returns Symbol which should throw if new'd", "Function is not a constructor");
assert.isTrue(Symbol === new tagReturnNestedConstructorObject`ignored`.constructor_nested(`also ignored`)`one more ignored thing`.constructor_object.symbol, `Mixing MemberExpression token types`);
assert.isTrue(Array.isArray(new new tagReturnNestedConstructorObject`ignored`.constructor_nested(`also ignored`)`one more ignored thing`.constructor_object.array), `Mixing MemberExpression token types`);
assert.areEqual(`object`, typeof new new tagReturnNestedConstructorObject`ignored`.constructor_nested(`also ignored`)`one more ignored thing`.constructor_object.number(42), `Mixing MemberExpression token types`);
assert.areEqual(`real string`, new new tagReturnNestedConstructorObject`ignored`.constructor_nested(`also ignored`)`one more ignored thing`.constructor_object.string(`Real String`).toLowerCase(), `Mixing MemberExpression token types`);
assert.isTrue(Symbol === new tagReturnNestedConstructorObject`ignored`[`constructor_nested`](`also ignored`)`one more ignored thing`.constructor_object.symbol, `Mixing MemberExpression token types`);
assert.isTrue(Array.isArray(new new tagReturnNestedConstructorObject`ignored`[`constructor_nested`](`also ignored`)`one more ignored thing`.constructor_object.array), `Mixing MemberExpression token types`);
assert.areEqual(`object`, typeof new new tagReturnNestedConstructorObject`ignored`[`constructor_nested`](`also ignored`)`one more ignored thing`.constructor_object.number(42), `Mixing MemberExpression token types`);
assert.areEqual(`real string`, new new tagReturnNestedConstructorObject`ignored`[`constructor_nested`](`also ignored`)`one more ignored thing`.constructor_object.string(`Real String`).toLowerCase(), `Mixing MemberExpression token types`);
assert.isTrue(Symbol === tagReturnNestedConstructorObject`ignored`[`constructor_nested`]`also ignored``one more ignored thing`[`constructor_object`].symbol, `Mixing MemberExpression token types`);
assert.isTrue(Array.isArray(new tagReturnNestedConstructorObject`ignored`[`constructor_nested`]`also ignored``one more ignored thing`[`constructor_object`].array), `Mixing MemberExpression token types`);
assert.areEqual(`object`, typeof new tagReturnNestedConstructorObject`ignored`[`constructor_nested`]`also ignored``one more ignored thing`[`constructor_object`].number(42), `Mixing MemberExpression token types`);
assert.areEqual(`real string`, new tagReturnNestedConstructorObject`ignored`[`constructor_nested`]`also ignored``one more ignored thing`[`constructor_object`].string(`Real String`).toLowerCase(), `Mixing MemberExpression token types`);
assert.isTrue(Symbol === tagReturnNestedConstructorObject`ignored`[`constructor_nested`]`also ignored``one more ignored thing`[`constructor_object`][`symbol`], `Mixing MemberExpression token types`);
assert.isTrue(Array.isArray(new tagReturnNestedConstructorObject`ignored`[`constructor_nested`]`also ignored``one more ignored thing`[`constructor_object`][`array`]), `Mixing MemberExpression token types`);
assert.areEqual(`object`, typeof new tagReturnNestedConstructorObject`ignored`[`constructor_nested`]`also ignored``one more ignored thing`[`constructor_object`][`number`](42), `Mixing MemberExpression token types`);
assert.areEqual(`real string`, new tagReturnNestedConstructorObject`ignored`[`constructor_nested`]`also ignored``one more ignored thing`[`constructor_object`][`string`](`Real String`).toLowerCase(), `Mixing MemberExpression token types`);
assert.throws(function() { new tagReturnNestedConstructorArray`ignored`[0]; }, TypeError, `Array access returns Symbol which throws when called as new expression`, `Function is not a constructor`);
assert.isTrue(Array.isArray(new tagReturnNestedConstructorArray`ignored`[2]), `Array access returning Array used in a new expression`);
assert.areEqual(Object(42), new tagReturnNestedConstructorArray`ignored`[3](42), `Array access returning Number used in a new expression`);
assert.isTrue(Array.isArray(new tagReturnNestedConstructorArrayWrapper`ignored`[0]`also ignored`[2]), `Array access returning Array used in a new expression`);
assert.areEqual(`string`, new tagReturnNestedConstructorArrayWrapper`ignored`[0]`also ignored`[1](`STRING`).toLowerCase(), `Array access returning String used in a new expression`);
assert.areEqual(`string objectstring literal,,,`, String.raw`${new tagReturnNestedConstructorArray`ignored${String.raw`nothing`}`[1](`string object`)}string literal${new tagReturnNestedConstructorObject`ignored`[`constructor_object`][`array`](4)}`, `Nested string templates can have their own new expressions`);
function tagReturnNestedConstructorObjectWrapper(callsite, expr1, expr2) {
assert.areEqual([`string literal (pre)`,`string literal (middle)`,`string literal (post)`], callsite, `Callsite cooked string constants are correct`);
assert.areEqual([`string literal (pre)`,`string literal (middle)`,`string literal (post)`], callsite.raw, `Callsite raw string constants are correct`);
assert.areEqual(Object(`s1bprereplacement1s1bpost`), expr1, `First replacement expression is created via 'new String("...")'`);
assert.isTrue(Array.isArray(expr2), `Second replacement expression is created via Array constructor`)
assert.areEqual(new Array(6), expr2, `Second replacement expression has the right size and contents`);
return function() { return tagReturnNestedConstructorObject(); }
}
assert.isTrue(Array.isArray(
new tagReturnNestedConstructorObjectWrapper
`string literal (pre)${new tagReturnNestedConstructorArray`ignored${String.raw`ignored replacement`}ignored`[1](`s1bpre${String.raw`replacement1`}s1bpost`)}string literal (middle)${new tagReturnNestedConstructorObject`s2pre${new Number(42)}s2post`[`constructor_object`][`array`](6)}string literal (post)``ignored`
[`constructor_object`][`array`]
),
`Many-levels of nested string templates can all have different expression types - NewExpressions, MemberExpressions, PrimaryExpressions`
)
var _counter = 0;
function nestedNewOperatorFunction(callsite) {
if (_counter > 2) {
return tagReturnNestedConstructorFunction(callsite ? callsite[0] : ``);
} else {
_counter++;
return nestedNewOperatorFunction;
}
}
var obj = new new new new new nestedNewOperatorFunction
assert.areEqual(`constructed object`, obj.name, `Calling nested constructor helper with no arguments eventually returns a constructor which returns an object with name property`);
_counter = 0;
assert.areEqual(new String(`help...`), new new new new new nestedNewOperatorFunction(`1`)(`2`)(`3`)([`string`])(`help...`), `Nested constructor wrapper chooses String builtin constructor`)
_counter = 0;
assert.isTrue(Array.isArray(new nestedNewOperatorFunction`1``2``3``array`), `Nested constructor wrapper is called inherently by string template evaluation - empty arguments`)
_counter = 0;
assert.areEqual(new String(`help...`), new nestedNewOperatorFunction`1``2``3``string`(`help...`), `Nested constructor wrapper is called inherently by string template evaluation - passing arguments to NewExpression`)
}
},
{
name: "Multiple post-fix operators in a row including string templates",
body: function() {
function foo() {
return { 'something' : function() { return ['v1','v2','v3']; } };
}
assert.areEqual('v3', foo`'nothing'`['something']()[2], "Multiple post-fix operators after a string template");
function tag(callsite, replacement) {
return {
v : callsite[0] + " " + replacement,
f : function() { return ['a1', function(s) { return s; }]; }
};
}
assert.areEqual('another string template - ', tag`a ${'b'} c`.f()[1]`another string template - ${'with replacements'}`[0], "Multiple post-fix operators including multiple string templates");
}
},
{
name: "String template callsite objects are unique per-Realm and indexed by the raw strings",
body: function() {
var callsite = undefined;
var counter = 0;
function tag(c)
{
counter++;
assert.areEqual('uniquestringforrealmcachetest\\n', c.raw[0], 'String template callsite has correct raw value');
if (callsite === undefined) {
callsite = c;
} else {
assert.isTrue(c === callsite, 'Callsite is correctly cached per-Realm');
}
}
function foo() {
tag`uniquestringforrealmcachetest\n`;
tag`uniquestringforrealmcachetest\n`;
}
foo();
foo();
function foo2() {
tag`uniquestringforrealmcachetest\n`;
tag`uniquestringforrealmcachetest\n`;
}
foo2();
foo2();
function foo3() {
eval('tag`uniquestringforrealmcachetest\\n`');
eval('tag`uniquestringforrealmcachetest\\n`');
}
foo3();
foo3();
counter = 0;
var foo4 = new Function('t','t`uniquestringforrealmcachetest\\n`;');
foo4(tag);
foo4(tag);
assert.areEqual(2, counter, "tag function is called correct number of times");
counter = 0;
var foo5 = new Function('t','eval("t`uniquestringforrealmcachetest\\\\n`;");');
foo5(tag);
foo5(tag);
assert.areEqual(2, counter, "tag function is called correct number of times");
}
},
{
name: "Callsite objects are not strict equal if raw strings differ (even when cooked strings are strict equal)",
body: function() {
var callsite1 = GetCallsite`before
after`;
var callsite2 = GetCallsite`before\nafter`;
assert.areEqual("before\nafter", callsite1[0], 'Explicit line terminator character is copied directly into cooked strings');
assert.areEqual("before\nafter", callsite2[0], 'Escaped line terminator is translated into cooked strings');
assert.areEqual("before\nafter", callsite1.raw[0], 'Explicit line terminator character is copied directly into raw strings');
assert.areEqual("before\\nafter", callsite2.raw[0], 'Escaped line terminator is re-escaped into raw strings');
assert.isTrue(callsite1[0] === callsite2[0], 'Cooked strings are strictly equal');
assert.isFalse(callsite1.raw[0] === callsite2.raw[0], 'Raw strings are not strictly equal');
assert.isFalse(callsite1 === callsite2, 'Callsite objects are not the same ');
}
},
{
name: "Callsite objects are constant even if replacement values differ",
body: function() {
var callsite1 = GetCallsite`string1${'r1'}string2${'r2'}string3`;
var callsite2 = GetCallsite`string1${'r3'}string2${'r4'}string3`;
assert.isTrue(callsite1 === callsite2, "Callsite objects are strictly equal");
}
},
{
name: "Octal escape sequences are not allowed in string template literals",
body: function() {
assert.throws(function () { eval('print(`\\00`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\01`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\1`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\2`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\3`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\4`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\5`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\6`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\7`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\10`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\50`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\30`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\70`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\123`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\377`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\0123`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
assert.throws(function () { eval('print(`\\567`)'); }, SyntaxError, "Scanning an octal escape sequence throws SyntaxError.", "Octal numeric literals and escape characters not allowed in strict mode");
}
},
{
name: "Extended unicode escape sequences",
body: function() {
assert.areEqual("a\u{d}c", `a\u{d}c`, "Cooked value for extended unicode escape sequence of 1 hex character");
assert.areEqual("a\\u{d}c", GetRawStringValue`a\u{d}c${0}`, "Raw value for extended unicode escape sequence of 1 hex character");
assert.areEqual("a\u{62}c", `a\u{62}c`, "Cooked value for extended unicode escape sequence of 2 hex characters");
assert.areEqual("a\\u{62}c", GetRawStringValue`a\u{62}c${0}`, "Raw value for extended unicode escape sequence of 2 hex characters");
assert.areEqual("a\u{062}c", `a\u{062}c`, "Cooked value for extended unicode escape sequence of 3 hex characters");
assert.areEqual("a\\u{062}c", GetRawStringValue`a\u{062}c${0}`, "Raw value for extended unicode escape sequence of 3 hex characters");
assert.areEqual("a\u{0062}c", `a\u{0062}c`, "Cooked value for extended unicode escape sequence of 4 hex characters");
assert.areEqual("a\\u{0062}c", GetRawStringValue`a\u{0062}c${0}`, "Raw value for extended unicode escape sequence of 4 hex characters");
assert.areEqual("a\u{00062}c", `a\u{00062}c`, "Cooked value for extended unicode escape sequence of 5 hex characters");
assert.areEqual("a\\u{00062}c", GetRawStringValue`a\u{00062}c${0}`, "Raw value for extended unicode escape sequence of 5 hex characters");
assert.areEqual("a\u{000062}c", `a\u{000062}c`, "Cooked value for extended unicode escape sequence of 6 hex characters");
assert.areEqual("a\\u{000062}c", GetRawStringValue`a\u{000062}c${0}`, "Raw value for extended unicode escape sequence of 6 hex characters");
assert.areEqual("a\u{00000062}c", `a\u{00000062}c`, "Cooked value for extended unicode escape sequence of 7 hex characters");
assert.areEqual("a\\u{00000062}c", GetRawStringValue`a\u{00000062}c${0}`, "Raw value for extended unicode escape sequence of 7 hex characters");
assert.areEqual("a\u{000000062}c", `a\u{000000062}c`, "Cooked value for extended unicode escape sequence of 8 hex characters");
assert.areEqual("a\\u{000000062}c", GetRawStringValue`a\u{000000062}c${0}`, "Raw value for extended unicode escape sequence of 8 hex characters");
}
},
{
name: "Template value for escaped null characters",
body: function() {
assert.areEqual("\0", eval("GetRawStringValue`\0${0}`"), "The template raw value of <NULL> is <NULL>");
assert.areEqual("\\\u0030", eval("GetRawStringValue`\\\0${0}`"), "The template raw value of \\<NULL> is \\ + \u0030");
assert.areEqual("\0", eval("GetStringValue`\0${0}`"), "The template cooked value of <NULL> is <NULL>");
assert.areEqual("\0", eval("GetStringValue`\\\0${0}`"), "The template cooked value of \\<NULL> is <NULL>");
}
},
{
name: "String template line terminator sequence normalization",
body: function() {
assert.isTrue(eval("GetStringValue`\n${0}`") === "\n", "String template literal doesn't normalize <LF> line terminator sequence"); // `<LF>` => '<LF>'
assert.isTrue(eval("GetStringValue`\\\n${0}`") === "", "String template literal ignores escaped <LF> line continuation token"); // `\<LF>` => ''
assert.isTrue(eval("GetStringValue`\\n${0}`") === "\n", "String template literal doesn't normalize <LF> escape sequence"); // `\n` => '<LF>'
assert.isTrue(eval("GetStringValue`\r${0}`") === "\n", "String template literal normalizes <CR> line terminator sequence to <LF>"); // `<CR>` => '<LF>'
assert.isTrue(eval("GetStringValue`\\\r${0}`") === "", "String template literal ignores escaped <CR> line continuation token"); // `\<CR>` => ''
assert.isTrue(eval("GetStringValue`\\r${0}`") === "\r", "String template literal doesn't normalize <CR> escape sequence"); // `\r` => '<CR>'
assert.isTrue(eval("GetStringValue`\r\n${0}`") === "\n", "String template literal normalizes <CR><LF> line terminator sequence to <LF>"); // `<CR><LF>` => '<LF>'
assert.isTrue(eval("GetStringValue`\\\r\\\n${0}`") === "", "String template literal ignores separately escaped <CR><LF> line continuation sequence tokens"); // `\<CR>\<LF>` => ''
assert.isTrue(eval("GetStringValue`\\\r\n${0}`") === "", "String template literal ignores escaped <CR><LF> line continuation sequence"); // `\<CR><LF>` => ''
assert.isTrue(eval("GetStringValue`\r\\\n${0}`") === "\n", "String template literal normalizes <CR> line terminator sequence if the next character is <LF> line continuation sequence"); // `<CR>\<LF>` => '<LF>'
assert.isTrue(eval("GetStringValue`\\r\\n${0}`") === "\r\n", "String template literal doesn't normalize <CR><LF> as escape sequences"); // `\r\n` => '<CR><LF>'
assert.isTrue(eval("GetStringValue`\\r\n${0}`") === "\r\n", "String template literal doesn't normalize <CR> as an escape sequence if the next character is <LF> line terminator sequence"); // `\r<LF>` => '<CR><LF>'
assert.isTrue(eval("GetStringValue`\r\\n${0}`") === "\n\n", "String template literal normalizes <CR> as a line terminator sequence if the next character is <LF> escape sequence"); // `<CR>\n` => '<LF><LF>'
assert.isTrue(eval("GetStringValue`\\\r\\n${0}`") === "\n", "String template literal ignores <CR> as a line continuation if the next character is <LF> escape sequence"); // `\<CR>\n` => '<LF>'
assert.isTrue(eval("GetStringValue`\\r\\\n${0}`") === "\r", "String template literal doesn't normalize <CR> as an escape sequence if the next character is <LF> line continuation token"); // `\r\<LF>` => '<CR>'
assert.isTrue(eval("GetStringValue`\u2028${0}`") === "\u2028", "String template literal doesn't normalize <LS> line terminator sequence"); // `<LS>` => '<LS>'
assert.isTrue(eval("GetStringValue`\\\u2028${0}`") === "", "String template literal ignores escaped <LS> line continuation token"); // `\<LS>` => ''
assert.isTrue(eval("GetStringValue`\u2029${0}`") === "\u2029", "String template literal doesn't normalize <PS> line terminator sequence"); // `<PS>` => '<PS>'
assert.isTrue(eval("GetStringValue`\\\u2029${0}`") === "", "String template literal ignores escaped <PS> line continuation token"); // `\<PS>` => ''
}
},
{
name: "String template line terminator sequence normalization in raw string values",
body: function() {
assert.isTrue(eval("GetRawStringValue`\n${0}`") === '\n', "String template raw literal doesn't normalize <LF> line terminator sequence"); // `<LF>`.raw => '<LF>'
assert.isTrue(eval("GetRawStringValue`\\\n${0}`") === "\\\n", "String template raw literal doesn't normalize escaped <LF> line continuation token"); // `\<LF>`.raw => '\<LF>'
assert.isTrue(eval("GetRawStringValue`\\n${0}`") === "\\n", "String template raw literal doesn't evaluate <LF> escape sequence"); // `\n`.raw => '\n'
assert.isTrue(eval("GetRawStringValue`\r${0}`") === "\n", "String template raw literal normalizes <CR> line terminator sequence to <LF>"); // `<CR>`.raw => '<LF>'
assert.isTrue(eval("GetRawStringValue`\\\r${0}`") === "\\\n", "String template raw literal normalizes escaped <CR> line continuation token to <LF>"); // `\<CR>`.raw => '\<LF>'
assert.isTrue(eval("GetRawStringValue`\\r${0}`") === "\\r", "String template raw literal doesn't evaluate <CR> escape sequence"); // `\r`.raw => '\r'
assert.isTrue(eval("GetRawStringValue`\r\n${0}`") === "\n", "String template raw literal normalizes <CR><LF> line terminator sequence to <LF>"); // `<CR><LF>`.raw => '<LF>'
assert.isTrue(eval("GetRawStringValue`\\\r\\\n${0}`") === "\\\n\\\n", "String template raw literal normalizes separately escaped <CR><LF> line continuation sequence tokens"); // `\<CR>\<LF>`.raw => '\<LF>\<LF>'
assert.isTrue(eval("GetRawStringValue`\\\r\n${0}`") === "\\\n", "String template raw literal normalizes escaped <CR><LF> line continuation sequence to <LF>"); // `\<CR><LF>`.raw => '\<LF>'
assert.isTrue(eval("GetRawStringValue`\r\\\n${0}`") === "\n\\\n", "String template raw literal normalizes <CR> line terminator sequence if the next character is <LF> line continuation sequence"); // `<CR>\<LF>`.raw => '<LF>\<LF>'
assert.isTrue(eval("GetRawStringValue`\\r\\n${0}`") === "\\r\\n", "String template raw literal doesn't evaluate <CR><LF> as escape sequences"); // `\r\n`.raw => '\r\n'
assert.isTrue(eval("GetRawStringValue`\\r\n${0}`") === "\\r\n", "String template raw literal doesn't evaluate <CR> as an escape sequence if the next character is <LF> line terminator sequence"); // `\r<LF>`.raw => '\r<LF>'
assert.isTrue(eval("GetRawStringValue`\r\\n${0}`") === "\n\\n", "String template raw literal normalizes <CR> as a line terminator sequence if the next character is <LF> escape sequence"); // `<CR>\n`.raw => '<LF>\n'
assert.isTrue(eval("GetRawStringValue`\\\r\\n${0}`") === "\\\n\\n", "String template raw literal normalizes <CR> as a line continuation if the next character is <LF> escape sequence"); // `\<CR>\n`.raw => '\<LF>\n'
assert.isTrue(eval("GetRawStringValue`\\r\\\n${0}`") === "\\r\\\n", "String template raw literal doesn't evaluate <CR> as an escape sequence if the next character is <LF> line continuation token"); // `\r\<LF>`.raw => '\r\<LF>'
assert.isTrue(eval("GetRawStringValue`\u2028${0}`") === "\u2028", "String template raw literal doesn't normalize <LS> line terminator sequence"); // `<LS>`.raw => '<LS>'
assert.isTrue(eval("GetRawStringValue`\\\u2028${0}`") === "\\\u2028", "String template raw literal doesn't normalize escaped <LS> line continuation token"); // `\<LS>`.raw => '\<LS>'
assert.isTrue(eval("GetRawStringValue`\u2029${0}`") === "\u2029", "String template raw literal doesn't normalize <PS> line terminator sequence"); // `<PS>`.raw => '<PS>'
assert.isTrue(eval("GetRawStringValue`\\\u2029${0}`") === "\\\u2029", "String template raw literal doesn't normalize escaped <PS> line continuation token"); // `\<PS>`.raw => '\<PS>'
}
},
{
name: "Bug fix : 4532336 String Template should trigger ToString on the substitution expression",
body: function() {
var a = {
toString: function (){ return "foo";},
valueOf: function() { return "bar";}
};
assert.areEqual(`${a}`, "foo", "toString should be called instead of valueOf on the substitution expression");
}
},
{
name: "String template converts `\\0` into a null character - not an octal escape sequence",
body: function() {
assert.areEqual('\0', `\0`, "Simple null escape sequence in string template is treated the same as in a normal string");
assert.areEqual('\08', `\08`, "Null escape sequence followed by non-octal number is valid");
assert.areEqual('\0abc', `\0abc`, "Null escape sequence followed by non-number is valid");
assert.areEqual('\0\0', `\0\0`, "Null escape sequence followed by another null escape sequence is valid");
var called = false;
function tag(obj) {
assert.areEqual('\0', obj[0], "Null escape sequence is cooked into null character");
assert.areEqual('\\0', obj.raw[0], "Null escape sequence is not cooked in raw string");
called = true;
}
tag`\0`;
assert.isTrue(called);
}
},
];
testRunner.runTests(tests, { verbose: WScript.Arguments[0] != "summary" });