| <!DOCTYPE html> | |
| <!-- | |
| Copyright (C) 2016 The Android Open Source Project | |
| Licensed under the Apache License, Version 2.0 (the "License"); | |
| you may not use this file except in compliance with the License. | |
| You may obtain a copy of the License at | |
| http://www.apache.org/licenses/LICENSE-2.0 | |
| Unless required by applicable law or agreed to in writing, software | |
| distributed under the License is distributed on an "AS IS" BASIS, | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| See the License for the specific language governing permissions and | |
| limitations under the License. | |
| --> | |
| <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> | |
| <title>gr-diff-builder</title> | |
| <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> | |
| <script src="../../../bower_components/web-component-tester/browser.js"></script> | |
| <script src="../../../scripts/util.js"></script> | |
| <script src="../gr-diff/gr-diff-line.js"></script> | |
| <script src="../gr-diff/gr-diff-group.js"></script> | |
| <script src="../gr-diff-highlight/gr-annotation.js"></script> | |
| <script src="gr-diff-builder.js"></script> | |
| <link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html"> | |
| <link rel="import" href="gr-diff-builder.html"> | |
| <test-fixture id="basic"> | |
| <template> | |
| <gr-diff-builder> | |
| <table id="diffTable"></table> | |
| </gr-diff-builder> | |
| </template> | |
| </test-fixture> | |
| <test-fixture id="div-with-text"> | |
| <template> | |
| <div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div> | |
| </template> | |
| </test-fixture> | |
| <test-fixture id="mock-diff"> | |
| <template> | |
| <gr-diff-builder view-mode="SIDE_BY_SIDE"> | |
| <table id="diffTable"></table> | |
| </gr-diff-builder> | |
| </template> | |
| </test-fixture> | |
| <script> | |
| suite('gr-diff-builder tests', function() { | |
| var element; | |
| var builder; | |
| setup(function() { | |
| var prefs = { | |
| line_length: 10, | |
| show_tabs: true, | |
| tab_size: 4, | |
| }; | |
| builder = new GrDiffBuilder({content: []}, {left: [], right: []}, prefs); | |
| }); | |
| test('context control buttons', function() { | |
| var section = {}; | |
| var line = {contextGroup: {lines: []}}; | |
| // Create 10 lines. | |
| for (var i = 0; i < 10; i++) { | |
| line.contextGroup.lines.push('lorem upsum'); | |
| } | |
| // Does not include +10 buttons when there are fewer than 11 lines. | |
| var td = builder._createContextControl(section, line); | |
| var buttons = td.querySelectorAll('gr-button.showContext'); | |
| assert.equal(buttons.length, 1); | |
| assert.equal(buttons[0].textContent, 'Show 10 common lines'); | |
| // Add another line. | |
| line.contextGroup.lines.push('lorem upsum'); | |
| // Includes +10 buttons when there are at least 11 lines. | |
| td = builder._createContextControl(section, line); | |
| buttons = td.querySelectorAll('gr-button.showContext'); | |
| assert.equal(buttons.length, 3); | |
| assert.equal(buttons[0].textContent, '+10↑'); | |
| assert.equal(buttons[1].textContent, 'Show 11 common lines'); | |
| assert.equal(buttons[2].textContent, '+10↓'); | |
| }); | |
| test('newlines', function() { | |
| var text = 'abcdef'; | |
| assert.equal(builder._addNewlines(text, text), text); | |
| text = 'a'.repeat(20); | |
| assert.equal(builder._addNewlines(text, text), | |
| 'a'.repeat(10) + | |
| GrDiffBuilder.LINE_FEED_HTML + | |
| 'a'.repeat(10)); | |
| text = '<span class="thumbsup">👍</span>'; | |
| var html = '<span class="thumbsup">👍</span>'; | |
| assert.equal(builder._addNewlines(text, html), | |
| '<span clas' + | |
| GrDiffBuilder.LINE_FEED_HTML + | |
| 's="thumbsu' + | |
| GrDiffBuilder.LINE_FEED_HTML + | |
| 'p">👍</spa' + | |
| GrDiffBuilder.LINE_FEED_HTML + | |
| 'n>'); | |
| text = '01234\t56789'; | |
| assert.equal(builder._addNewlines(text, text), | |
| '01234\t5' + | |
| GrDiffBuilder.LINE_FEED_HTML + | |
| '6789'); | |
| }); | |
| test('text length with tabs and unicode', function() { | |
| assert.equal(builder._textLength('12345', 4), 5); | |
| assert.equal(builder._textLength('\t\t12', 4), 10); | |
| assert.equal(builder._textLength('abc💢123', 4), 7); | |
| assert.equal(builder._textLength('abc\t', 8), 8); | |
| assert.equal(builder._textLength('abc\t\t', 10), 20); | |
| assert.equal(builder._textLength('', 10), 0); | |
| assert.equal(builder._textLength('', 10), 0); | |
| assert.equal(builder._textLength('abc\tde', 10), 12); | |
| assert.equal(builder._textLength('abc\tde\t', 10), 20); | |
| assert.equal(builder._textLength('\t\t\t\t\t', 20), 100); | |
| }); | |
| test('tab wrapper insertion', function() { | |
| var html = 'abc\tdef'; | |
| var wrapper = builder._getTabWrapper( | |
| builder._prefs.tab_size - 3, | |
| builder._prefs.show_tabs); | |
| assert.ok(wrapper); | |
| assert.isAbove(wrapper.length, 0); | |
| assert.equal(builder._addTabWrappers(html, builder._prefs.tab_size), | |
| 'abc' + wrapper + 'def'); | |
| assert.throws(builder._getTabWrapper.bind( | |
| builder, | |
| // using \x3c instead of < in string so gjslint can parse | |
| '">\x3cimg src="/" onerror="alert(1);">\x3cspan class="', | |
| true)); | |
| }); | |
| test('comments', function() { | |
| var line = new GrDiffLine(GrDiffLine.Type.BOTH); | |
| line.beforeNumber = 3; | |
| line.afterNumber = 5; | |
| var comments = {left: [], right: []}; | |
| assert.deepEqual(builder._getCommentsForLine(comments, line), []); | |
| assert.deepEqual(builder._getCommentsForLine(comments, line, | |
| GrDiffBuilder.Side.LEFT), []); | |
| assert.deepEqual(builder._getCommentsForLine(comments, line, | |
| GrDiffBuilder.Side.RIGHT), []); | |
| comments = { | |
| left: [ | |
| {id: 'l3', line: 3}, | |
| {id: 'l5', line: 5}, | |
| ], | |
| right: [ | |
| {id: 'r3', line: 3}, | |
| {id: 'r5', line: 5}, | |
| ], | |
| }; | |
| assert.deepEqual(builder._getCommentsForLine(comments, line), | |
| [{id: 'l3', line: 3}, {id: 'r5', line: 5}]); | |
| assert.deepEqual(builder._getCommentsForLine(comments, line, | |
| GrDiffBuilder.Side.LEFT), [{id: 'l3', line: 3}]); | |
| assert.deepEqual(builder._getCommentsForLine(comments, line, | |
| GrDiffBuilder.Side.RIGHT), [{id: 'r5', line: 5}]); | |
| }); | |
| test('comment thread creation', function() { | |
| var l3 = {id: 'l3', line: 3, updated: '2016-08-09 00:42:32.000000000'}; | |
| var l5 = {id: 'l5', line: 5, updated: '2016-08-09 00:42:32.000000000'}; | |
| var r5 = {id: 'r5', line: 5, updated: '2016-08-09 00:42:32.000000000'}; | |
| builder._comments = { | |
| meta: { | |
| changeNum: '42', | |
| patchRange: { | |
| basePatchNum: 'PARENT', | |
| patchNum: '3', | |
| }, | |
| path: '/path/to/foo', | |
| projectConfig: {foo: 'bar'}, | |
| }, | |
| left: [l3, l5], | |
| right: [r5], | |
| }; | |
| function checkThreadProps(threadEl, patchNum, side, comments) { | |
| assert.equal(threadEl.changeNum, '42'); | |
| assert.equal(threadEl.patchNum, patchNum); | |
| assert.equal(threadEl.path, '/path/to/foo'); | |
| assert.equal(threadEl.side, side); | |
| assert.deepEqual(threadEl.projectConfig, {foo: 'bar'}); | |
| assert.deepEqual(threadEl.comments, comments); | |
| } | |
| var line = new GrDiffLine(GrDiffLine.Type.BOTH); | |
| line.beforeNumber = 5; | |
| line.afterNumber = 5; | |
| var threadEl = builder._commentThreadForLine(line); | |
| checkThreadProps(threadEl, '3', 'REVISION', [l5, r5]); | |
| threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT); | |
| checkThreadProps(threadEl, '3', 'REVISION', [r5]); | |
| threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT); | |
| checkThreadProps(threadEl, '3', 'PARENT', [l5]); | |
| builder._comments.meta.patchRange.basePatchNum = '1'; | |
| threadEl = builder._commentThreadForLine(line); | |
| checkThreadProps(threadEl, '3', 'REVISION', [l5, r5]); | |
| threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT); | |
| checkThreadProps(threadEl, '1', 'REVISION', [l5]); | |
| threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT); | |
| checkThreadProps(threadEl, '3', 'REVISION', [r5]); | |
| builder._comments.meta.patchRange.basePatchNum = 'PARENT'; | |
| line = new GrDiffLine(GrDiffLine.Type.REMOVE); | |
| line.beforeNumber = 5; | |
| line.afterNumber = 5; | |
| threadEl = builder._commentThreadForLine(line); | |
| checkThreadProps(threadEl, '3', 'PARENT', [l5, r5]); | |
| line = new GrDiffLine(GrDiffLine.Type.ADD); | |
| line.beforeNumber = 3; | |
| line.afterNumber = 5; | |
| threadEl = builder._commentThreadForLine(line); | |
| checkThreadProps(threadEl, '3', 'REVISION', [l3, r5]); | |
| }); | |
| suite('intraline differences', function() { | |
| var el; | |
| var str; | |
| var annotateElementSpy; | |
| var layer; | |
| function slice(str, start, end) { | |
| return Array.from(str).slice(start, end).join(''); | |
| } | |
| setup(function() { | |
| el = fixture('div-with-text'); | |
| str = el.textContent; | |
| annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement'); | |
| layer = document.createElement('gr-diff-builder') | |
| ._createIntralineLayer(); | |
| }); | |
| teardown(function() { | |
| annotateElementSpy.restore(); | |
| }); | |
| test('annotate no highlights', function() { | |
| var line = { | |
| text: str, | |
| highlights: [], | |
| }; | |
| layer.annotate(el, line); | |
| // The content is unchanged. | |
| assert.isFalse(annotateElementSpy.called); | |
| assert.equal(el.childNodes.length, 1); | |
| assert.instanceOf(el.childNodes[0], Text); | |
| assert.equal(str, el.childNodes[0].textContent); | |
| }); | |
| test('annotate with highlights', function() { | |
| var line = { | |
| text: str, | |
| highlights: [ | |
| {startIndex: 6, endIndex: 12}, | |
| {startIndex: 18, endIndex: 22}, | |
| ], | |
| }; | |
| var str0 = slice(str, 0, 6); | |
| var str1 = slice(str, 6, 12); | |
| var str2 = slice(str, 12, 18); | |
| var str3 = slice(str, 18, 22); | |
| var str4 = slice(str, 22); | |
| layer.annotate(el, line); | |
| assert.isTrue(annotateElementSpy.called); | |
| assert.equal(el.childNodes.length, 5); | |
| assert.instanceOf(el.childNodes[0], Text); | |
| assert.equal(el.childNodes[0].textContent, str0); | |
| assert.notInstanceOf(el.childNodes[1], Text); | |
| assert.equal(el.childNodes[1].textContent, str1); | |
| assert.instanceOf(el.childNodes[2], Text); | |
| assert.equal(el.childNodes[2].textContent, str2); | |
| assert.notInstanceOf(el.childNodes[3], Text); | |
| assert.equal(el.childNodes[3].textContent, str3); | |
| assert.instanceOf(el.childNodes[4], Text); | |
| assert.equal(el.childNodes[4].textContent, str4); | |
| }); | |
| test('annotate without endIndex', function() { | |
| var line = { | |
| text: str, | |
| highlights: [ | |
| {startIndex: 28}, | |
| ], | |
| }; | |
| var str0 = slice(str, 0, 28); | |
| var str1 = slice(str, 28); | |
| layer.annotate(el, line); | |
| assert.isTrue(annotateElementSpy.called); | |
| assert.equal(el.childNodes.length, 2); | |
| assert.instanceOf(el.childNodes[0], Text); | |
| assert.equal(el.childNodes[0].textContent, str0); | |
| assert.notInstanceOf(el.childNodes[1], Text); | |
| assert.equal(el.childNodes[1].textContent, str1); | |
| }); | |
| test('annotate ignores empty highlights', function() { | |
| var line = { | |
| text: str, | |
| highlights: [ | |
| {startIndex: 28, endIndex: 28}, | |
| ], | |
| }; | |
| layer.annotate(el, line); | |
| assert.isFalse(annotateElementSpy.called); | |
| assert.equal(el.childNodes.length, 1); | |
| }); | |
| test('annotate handles unicode', function() { | |
| // Put some unicode into the string: | |
| str = str.replace(/\s/g, '💢'); | |
| el.textContent = str; | |
| var line = { | |
| text: str, | |
| highlights: [ | |
| {startIndex: 6, endIndex: 12}, | |
| ], | |
| }; | |
| var str0 = slice(str, 0, 6); | |
| var str1 = slice(str, 6, 12); | |
| var str2 = slice(str, 12); | |
| layer.annotate(el, line); | |
| assert.isTrue(annotateElementSpy.called); | |
| assert.equal(el.childNodes.length, 3); | |
| assert.instanceOf(el.childNodes[0], Text); | |
| assert.equal(el.childNodes[0].textContent, str0); | |
| assert.notInstanceOf(el.childNodes[1], Text); | |
| assert.equal(el.childNodes[1].textContent, str1); | |
| assert.instanceOf(el.childNodes[2], Text); | |
| assert.equal(el.childNodes[2].textContent, str2); | |
| }); | |
| test('annotate handles unicode w/o endIndex', function() { | |
| // Put some unicode into the string: | |
| str = str.replace(/\s/g, '💢'); | |
| el.textContent = str; | |
| var line = { | |
| text: str, | |
| highlights: [ | |
| {startIndex: 6}, | |
| ], | |
| }; | |
| var str0 = slice(str, 0, 6); | |
| var str1 = slice(str, 6); | |
| layer.annotate(el, line); | |
| assert.isTrue(annotateElementSpy.called); | |
| assert.equal(el.childNodes.length, 2); | |
| assert.instanceOf(el.childNodes[0], Text); | |
| assert.equal(el.childNodes[0].textContent, str0); | |
| assert.notInstanceOf(el.childNodes[1], Text); | |
| assert.equal(el.childNodes[1].textContent, str1); | |
| }); | |
| }); | |
| suite('rendering', function() { | |
| var content; | |
| var outputEl; | |
| setup(function(done) { | |
| var prefs = { | |
| line_length: 10, | |
| show_tabs: true, | |
| tab_size: 4, | |
| context: -1 | |
| }; | |
| content = [ | |
| { | |
| a: ['all work and no play make andybons a dull boy'], | |
| b: ['elgoog elgoog elgoog'] | |
| }, | |
| { | |
| ab: [ | |
| 'Non eram nescius, Brute, cum, quae summis ingeniis ', | |
| 'exquisitaque doctrina philosophi Graeco sermone tractavissent', | |
| ] | |
| }, | |
| ]; | |
| element = fixture('basic'); | |
| outputEl = element.queryEffectiveChildren('#diffTable'); | |
| element.addEventListener('render', function() { | |
| done(); | |
| }); | |
| sinon.stub(element, '_getDiffBuilder', function() { | |
| var builder = new GrDiffBuilder( | |
| {content: content}, {left: [], right: []}, prefs, outputEl); | |
| builder.buildSectionElement = function(group) { | |
| var section = document.createElement('stub'); | |
| section.textContent = group.lines.reduce(function(acc, line) { | |
| return acc + line.text; | |
| }, ''); | |
| return section; | |
| }; | |
| return builder; | |
| }); | |
| element.diff = {content: content}; | |
| element.render({left: [], right: []}, prefs); | |
| }); | |
| test('renderSection', function() { | |
| var section = outputEl.querySelector('stub:nth-of-type(2)'); | |
| var prevInnerHTML = section.innerHTML; | |
| section.innerHTML = 'wiped'; | |
| element._builder.renderSection(section); | |
| section = outputEl.querySelector('stub:nth-of-type(2)'); | |
| assert.equal(section.innerHTML, prevInnerHTML); | |
| }); | |
| test('getSectionsByLineRange one line', function() { | |
| var section = outputEl.querySelector('stub:nth-of-type(2)'); | |
| var sections = element._builder.getSectionsByLineRange(1, 1, 'left'); | |
| assert.equal(sections.length, 1); | |
| assert.strictEqual(sections[0], section); | |
| }); | |
| test('getSectionsByLineRange over diff', function() { | |
| var section = [ | |
| outputEl.querySelector('stub:nth-of-type(2)'), | |
| outputEl.querySelector('stub:nth-of-type(3)'), | |
| ]; | |
| var sections = element._builder.getSectionsByLineRange(1, 2, 'left'); | |
| assert.equal(sections.length, 2); | |
| assert.strictEqual(sections[0], section[0]); | |
| assert.strictEqual(sections[1], section[1]); | |
| }); | |
| }); | |
| suite('mock-diff', function() { | |
| var element; | |
| var builder; | |
| var diff; | |
| var prefs; | |
| setup(function(done) { | |
| element = fixture('mock-diff'); | |
| diff = document.createElement('mock-diff-response').diffResponse; | |
| element.diff = diff; | |
| prefs = { | |
| line_length: 80, | |
| show_tabs: true, | |
| tab_size: 4, | |
| }; | |
| element.render({left: [], right: []}, prefs).then(function() { | |
| builder = element._builder; | |
| done(); | |
| }); | |
| }); | |
| test('getContentByLine', function() { | |
| var actual; | |
| actual = builder.getContentByLine(2, 'left'); | |
| assert.equal(actual.textContent, diff.content[0].ab[1]); | |
| actual = builder.getContentByLine(2, 'right'); | |
| assert.equal(actual.textContent, diff.content[0].ab[1]); | |
| actual = builder.getContentByLine(5, 'left'); | |
| assert.equal(actual.textContent, diff.content[2].ab[0]); | |
| actual = builder.getContentByLine(5, 'right'); | |
| assert.equal(actual.textContent, diff.content[1].b[0]); | |
| }); | |
| test('findLinesByRange', function() { | |
| var lines = []; | |
| var elems = []; | |
| var start = 6; | |
| var end = 10; | |
| var count = end - start + 1; | |
| builder.findLinesByRange(start, end, 'right', lines, elems); | |
| assert.equal(lines.length, count); | |
| assert.equal(elems.length, count); | |
| for (var i = 0; i < 5; i++) { | |
| assert.instanceOf(lines[i], GrDiffLine); | |
| assert.equal(lines[i].afterNumber, start + i); | |
| assert.instanceOf(elems[i], HTMLElement); | |
| assert.equal(lines[i].text, elems[i].textContent); | |
| } | |
| }); | |
| test('_renderContentByRange', function() { | |
| var spy = sinon.spy(builder, '_createTextEl'); | |
| var start = 9; | |
| var end = 14; | |
| var count = end - start + 1; | |
| builder._renderContentByRange(start, end, 'left'); | |
| assert.equal(spy.callCount, count); | |
| spy.getCalls().forEach(function(call, i) { | |
| assert.equal(call.args[0].beforeNumber, start + i); | |
| }); | |
| spy.restore(); | |
| }); | |
| test('_getNextContentOnSide side-by-side left', function() { | |
| var startElem = builder.getContentByLine(5, 'left', | |
| element.$.diffTable); | |
| var expectedStartString = diff.content[2].ab[0]; | |
| var expectedNextString = diff.content[2].ab[1]; | |
| assert.equal(startElem.textContent, expectedStartString); | |
| var nextElem = builder._getNextContentOnSide(startElem, | |
| 'left'); | |
| assert.equal(nextElem.textContent, expectedNextString); | |
| }); | |
| test('_getNextContentOnSide side-by-side right', function() { | |
| var startElem = builder.getContentByLine(5, 'right', | |
| element.$.diffTable); | |
| var expectedStartString = diff.content[1].b[0]; | |
| var expectedNextString = diff.content[1].b[1]; | |
| assert.equal(startElem.textContent, expectedStartString); | |
| var nextElem = builder._getNextContentOnSide(startElem, | |
| 'right'); | |
| assert.equal(nextElem.textContent, expectedNextString); | |
| }); | |
| test('_getNextContentOnSide unified left', function(done) { | |
| // Re-render as unified: | |
| element.viewMode = 'UNIFIED_DIFF'; | |
| element.render({left: [], right: []}, prefs).then(function() { | |
| builder = element._builder; | |
| var startElem = builder.getContentByLine(5, 'left', | |
| element.$.diffTable); | |
| var expectedStartString = diff.content[2].ab[0]; | |
| var expectedNextString = diff.content[2].ab[1]; | |
| assert.equal(startElem.textContent, expectedStartString); | |
| var nextElem = builder._getNextContentOnSide(startElem, | |
| 'left'); | |
| assert.equal(nextElem.textContent, expectedNextString); | |
| done(); | |
| }); | |
| }); | |
| test('_getNextContentOnSide unified right', function(done) { | |
| // Re-render as unified: | |
| element.viewMode = 'UNIFIED_DIFF'; | |
| element.render({left: [], right: []}, prefs).then(function() { | |
| builder = element._builder; | |
| var startElem = builder.getContentByLine(5, 'right', | |
| element.$.diffTable); | |
| var expectedStartString = diff.content[1].b[0]; | |
| var expectedNextString = diff.content[1].b[1]; | |
| assert.equal(startElem.textContent, expectedStartString); | |
| var nextElem = builder._getNextContentOnSide(startElem, | |
| 'right'); | |
| assert.equal(nextElem.textContent, expectedNextString); | |
| done(); | |
| }); | |
| }); | |
| }); | |
| }); | |
| </script> |