beforeEach(function() { global.disableOptInFeatures(); global.addToEqualMatchMatcher(); }); describe('When overridden, RegExp.prototype.exec()', function() { beforeEach(function() { XRegExp.install('natives'); }); // This is broken in old Firefox (tested in v2.0; it works in v8+), but not for any fault of // XRegExp. Uncomment this test if future XRegExp fixes it for old Firefox. /*it('should type convert an undefined argument to a string', function() { expect(/undefined/.exec()).toEqualMatch(['undefined']); });*/ /* * NOTE: These specs should mirror those for RegExp.prototype.test, nonglobal * String.prototype.match, XRegExp.exec, and XRegExp.test as closely as possible. */ // TODO: Copy/update specs from XRegExp.exec here /* * The following specs: * - Have no corresponding specs for XRegExp.exec and XRegExp.test. * - Are mirrored by RegExp.prototype.test and nonglobal String.prototype.match. */ it('should ignore lastIndex and set the search start position at 0 for a nonglobal regex', function() { var regex = /x/; regex.lastIndex = 4; expect(regex.exec('123x5')).toEqualMatch(['x']); }); /* * The following specs: * - Have no corresponding specs for XRegExp.exec, XRegExp.test, and nonglobal * String.prototype.match. * - Are mirrored by RegExp.prototype.test. */ it('should use lastIndex to set the search start position for a global regex', function() { var regex = /x/g; regex.lastIndex = 4; expect(regex.exec('123x5')).toBeNull(); regex.lastIndex = 2; expect(regex.exec('123x5')).toEqualMatch(['x']); }); it('should type convert lastIndex when setting the search start position', function() { var regex = /x/g; regex.lastIndex = '3'; expect(regex.exec('123x5')).toEqualMatch(['x']); regex.lastIndex = '4'; expect(regex.exec('123x5')).toBeNull(); }); /* * The following specs: * - Have no corresponding specs for RegExp.prototype.test and XRegExp.test. * - Are mirrored by XRegExp.exec and nonglobal String.prototype.match. */ xdescribe('provides extensions:', function() { // TODO: Copy/update named capture specs from XRegExp.exec here }); }); describe('When overridden, RegExp.prototype.test()', function() { beforeEach(function() { XRegExp.install('natives'); }); /* * NOTE: These specs should mirror those for RegExp.prototype.exec, nonglobal * String.prototype.match, XRegExp.test, and XRegExp.exec as closely as possible. */ // TODO: Copy/update specs from XRegExp.test here /* * The following specs: * - Have no corresponding specs for XRegExp.test and XRegExp.exec. * - Are mirrored by RegExp.prototype.exec and nonglobal String.prototype.match. */ it('should ignore lastIndex and set the search start position at 0 for a nonglobal regex', function() { var regex = /x/; regex.lastIndex = 4; expect(regex.test('123x5')).toBe(true); }); /* * The following specs: * - Have no corresponding specs for XRegExp.test, XRegExp.exec, and nonglobal * String.prototype.match. * - Are mirrored by RegExp.prototype.exec. */ it('should use lastIndex to set the search start position for a global regex', function() { var regex = /x/g; regex.lastIndex = 4; expect(regex.test('123x5')).toBe(false); regex.lastIndex = 2; expect(regex.test('123x5')).toBe(true); }); it('should type convert lastIndex when setting the search start position', function() { var regex = /x/g; regex.lastIndex = '3'; expect(regex.test('123x5')).toBe(true); regex.lastIndex = '4'; expect(regex.test('123x5')).toBe(false); }); // NOTE: The fixed RegExp.prototype.test does not provide any extensions to native handling }); describe('When overridden, String.prototype.match()', function() { beforeEach(function() { XRegExp.install('natives'); }); describe('with a global regex', function() { it('should return an array with all matches', function() { expect('a bc'.match(/(\w)/g)).toEqualMatch(['a', 'b', 'c']); }); it('should return null if no match is found', function() { expect('a bc'.match(/x/g)).toBeNull(); }); it('should reset lastIndex to 0 when a match is found', function() { var regex = /x/g; regex.lastIndex = 1; '123x5'.match(regex); expect(regex.lastIndex).toBe(0); }); it('should reset lastIndex to 0 when no match is found', function() { var regex = /x/g; regex.lastIndex = 1; '123'.match(regex); expect(regex.lastIndex).toBe(0); }); it('should start the search at the beginning of the string, ignoring lastIndex', function() { var regex = /x/g; regex.lastIndex = 4; expect('123x5'.match(regex)).toBeTruthy(); }); it('should convert any nonstring context to a string (except null and undefined)', function() { expect(String.prototype.match.call(11, /1/g)).toEqualMatch(['1', '1']); }); it('should throw an exception when called on null or undefined context, if strict mode is supported', function() { [null, undefined].forEach(function(value) { // This doesn't work the same when strict mode isn't supported, because the match // method will be called with the global object (window) as its context, rather // than null or undefined if (hasStrictMode) { expect(function() {String.prototype.match.call(value, /x/g);}).toThrowError(TypeError); } else { // Keep the assertion count consistent cross-browser expect(hasStrictMode).toBe(false); } }); }); }); describe('with a nonglobal regex', function() { /* * NOTE: These specs should mirror those for RegExp.prototype.exec, RegExp.prototype.test, * XRegExp.exec, and XRegExp.test as closely as possible. */ // TODO: Copy/update specs from RegExp.prototype.exec here /* * The following specs: * - Have no corresponding specs for RegExp.prototype.exec, RegExp.prototype.test, * XRegExp.test, and XRegExp.exec. */ it('should convert any provided non RegExp object to a RegExp', function() { // These don't error because, per the spec, the values are passed through new RegExp() // before being used as the context object for the (fixed) RegExp.prototype.exec var tests = [ {str: '12', regex: '^(1)', result: ['1', '1']}, // This would throw if the string was converted to an XRegExp rather than RegExp {str: '\x01', regex: '\\1', result: ['\x01']}, // The converted value '[object Object]' creates a character class {str: '[obj]', regex: {}, result: ['o']}, {str: 'null', regex: null, result: ['null']} ]; tests.forEach(function(test) { expect(test.str.match(test.regex)).toEqualMatch(test.result); }); // The native RegExp with an explicit undefined pattern incorrectly creates /undefined/ // in Firefox 3 and 3.6 (but not v4) //expect('undefined'.match()).toEqualMatch(['']); //expect('undefined'.match(undefined)).toEqualMatch(['']); }); it('should throw an exception when called on null or undefined context, if strict mode is supported', function() { [null, undefined].forEach(function(value) { // This doesn't work the same when strict mode isn't supported, because the match // method will be called with the global object (window) as its context, rather // than null or undefined if (hasStrictMode) { expect(function() {String.prototype.match.call(value, /x/);}).toThrowError(TypeError); } else { // Keep the assertion count consistent cross-browser expect(hasStrictMode).toBe(false); } }); }); /* * The following specs: * - Have no corresponding specs for RegExp.prototype.test and XRegExp.test. * - Are mirrored by RegExp.prototype.exec and XRegExp.exec. */ xdescribe('provides extensions:', function() { // TODO: Copy/update specs from RegExp.prototype.exec here }); }); }); describe('When overridden, String.prototype.replace()', function() { beforeEach(function() { XRegExp.install('natives'); }); /* * NOTE: These specs should mirror those for XRegExp.replace as closely as possible. */ it('should replace the first match only when given a nonglobal regex', function() { expect('aaa'.replace(/a/, 'b')).toBe('baa'); }); it('should replace all matches when given a global regex', function() { expect('aaa'.replace(/a/g, 'b')).toBe('bbb'); }); it('should replace the first match only when given a string as the search pattern', function() { expect('aaa'.replace('a', 'b')).toBe('baa'); }); it('should not type convert a string search pattern to a regex', function() { expect('aaa'.replace('a(a)', 'b')).toBe('aaa'); expect('a(a)a'.replace('a(a)', 'b')).toBe('ba'); }); it('should handle single-digit backreference $1 in the replacement string', function() { expect('aaa'.replace(/a(a)/, '$1b')).toBe('aba'); // Backreference to a nonparticipating capturing group expect('test'.replace(/t|(e)/g, '$1')).toBe('es'); }); it('should handle double-digit backreferences $01, $10, and $99 in the replacement string', function() { expect('aaa'.replace(/a(a)/, '$01b')).toBe('aba'); expect('aaa'.replace(new RegExp('a' + repeat('()', 9) + '(a)'), '$10b')).toBe('aba'); expect('aaa'.replace(new RegExp('a' + repeat('()', 98) + '(a)'), '$99b')).toBe('aba'); }); it('should end backreferences in the replacement string after two digits', function() { expect('aaa'.replace(new RegExp('a' + repeat('()', 99) + '(a)'), '$100b')).toBe('0ba'); }); // NOTE: IE < 9 incorrectly treats all occurrences of $ as literal text when performing a // replacement based on a search value that is not a regex. XRegExp restores the special // meaning of $$, $&, etc. for all replacements. it('should handle backreference $& in the replacement string', function() { expect('aaa'.replace(/aa/, '$&b')).toBe('aaba'); expect('aaa'.replace('aa', '$&b')).toBe('aaba'); }); it("should handle right context token $' in the replacement string", function() { expect('aaa'.replace(/aa/, "$'b")).toBe('aba'); expect('aaa'.replace('aa', "$'b")).toBe('aba'); }); it('should handle left context token $` in the replacement string', function() { expect('xaaa'.replace(/aa/, '$`b')).toBe('xxba'); expect('xaaa'.replace('aa', '$`b')).toBe('xxba'); }); it('should handle token $$ in the replacement string', function() { expect('aaa'.replace(/aa/, '$$b')).toBe('$ba'); expect('aaa'.replace('aa', '$$b')).toBe('$ba'); }); it('should allow a function to generate the replacement', function() { expect('aaa'.replace(/a/, function() {return 'b';})).toBe('baa'); expect('aaa'.replace(/a/g, function() {return 'b';})).toBe('bbb'); expect('aaa'.replace('a', function() {return 'b';})).toBe('baa'); }); it('should allow using backreferences with replacement functions', function() { expect('aaa'.replace(/aa/, function($0) {return $0 + 'b';})).toBe('aaba'); expect('aaa'.replace(/a(a)/, function($0, $1) {return $1 + 'b';})).toBe('aba'); expect('aaa'.replace('aa', function($0) {return $0 + 'b';})).toBe('aaba'); }); // Firefox (last tested v14.0.1) provides empty strings instead of undefined, but not for any // fault of XRegExp /*it('should provide undefined within replacement functions for nonparticipating capturing groups', function() { expect('y'.replace(/(x)?y/, function($0, $1) {return String($1);})).toBe('undefined'); expect('y'.replace(/(x)?y/, function($0, $1) {return $1;})).toBe('undefined'); });*/ it('should not substitute tokens returned by replacement functions', function() { // Regex search... expect('aaa'.replace(/a(a)/, function($0, $1) {return '$1';})).toBe('$1a'); expect('aaa'.replace(/a/, function() {return '$&';})).toBe('$&aa'); // String search... // This is broken in Safari (tested in Safari 5.1.2/7534.52.7), but not for any fault of // XRegExp. Uncomment this test if future XRegExp fixes it for Safari. //expect('aaa'.replace('a', function() {return '$&';})).toBe('$&aa'); }); it('should allow using the match position within replacement functions', function() { expect('xaaa'.replace(/a/, function($0, pos) {return '' + pos;})).toBe('x1aa'); expect('xaaa'.replace(/a/g, function($0, pos) {return '' + pos;})).toBe('x123'); expect('xaaa'.replace(/(a)/g, function($0, $1, pos) {return '' + pos;})).toBe('x123'); expect('xaaa'.replace('a', function($0, pos) {return '' + pos;})).toBe('x1aa'); }); it('should allow using the source string within replacement functions', function() { expect('xaaa'.replace(/a/, function($0, pos, str) {return str;})).toBe('xxaaaaa'); expect('xaaa'.replace(/(a)/, function($0, $1, pos, str) {return str;})).toBe('xxaaaaa'); expect('xaaa'.replace('a', function($0, pos, str) {return str;})).toBe('xxaaaaa'); }); it('should return string as the typeof the last argument in replacement functions', function() { // NOTE: This tests a fix for IE < 9, which doesn't get this correct natively expect('100'.replace(/0/, function($0, pos, str) {return typeof str;})).toBe('1string0'); expect(new String('100').replace(/0/, function($0, pos, str) {return typeof str;})).toBe('1string0'); expect(String.prototype.replace.call(100, /0/, function($0, pos, str) {return typeof str;})).toBe('1string0'); }); it('should handle nonstring context when using a replacement text token that references the subject text', function() { expect(String.prototype.replace.call(0, /^/, '$`')).toBe('0'); }); it('should not modify the lastIndex of a nonglobal regex', function() { var regex = /x/; '123x567'.replace(regex, '_'); expect(regex.lastIndex).toBe(0); regex.lastIndex = 1; '123x567'.replace(regex, '_'); expect(regex.lastIndex).toBe(1); 'nomatch'.replace(regex, '_'); expect(regex.lastIndex).toBe(1); }); it('should reset the lastIndex of a global regex to 0', function() { var regex = /x/g; regex.lastIndex = 1; '123x567'.replace(regex, '_'); expect(regex.lastIndex).toBe(0); regex.lastIndex = 1; 'nomatch'.replace(regex, '_'); expect(regex.lastIndex).toBe(0); }); it('should ignore lastIndex when setting the search start position', function() { [/x/, /x/g].forEach(function(regex) { regex.lastIndex = 5; expect('123x567'.replace(regex, '_')).toBe('123_567'); }); }); it('should update lastIndex during replacement iterations', function() { var regex = /x/g; var interimLastIndex = 0; '1x2'.replace(regex, function() { interimLastIndex = regex.lastIndex; }); expect(interimLastIndex).toBe(2); }); it('should convert any provided nonstring search to a string', function() { var values = [ {target: '10x10', search: 10, replacement: 'x', expected: 'xx10'}, {target: 'xaaa,ba,b', search: ['a','b'], replacement: 'x', expected: 'xaaxa,b'}, {target: 'undefined', search: undefined, replacement: 'x', expected: 'x'} ]; values.forEach(function(value) { expect(value.target.replace(value.search, value.replacement)).toBe(value.expected); }); // Implicit undefined search and replacement expect('undefined'.replace()).toBe('undefined'); }); it('should convert any provided nonstring/nonfunction replacement to a string', function() { var values = [ {target: 'xaaa', search: /a/g, replacement: 1.1, expected: 'x1.11.11.1'}, {target: 'xaaa', search: /a/g, replacement: ['a','b'], expected: 'xa,ba,ba,b'}, {target: 'x', search: /x/, replacement: /x/, expected: '/x/'}, {target: 'xaaa', search: /a/, replacement: undefined, expected: 'xundefinedaa'} ]; values.forEach(function(value) { expect(value.target.replace(value.search, value.replacement)).toBe(value.expected); }); // Implicit undefined replacement expect('xaaa'.replace(/a/)).toBe('xundefinedaa'); }); it('should convert any nonstring context to a string (except null and undefined)', function() { var values = [ 100, {}, true, false, NaN, ['a'] ]; values.forEach(function(value) { expect(String.prototype.replace.call(value, /^/, 'x')).toBe('x' + value); }); }); it('should throw an exception when called on null or undefined context, if strict mode is supported', function() { [null, undefined].forEach(function(value) { // This doesn't work the same when strict mode isn't supported, because the replace // method will be called with the global object (window) as its context, rather // than null or undefined if (hasStrictMode) { expect(function() {String.prototype.replace.call(value, /^/, '');}).toThrowError(TypeError); } else { // Keep the assertion count consistent cross-browser expect(hasStrictMode).toBe(false); } }); }); describe('provides extensions:', function() { it('should allow accessing named backreferences in a callback function as properties of the first argument', function() { expect('abc'.replace(XRegExp('(?.).'), function(match) { return ':' + match.name + ':'; })).toBe(':a:c'); }); describe('supports new replacement text syntax:', function() { describe('backreference $0', function() { it('should work like $&', function() { expect('xaaa'.replace(/aa/, '$0b')).toBe('xaaba'); expect('xaaa'.replace('aa', '$0b')).toBe('xaaba'); }); it('should be allowed as $00', function() { expect('xaaa'.replace(/aa/, '$00b')).toBe('xaaba'); }); it('should end after two zeros', function() { expect('xaaa'.replace(/aa/, '$000b')).toBe('xaa0ba'); expect('xaaa'.replace(/aa/, '$001b')).toBe('xaa1ba'); }); }); describe('named backreferences', function() { it('should return the named backreference', function() { expect('test'.replace(XRegExp('(?t)', 'g'), ':${test}:')).toBe(':t:es:t:'); expect('test'.replace(XRegExp('(?t)', 'g'), ':$:')).toBe(':t:es:t:'); // Backreference to a nonparticipating capturing group expect('test'.replace(XRegExp('t|(?t)', 'g'), ':${test}:')).toBe('::es::'); expect('test'.replace(XRegExp('t|(?t)', 'g'), ':$:')).toBe('::es::'); }); it('should throw an exception for backreferences to unknown group names', function() { expect(function() {'test'.replace(XRegExp('(?t)', 'g'), ':${x}:');}).toThrowError(SyntaxError); expect(function() {'test'.replace(XRegExp('(?t)', 'g'), ':$:');}).toThrowError(SyntaxError); }); }); describe('explicit numbered backreferences', function() { it('should return the numbered backreference', function() { expect('test'.replace(/(.)./g, '${1}')).toBe('ts'); expect('test'.replace(/(.)./g, '$<1>')).toBe('ts'); // Backreference to a nonparticipating capturing group expect('test'.replace(/t|(e)/g, '${1}')).toBe('es'); expect('test'.replace(/t|(e)/g, '$<1>')).toBe('es'); }); it('should allow leading zeros', function() { expect('test'.replace(/(.)./g, '${01}')).toBe('ts'); expect('test'.replace(/(.)./g, '$<01>')).toBe('ts'); expect('test'.replace(/(.)./g, '${001}')).toBe('ts'); expect('test'.replace(/(.)./g, '$<001>')).toBe('ts'); }); it('should return named backreferences by number', function() { expect('test'.replace(XRegExp('(?.).', 'g'), '${1}')).toBe('ts'); expect('test'.replace(XRegExp('(?.).', 'g'), '$<1>')).toBe('ts'); }); it('should separate numbered backreferences from following literal digits', function() { expect('test'.replace(new RegExp('(.).', 'g'), '${1}0')).toBe('t0s0'); expect('test'.replace(new RegExp('(.).', 'g'), '$<1>0')).toBe('t0s0'); expect('test'.replace(new RegExp('(.).' + repeat('()', 9), 'g'), '${1}0')).toBe('t0s0'); expect('test'.replace(new RegExp('(.).' + repeat('()', 9), 'g'), '$<1>0')).toBe('t0s0'); }); it('should throw an exception for backreferences to unknown group numbers', function() { expect(function() {'test'.replace(/t/, '${1}');}).toThrowError(SyntaxError); expect(function() {'test'.replace(/t/, '$<1>');}).toThrowError(SyntaxError); expect(function() {'test'.replace(/(t)/, '${2}');}).toThrowError(SyntaxError); expect(function() {'test'.replace(/(t)/, '$<2>');}).toThrowError(SyntaxError); }); it('should allow ${0} to refer to the entire match', function() { expect('test'.replace(/../g, '${0}:')).toBe('te:st:'); expect('test'.replace(/../g, '$<0>:')).toBe('te:st:'); expect('test'.replace(/../g, '${00}:')).toBe('te:st:'); expect('test'.replace(/../g, '$<00>:')).toBe('te:st:'); expect('test'.replace(/../g, '${000}:')).toBe('te:st:'); expect('test'.replace(/../g, '$<000>:')).toBe('te:st:'); }); it('should support backreferences 100 and greater, if the browser does natively', function() { // IE < 9 doesn't allow backreferences greater than \99 *within* a regex, but // XRegExp still allows backreferences to groups 100+ within replacement text try { // Regex with 1,000 capturing groups. This fails in Firefox 4-6 (but not v3.6 // or v7+) with `InternalError: regular expression too complex` var lottaGroups = new RegExp([ '^(a)\\1', repeat('()', 8), '(b)\\10', repeat('()', 89), '(c)', repeat('()', 899), '(d)$' ].join('')); expect('aabbcd'.replace(lottaGroups, '${0} ${01} ${001} ${0001} ${1} ${10} ${100} ${1000}')).toBe('aabbcd a a a a b c d'); expect('aabbcd'.replace(lottaGroups, '$<0> $<01> $<001> $<0001> $<1> $<10> $<100> $<1000>')).toBe('aabbcd a a a a b c d'); expect('aabbcd'.replace(lottaGroups, '$<0> ${01} $<001> ${0001} $<1> ${10} $<100> ${1000}')).toBe('aabbcd a a a a b c d'); // For comparison... expect('aabbcd'.replace(lottaGroups, '$0 $01 $001 $0001 $1 $10 $100 $1000')).toBe('aabbcd a aabbcd1 aabbcd01 a b b0 b00'); } catch (err) { // Keep the assertion count consistent cross-browser expect(true).toBe(true); expect(true).toBe(true); } }); }); describe('strict error handling', function() { it('should throw an exception for backreferences to unknown group numbers', function() { expect(function() {'xaaa'.replace(/aa/, '$1b');}).toThrowError(SyntaxError); expect(function() {'xaaa'.replace(/aa/, '$01b');}).toThrowError(SyntaxError); expect(function() {'xaaa'.replace(/a(a)/, '$2b');}).toThrowError(SyntaxError); expect(function() {'xa(a)a'.replace('a(a)', '$1b');}).toThrowError(SyntaxError); }); }); }); }); }); describe('When overridden, String.prototype.split()', function() { beforeEach(function() { XRegExp.install('natives'); }); /* * NOTE: These specs should mirror those for XRegExp.split as closely as possible. */ // TODO: Copy/update specs from XRegExp.split here // NOTE: The fixed String.prototype.split does not provide any extensions to native handling });