From 185d096e010f00947bbecd766bb9e985a43b7881 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 11 Jan 2019 10:30:35 -0500 Subject: [PATCH 1/4] Cover uncovered lines in Side --- lib/models/conflicts/banner.js | 1 + lib/models/conflicts/side.js | 1 + .../single-2way-diff-empty.txt | 8 + test/models/conflicts/conflict.test.js | 369 +++++++++++++++++- 4 files changed, 370 insertions(+), 9 deletions(-) create mode 100644 test/fixtures/conflict-marker-examples/single-2way-diff-empty.txt diff --git a/lib/models/conflicts/banner.js b/lib/models/conflicts/banner.js index 47080ca2ce..25630fe329 100644 --- a/lib/models/conflicts/banner.js +++ b/lib/models/conflicts/banner.js @@ -26,6 +26,7 @@ export default class Banner { revert() { const range = this.getMarker().getBufferRange(); this.editor.setTextInBufferRange(range, this.originalText); + this.getMarker().setBufferRange(range); } delete() { diff --git a/lib/models/conflicts/side.js b/lib/models/conflicts/side.js index e99174c89a..17585511a0 100644 --- a/lib/models/conflicts/side.js +++ b/lib/models/conflicts/side.js @@ -98,6 +98,7 @@ export default class Side { revert() { const range = this.getMarker().getBufferRange(); this.editor.setTextInBufferRange(range, this.originalText); + this.getMarker().setBufferRange(range); } deleteBanner() { diff --git a/test/fixtures/conflict-marker-examples/single-2way-diff-empty.txt b/test/fixtures/conflict-marker-examples/single-2way-diff-empty.txt new file mode 100644 index 0000000000..802eb878f7 --- /dev/null +++ b/test/fixtures/conflict-marker-examples/single-2way-diff-empty.txt @@ -0,0 +1,8 @@ +Before the start + +<<<<<<< HEAD +These are my changes +======= +>>>>>>> master + +Past the end diff --git a/test/models/conflicts/conflict.test.js b/test/models/conflicts/conflict.test.js index f8ac15018d..8a1c877a8a 100644 --- a/test/models/conflicts/conflict.test.js +++ b/test/models/conflicts/conflict.test.js @@ -21,10 +21,10 @@ describe('Conflict', function() { return atomEnv.workspace.open(fullPath); }; - const assertConflictOnRows = function(conflict, description, message) { + const assertConflictOnRows = function(conflict, description) { const isRangeOnRows = function(range, startRow, endRow, rangeName) { - assert( - range.start.row === startRow && range.start.column === 0 && range.end.row === endRow && range.end.column === 0, + assert.isTrue( + range.start.row === startRow && range.end.row === endRow, `expected conflict's ${rangeName} range to cover rows ${startRow} to ${endRow}, but it was ${range}`, ); }; @@ -33,27 +33,37 @@ describe('Conflict', function() { return isRangeOnRows(range, row, row + 1, rangeName); }; - const ourBannerRange = conflict.getSide(OURS).banner.marker.getBufferRange(); + const isPointOnRow = function(range, row, rangeName) { + return isRangeOnRows(range, row, row, rangeName); + }; + + const ourBannerRange = conflict.getSide(OURS).getBannerMarker().getBufferRange(); isRangeOnRow(ourBannerRange, description.ourBannerRow, '"ours" banner'); - const ourSideRange = conflict.getSide(OURS).marker.getBufferRange(); + const ourSideRange = conflict.getSide(OURS).getMarker().getBufferRange(); isRangeOnRows(ourSideRange, description.ourSideRows[0], description.ourSideRows[1], '"ours"'); assert.strictEqual(conflict.getSide(OURS).position, description.ourPosition || TOP, '"ours" in expected position'); - const theirBannerRange = conflict.getSide(THEIRS).banner.marker.getBufferRange(); + const ourBlockRange = conflict.getSide(OURS).getBlockMarker().getBufferRange(); + isPointOnRow(ourBlockRange, description.ourBannerRow, '"ours" block range'); + + const theirBannerRange = conflict.getSide(THEIRS).getBannerMarker().getBufferRange(); isRangeOnRow(theirBannerRange, description.theirBannerRow, '"theirs" banner'); - const theirSideRange = conflict.getSide(THEIRS).marker.getBufferRange(); + const theirSideRange = conflict.getSide(THEIRS).getMarker().getBufferRange(); isRangeOnRows(theirSideRange, description.theirSideRows[0], description.theirSideRows[1], '"theirs"'); assert.strictEqual(conflict.getSide(THEIRS).position, description.theirPosition || BOTTOM, '"theirs" in expected position'); + const theirBlockRange = conflict.getSide(THEIRS).getBlockMarker().getBufferRange(); + isPointOnRow(theirBlockRange, description.theirBannerRow, '"theirs" block range'); + if (description.baseBannerRow || description.baseSideRows) { assert.isNotNull(conflict.getSide(BASE), "expected conflict's base side to be non-null"); - const baseBannerRange = conflict.getSide(BASE).banner.marker.getBufferRange(); + const baseBannerRange = conflict.getSide(BASE).getBannerMarker().getBufferRange(); isRangeOnRow(baseBannerRange, description.baseBannerRow, '"base" banner'); - const baseSideRange = conflict.getSide(BASE).marker.getBufferRange(); + const baseSideRange = conflict.getSide(BASE).getMarker().getBufferRange(); isRangeOnRows(baseSideRange, description.baseSideRows[0], description.baseSideRows[1], '"base"'); assert.strictEqual(conflict.getSide(BASE).position, MIDDLE, '"base" in MIDDLE position'); } else { @@ -277,4 +287,345 @@ end assert.isTrue(conflict.getSeparator().isModified()); }); }); + + describe('contextual block position and CSS class generation', function() { + let editor, conflict; + + describe('from a merge', function() { + beforeEach(async function() { + editor = await editorOnFixture('single-3way-diff.txt'); + conflict = Conflict.allFromEditor(editor, editor.getDefaultMarkerLayer(), false)[0]; + }); + + it('accesses the block decoration position', function() { + assert.strictEqual(conflict.getSide(OURS).getBlockPosition(), 'before'); + assert.strictEqual(conflict.getSide(BASE).getBlockPosition(), 'before'); + assert.strictEqual(conflict.getSide(THEIRS).getBlockPosition(), 'after'); + }); + + it('accesses the line decoration CSS class', function() { + assert.strictEqual(conflict.getSide(OURS).getLineCSSClass(), 'github-ConflictOurs'); + assert.strictEqual(conflict.getSide(BASE).getLineCSSClass(), 'github-ConflictBase'); + assert.strictEqual(conflict.getSide(THEIRS).getLineCSSClass(), 'github-ConflictTheirs'); + }); + + it('accesses the line decoration CSS class when modified', function() { + for (const position of [[5, 1], [3, 1], [1, 1]]) { + editor.setCursorBufferPosition(position); + editor.insertText('change'); + } + + assert.strictEqual(conflict.getSide(OURS).getLineCSSClass(), 'github-ConflictModified'); + assert.strictEqual(conflict.getSide(BASE).getLineCSSClass(), 'github-ConflictModified'); + assert.strictEqual(conflict.getSide(THEIRS).getLineCSSClass(), 'github-ConflictModified'); + }); + + it('accesses the line decoration CSS class when the banner is modified', function() { + for (const position of [[6, 1], [2, 1], [0, 1]]) { + editor.setCursorBufferPosition(position); + editor.insertText('change'); + } + + assert.strictEqual(conflict.getSide(OURS).getLineCSSClass(), 'github-ConflictModified'); + assert.strictEqual(conflict.getSide(BASE).getLineCSSClass(), 'github-ConflictModified'); + assert.strictEqual(conflict.getSide(THEIRS).getLineCSSClass(), 'github-ConflictModified'); + }); + + it('accesses the banner CSS class', function() { + assert.strictEqual(conflict.getSide(OURS).getBannerCSSClass(), 'github-ConflictOursBanner'); + assert.strictEqual(conflict.getSide(BASE).getBannerCSSClass(), 'github-ConflictBaseBanner'); + assert.strictEqual(conflict.getSide(THEIRS).getBannerCSSClass(), 'github-ConflictTheirsBanner'); + }); + + it('accesses the banner CSS class when modified', function() { + for (const position of [[5, 1], [3, 1], [1, 1]]) { + editor.setCursorBufferPosition(position); + editor.insertText('change'); + } + + assert.strictEqual(conflict.getSide(OURS).getBannerCSSClass(), 'github-ConflictModifiedBanner'); + assert.strictEqual(conflict.getSide(BASE).getBannerCSSClass(), 'github-ConflictModifiedBanner'); + assert.strictEqual(conflict.getSide(THEIRS).getBannerCSSClass(), 'github-ConflictModifiedBanner'); + }); + + it('accesses the banner CSS class when the banner is modified', function() { + for (const position of [[6, 1], [2, 1], [0, 1]]) { + editor.setCursorBufferPosition(position); + editor.insertText('change'); + } + + assert.strictEqual(conflict.getSide(OURS).getBannerCSSClass(), 'github-ConflictModifiedBanner'); + assert.strictEqual(conflict.getSide(BASE).getBannerCSSClass(), 'github-ConflictModifiedBanner'); + assert.strictEqual(conflict.getSide(THEIRS).getBannerCSSClass(), 'github-ConflictModifiedBanner'); + }); + + it('accesses the block CSS classes', function() { + assert.strictEqual( + conflict.getSide(OURS).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictOursBlock github-ConflictTopBlock', + ); + assert.strictEqual( + conflict.getSide(BASE).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictBaseBlock github-ConflictMiddleBlock', + ); + assert.strictEqual( + conflict.getSide(THEIRS).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictTheirsBlock github-ConflictBottomBlock', + ); + }); + + it('accesses the block CSS classes when modified', function() { + for (const position of [[5, 1], [3, 1], [1, 1]]) { + editor.setCursorBufferPosition(position); + editor.insertText('change'); + } + + assert.strictEqual( + conflict.getSide(OURS).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictOursBlock github-ConflictTopBlock github-ConflictModifiedBlock', + ); + assert.strictEqual( + conflict.getSide(BASE).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictBaseBlock github-ConflictMiddleBlock github-ConflictModifiedBlock', + ); + assert.strictEqual( + conflict.getSide(THEIRS).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictTheirsBlock github-ConflictBottomBlock github-ConflictModifiedBlock', + ); + }); + + it('accesses the block CSS classes when the banner is modified', function() { + for (const position of [[6, 1], [2, 1], [0, 1]]) { + editor.setCursorBufferPosition(position); + editor.insertText('change'); + } + + assert.strictEqual( + conflict.getSide(OURS).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictOursBlock github-ConflictTopBlock github-ConflictModifiedBlock', + ); + assert.strictEqual( + conflict.getSide(BASE).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictBaseBlock github-ConflictMiddleBlock github-ConflictModifiedBlock', + ); + assert.strictEqual( + conflict.getSide(THEIRS).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictTheirsBlock github-ConflictBottomBlock github-ConflictModifiedBlock', + ); + }); + }); + + describe('from a rebase', function() { + beforeEach(async function() { + editor = await editorOnFixture('single-3way-diff.txt'); + conflict = Conflict.allFromEditor(editor, editor.getDefaultMarkerLayer(), true)[0]; + }); + + it('accesses the block decoration position', function() { + assert.strictEqual(conflict.getSide(THEIRS).getBlockPosition(), 'before'); + assert.strictEqual(conflict.getSide(BASE).getBlockPosition(), 'before'); + assert.strictEqual(conflict.getSide(OURS).getBlockPosition(), 'after'); + }); + + it('accesses the line decoration CSS class', function() { + assert.strictEqual(conflict.getSide(THEIRS).getLineCSSClass(), 'github-ConflictTheirs'); + assert.strictEqual(conflict.getSide(BASE).getLineCSSClass(), 'github-ConflictBase'); + assert.strictEqual(conflict.getSide(OURS).getLineCSSClass(), 'github-ConflictOurs'); + }); + + it('accesses the line decoration CSS class when modified', function() { + for (const position of [[5, 1], [3, 1], [1, 1]]) { + editor.setCursorBufferPosition(position); + editor.insertText('change'); + } + + assert.strictEqual(conflict.getSide(THEIRS).getLineCSSClass(), 'github-ConflictModified'); + assert.strictEqual(conflict.getSide(BASE).getLineCSSClass(), 'github-ConflictModified'); + assert.strictEqual(conflict.getSide(OURS).getLineCSSClass(), 'github-ConflictModified'); + }); + + it('accesses the line decoration CSS class when the banner is modified', function() { + for (const position of [[6, 1], [2, 1], [0, 1]]) { + editor.setCursorBufferPosition(position); + editor.insertText('change'); + } + + assert.strictEqual(conflict.getSide(THEIRS).getLineCSSClass(), 'github-ConflictModified'); + assert.strictEqual(conflict.getSide(BASE).getLineCSSClass(), 'github-ConflictModified'); + assert.strictEqual(conflict.getSide(OURS).getLineCSSClass(), 'github-ConflictModified'); + }); + + it('accesses the banner CSS class', function() { + assert.strictEqual(conflict.getSide(THEIRS).getBannerCSSClass(), 'github-ConflictTheirsBanner'); + assert.strictEqual(conflict.getSide(BASE).getBannerCSSClass(), 'github-ConflictBaseBanner'); + assert.strictEqual(conflict.getSide(OURS).getBannerCSSClass(), 'github-ConflictOursBanner'); + }); + + it('accesses the banner CSS class when modified', function() { + for (const position of [[5, 1], [3, 1], [1, 1]]) { + editor.setCursorBufferPosition(position); + editor.insertText('change'); + } + + assert.strictEqual(conflict.getSide(THEIRS).getBannerCSSClass(), 'github-ConflictModifiedBanner'); + assert.strictEqual(conflict.getSide(BASE).getBannerCSSClass(), 'github-ConflictModifiedBanner'); + assert.strictEqual(conflict.getSide(OURS).getBannerCSSClass(), 'github-ConflictModifiedBanner'); + }); + + it('accesses the banner CSS class when the banner is modified', function() { + for (const position of [[6, 1], [2, 1], [0, 1]]) { + editor.setCursorBufferPosition(position); + editor.insertText('change'); + } + + assert.strictEqual(conflict.getSide(THEIRS).getBannerCSSClass(), 'github-ConflictModifiedBanner'); + assert.strictEqual(conflict.getSide(BASE).getBannerCSSClass(), 'github-ConflictModifiedBanner'); + assert.strictEqual(conflict.getSide(OURS).getBannerCSSClass(), 'github-ConflictModifiedBanner'); + }); + + it('accesses the block CSS classes', function() { + assert.strictEqual( + conflict.getSide(THEIRS).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictTheirsBlock github-ConflictTopBlock', + ); + assert.strictEqual( + conflict.getSide(BASE).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictBaseBlock github-ConflictMiddleBlock', + ); + assert.strictEqual( + conflict.getSide(OURS).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictOursBlock github-ConflictBottomBlock', + ); + }); + + it('accesses the block CSS classes when modified', function() { + for (const position of [[5, 1], [3, 1], [1, 1]]) { + editor.setCursorBufferPosition(position); + editor.insertText('change'); + } + + assert.strictEqual( + conflict.getSide(THEIRS).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictTheirsBlock github-ConflictTopBlock github-ConflictModifiedBlock', + ); + assert.strictEqual( + conflict.getSide(BASE).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictBaseBlock github-ConflictMiddleBlock github-ConflictModifiedBlock', + ); + assert.strictEqual( + conflict.getSide(OURS).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictOursBlock github-ConflictBottomBlock github-ConflictModifiedBlock', + ); + }); + + it('accesses the block CSS classes when the banner is modified', function() { + for (const position of [[6, 1], [2, 1], [0, 1]]) { + editor.setCursorBufferPosition(position); + editor.insertText('change'); + } + + assert.strictEqual( + conflict.getSide(THEIRS).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictTheirsBlock github-ConflictTopBlock github-ConflictModifiedBlock', + ); + assert.strictEqual( + conflict.getSide(BASE).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictBaseBlock github-ConflictMiddleBlock github-ConflictModifiedBlock', + ); + assert.strictEqual( + conflict.getSide(OURS).getBlockCSSClasses(), + 'github-ConflictBlock github-ConflictOursBlock github-ConflictBottomBlock github-ConflictModifiedBlock', + ); + }); + }); + }); + + it('accesses a side range that encompasses the banner and content', async function() { + const editor = await editorOnFixture('single-3way-diff.txt'); + const conflict = Conflict.allFromEditor(editor, editor.getDefaultMarkerLayer(), false)[0]; + + assert.deepEqual(conflict.getSide(OURS).getRange().serialize(), [[0, 0], [2, 0]]); + assert.deepEqual(conflict.getSide(BASE).getRange().serialize(), [[2, 0], [4, 0]]); + assert.deepEqual(conflict.getSide(THEIRS).getRange().serialize(), [[5, 0], [7, 0]]); + }); + + it('determines the inclusion of points', async function() { + const editor = await editorOnFixture('single-3way-diff.txt'); + const conflict = Conflict.allFromEditor(editor, editor.getDefaultMarkerLayer(), false)[0]; + + assert.isTrue(conflict.getSide(OURS).includesPoint([0, 1])); + assert.isTrue(conflict.getSide(OURS).includesPoint([1, 3])); + assert.isFalse(conflict.getSide(OURS).includesPoint([2, 1])); + }); + + it('detects when a side is empty', async function() { + const editor = await editorOnFixture('single-2way-diff-empty.txt'); + const conflict = Conflict.allFromEditor(editor, editor.getDefaultMarkerLayer(), false)[0]; + + assert.isFalse(conflict.getSide(OURS).isEmpty()); + assert.isTrue(conflict.getSide(THEIRS).isEmpty()); + }); + + it('reverts a modified Side', async function() { + const editor = await editorOnFixture('single-3way-diff.txt'); + const conflict = Conflict.allFromEditor(editor, editor.getDefaultMarkerLayer(), false)[0]; + + editor.setCursorBufferPosition([5, 10]); + editor.insertText('MY-CHANGE'); + + assert.isTrue(conflict.getSide(THEIRS).isModified()); + assert.match(editor.getText(), /MY-CHANGE/); + + conflict.getSide(THEIRS).revert(); + + assert.isFalse(conflict.getSide(THEIRS).isModified()); + assert.notMatch(editor.getText(), /MY-CHANGE/); + }); + + it('reverts a modified Side banner', async function() { + const editor = await editorOnFixture('single-3way-diff.txt'); + const conflict = Conflict.allFromEditor(editor, editor.getDefaultMarkerLayer(), false)[0]; + + editor.setCursorBufferPosition([6, 4]); + editor.insertText('MY-CHANGE'); + + assert.isTrue(conflict.getSide(THEIRS).isBannerModified()); + assert.match(editor.getText(), /MY-CHANGE/); + + conflict.getSide(THEIRS).revertBanner(); + + assert.isFalse(conflict.getSide(THEIRS).isBannerModified()); + assert.notMatch(editor.getText(), /MY-CHANGE/); + }); + + it('deletes a banner', async function() { + const editor = await editorOnFixture('single-3way-diff.txt'); + const conflict = Conflict.allFromEditor(editor, editor.getDefaultMarkerLayer(), false)[0]; + + assert.match(editor.getText(), /<<<<<<< HEAD/); + conflict.getSide(OURS).deleteBanner(); + assert.notMatch(editor.getText(), /<<<<<<< HEAD/); + }); + + it('deletes a side', async function() { + const editor = await editorOnFixture('single-3way-diff.txt'); + const conflict = Conflict.allFromEditor(editor, editor.getDefaultMarkerLayer(), false)[0]; + + assert.match(editor.getText(), /your/); + conflict.getSide(THEIRS).delete(); + assert.notMatch(editor.getText(), /your/); + }); + + it('appends text to a side', async function() { + const editor = await editorOnFixture('single-3way-diff.txt'); + const conflict = Conflict.allFromEditor(editor, editor.getDefaultMarkerLayer(), false)[0]; + + assert.notMatch(editor.getText(), /APPENDED/); + conflict.getSide(THEIRS).appendText('APPENDED\n'); + assert.match(editor.getText(), /APPENDED/); + + assert.isTrue(conflict.getSide(THEIRS).isModified()); + assert.strictEqual(conflict.getSide(THEIRS).getText(), 'These are your changes\nAPPENDED\n'); + assert.isFalse(conflict.getSide(THEIRS).isBannerModified()); + }); }); From 94cb39ccb623bbb4d7f23f4f009f5e02c6bb8076 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 11 Jan 2019 11:32:59 -0500 Subject: [PATCH 2/4] Full coverage for Decoration --- lib/atom/decoration.js | 10 +++--- test/atom/decoration.test.js | 63 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/lib/atom/decoration.js b/lib/atom/decoration.js index cd3cbead27..9da22e42ac 100644 --- a/lib/atom/decoration.js +++ b/lib/atom/decoration.js @@ -64,12 +64,12 @@ class BareDecoration extends React.Component { componentDidUpdate(prevProps) { if (this.props.editorHolder !== prevProps.editorHolder) { this.editorSub.dispose(); - this.editorSub = this.state.editorHolder.observe(this.observeParents); + this.editorSub = this.props.editorHolder.observe(this.observeParents); } if (this.props.decorableHolder !== prevProps.decorableHolder) { this.decorableSub.dispose(); - this.decorableSub = this.state.decorableHolder.observe(this.observeParents); + this.decorableSub = this.props.decorableHolder.observe(this.observeParents); } if ( @@ -95,10 +95,10 @@ class BareDecoration extends React.Component { this.decorationHolder.map(decoration => decoration.destroy()); const editorValid = this.props.editorHolder.map(editor => !editor.isDestroyed()).getOr(false); - const markableValid = this.props.decorableHolder.map(decorable => !decorable.isDestroyed()).getOr(false); + const decorableValid = this.props.decorableHolder.map(decorable => !decorable.isDestroyed()).getOr(false); // Ensure the Marker or MarkerLayer corresponds to the context's TextEditor - const markableMatches = this.props.decorableHolder.map(decorable => this.props.editorHolder.map(editor => { + const decorableMatches = this.props.decorableHolder.map(decorable => this.props.editorHolder.map(editor => { const layer = decorable.layer || decorable; const displayLayer = editor.getMarkerLayer(layer.id); if (!displayLayer) { @@ -110,7 +110,7 @@ class BareDecoration extends React.Component { return true; }).getOr(false)).getOr(false); - if (!editorValid || !markableValid || !markableMatches) { + if (!editorValid || !decorableValid || !decorableMatches) { return; } diff --git a/test/atom/decoration.test.js b/test/atom/decoration.test.js index 2e9fd4fdcd..db187dfdcb 100644 --- a/test/atom/decoration.test.js +++ b/test/atom/decoration.test.js @@ -99,6 +99,69 @@ describe('Decoration', function() { }); }); + describe('when called with changed props', function() { + let wrapper, originalDecoration; + + beforeEach(function() { + const app = ( + + ); + wrapper = mount(app); + + originalDecoration = editor.getLineDecorations({position: 'head', class: 'something'})[0]; + }); + + it('redecorates when a new Editor and Marker are provided', async function() { + const newEditor = await workspace.open(require.resolve('../../package.json')); + const newMarker = newEditor.markBufferRange([[0, 0], [2, 0]]); + + wrapper.setProps({editor: newEditor, decorable: newMarker}); + + assert.isTrue(originalDecoration.isDestroyed()); + assert.lengthOf(editor.getLineDecorations({position: 'head', class: 'something'}), 0); + assert.lengthOf(newEditor.getLineDecorations({position: 'head', class: 'something'}), 1); + }); + + it('redecorates when a new MarkerLayer is provided', function() { + const newLayer = editor.addMarkerLayer(); + + wrapper.setProps({decorable: newLayer, decorateMethod: 'decorateMarkerLayer'}); + + assert.isTrue(originalDecoration.isDestroyed()); + assert.lengthOf(editor.getLineDecorations({position: 'head', class: 'something'}), 0); + + // Turns out Atom doesn't provide any public way to query the markers on a layer. + assert.lengthOf( + Array.from(editor.decorationManager.layerDecorationsByMarkerLayer.get(newLayer)), + 1, + ); + }); + + it('updates decoration properties', function() { + wrapper.setProps({className: 'different'}); + + assert.lengthOf(editor.getLineDecorations({position: 'head', class: 'something'}), 0); + assert.lengthOf(editor.getLineDecorations({position: 'head', class: 'different'}), 1); + assert.isFalse(originalDecoration.isDestroyed()); + assert.strictEqual(originalDecoration.getProperties().class, 'different'); + }); + + it('does not redecorate when the decorable is on the wrong TextEditor', async function() { + const newEditor = await workspace.open(require.resolve('../../package.json')); + + wrapper.setProps({editor: newEditor}); + + assert.isTrue(originalDecoration.isDestroyed()); + assert.lengthOf(editor.getLineDecorations({}), 0); + }); + }); + it('destroys its decoration on unmount', function() { const app = ( Date: Fri, 11 Jan 2019 13:59:25 -0500 Subject: [PATCH 3/4] Cover EditorConflictController --- lib/controllers/editor-conflict-controller.js | 9 +-- .../editor-conflict-controller.test.js | 71 +++++++++++++++++++ 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/lib/controllers/editor-conflict-controller.js b/lib/controllers/editor-conflict-controller.js index 728cfc5021..79f7309512 100644 --- a/lib/controllers/editor-conflict-controller.js +++ b/lib/controllers/editor-conflict-controller.js @@ -18,11 +18,7 @@ export default class EditorConflictController extends React.Component { commandRegistry: PropTypes.object.isRequired, resolutionProgress: PropTypes.object.isRequired, isRebase: PropTypes.bool.isRequired, - refreshResolutionProgress: PropTypes.func, - } - - static defaultProps = { - refreshResolutionProgress: () => {}, + refreshResolutionProgress: PropTypes.func.isRequired, } constructor(props, context) { @@ -49,7 +45,6 @@ export default class EditorConflictController extends React.Component { const buffer = this.props.editor.getBuffer(); this.subscriptions.add( - this.props.editor.onDidStopChanging(() => this.forceUpdate()), this.props.editor.onDidDestroy(() => this.props.refreshResolutionProgress(this.props.editor.getPath())), buffer.onDidReload(() => this.reparseConflicts()), ); @@ -171,7 +166,7 @@ export default class EditorConflictController extends React.Component { } dismissConflicts(conflicts) { - this.setState((prevState, props) => { + this.setState(prevState => { const {added} = compareSets(new Set(conflicts), prevState.conflicts); return {conflicts: added}; }); diff --git a/test/controllers/editor-conflict-controller.test.js b/test/controllers/editor-conflict-controller.test.js index 61ef4b376c..872acb70f2 100644 --- a/test/controllers/editor-conflict-controller.test.js +++ b/test/controllers/editor-conflict-controller.test.js @@ -166,6 +166,49 @@ describe('EditorConflictController', function() { assert.strictEqual(conflicts[2].getChosenSide(), conflicts[2].getSide(THEIRS)); }); + it('resolves multiple conflicts as "ours"', function() { + assert.isFalse(conflicts[0].isResolved()); + assert.isFalse(conflicts[1].isResolved()); + assert.isFalse(conflicts[2].isResolved()); + + editor.setCursorBufferPosition([8, 3]); // On "Your changes" + editor.addCursorAtBufferPosition([11, 2]); // On "Text in between 0 and 1." + editor.addCursorAtBufferPosition([14, 5]); // On "My middle changes" + editor.addCursorAtBufferPosition([15, 0]); // On "=======" + commandRegistry.dispatch(editorView, 'github:resolve-as-ours'); + + assert.isTrue(conflicts[0].isResolved()); + assert.strictEqual(conflicts[0].getChosenSide(), conflicts[0].getSide(OURS)); + assert.deepEqual(conflicts[0].getUnchosenSides(), [conflicts[0].getSide(THEIRS)]); + + assert.isTrue(conflicts[1].isResolved()); + assert.strictEqual(conflicts[1].getChosenSide(), conflicts[1].getSide(OURS)); + assert.deepEqual(conflicts[1].getUnchosenSides(), [conflicts[1].getSide(THEIRS)]); + + assert.isFalse(conflicts[2].isResolved()); + }); + + it('resolves multiple conflicts as "theirs"', function() { + assert.isFalse(conflicts[0].isResolved()); + assert.isFalse(conflicts[1].isResolved()); + assert.isFalse(conflicts[2].isResolved()); + + editor.setCursorBufferPosition([8, 3]); // On "Your changes" + editor.addCursorAtBufferPosition([11, 2]); // On "Text in between 0 and 1." + editor.addCursorAtBufferPosition([22, 5]); // On "More of my changes" + commandRegistry.dispatch(editorView, 'github:resolve-as-theirs'); + + assert.isTrue(conflicts[0].isResolved()); + assert.strictEqual(conflicts[0].getChosenSide(), conflicts[0].getSide(THEIRS)); + assert.deepEqual(conflicts[0].getUnchosenSides(), [conflicts[0].getSide(OURS)]); + + assert.isFalse(conflicts[1].isResolved()); + + assert.isTrue(conflicts[2].isResolved()); + assert.strictEqual(conflicts[2].getChosenSide(), conflicts[2].getSide(THEIRS)); + assert.deepEqual(conflicts[2].getUnchosenSides(), [conflicts[2].getSide(OURS)]); + }); + it('disregards conflicts with cursors on both sides', function() { editor.setCursorBufferPosition([6, 3]); // On "Multi-line even" editor.addCursorAtBufferPosition([14, 1]); // On "My middle changes" @@ -316,6 +359,26 @@ describe('EditorConflictController', function() { await assert.async.isTrue(refreshResolutionProgress.calledWith(fixtureFile)); }); + + it('performs a resolution from the context menu', function() { + const conflict = conflicts[1]; + assert.isFalse(conflict.isResolved()); + + wrapper.find('ConflictController').at(1).prop('resolveAsSequence')([OURS]); + + assert.isTrue(conflict.isResolved()); + assert.strictEqual(conflict.getChosenSide(), conflict.getSide(OURS)); + }); + + it('dismisses a conflict from the context menu', function() { + const conflict = conflicts[2]; + + wrapper.find('ConflictController').at(2).prop('dismiss')(); + wrapper.update(); + + assert.lengthOf(wrapper.find(ConflictController), 2); + assert.isFalse(wrapper.find(ConflictController).someWhere(cc => cc.prop('conflict') === conflict)); + }); }); describe('on a file with 3-way diff markers', function() { @@ -442,4 +505,12 @@ describe('EditorConflictController', function() { assert.equal(editor.getText(), 'These are my changes\nThese are your changes\n\nPast the end\n'); }); }); + + it('cleans up its subscriptions when unmounting', async function() { + await useFixture('triple-2way-diff.txt'); + wrapper.unmount(); + + editor.destroy(); + assert.isFalse(refreshResolutionProgress.called); + }); }); From a8a5d6168d03fb091f977f3eff569c889613314b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 11 Jan 2019 14:55:03 -0500 Subject: [PATCH 4/4] Coverage for the Present state --- lib/models/repository-states/present.js | 10 +-- test/models/repository.test.js | 81 +++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 830c18280e..8d93ffb7b8 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -610,9 +610,6 @@ export default class Present extends State { const bundle = await this.git().getStatusBundle(); const results = await this.formatChangedFiles(bundle); results.branch = bundle.branch; - if (!results.branch.aheadBehind) { - results.branch.aheadBehind = {ahead: null, behind: null}; - } return results; } catch (err) { if (err instanceof LargeRepoError) { @@ -855,7 +852,7 @@ export default class Present extends State { // Direct blob access getBlobContents(sha) { - return this.cache.getOrSet(Keys.blob(sha), () => { + return this.cache.getOrSet(Keys.blob.oneWith(sha), () => { return this.git().getBlobContents(sha); }); } @@ -876,6 +873,7 @@ export default class Present extends State { // Cache + /* istanbul ignore next */ getCache() { return this.cache; } @@ -974,6 +972,7 @@ class Cache { this.didUpdate(); } + /* istanbul ignore next */ [Symbol.iterator]() { return this.storage[Symbol.iterator](); } @@ -988,6 +987,7 @@ class Cache { this.emitter.emit('did-update'); } + /* istanbul ignore next */ onDidUpdate(callback) { return this.emitter.on('did-update', callback); } @@ -1025,6 +1025,7 @@ class CacheKey { } } + /* istanbul ignore next */ toString() { return `CacheKey(${this.primary})`; } @@ -1041,6 +1042,7 @@ class GroupKey { } } + /* istanbul ignore next */ toString() { return `GroupKey(${this.group})`; } diff --git a/test/models/repository.test.js b/test/models/repository.test.js index cfbf9df9b0..e802d8cdd8 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -7,6 +7,7 @@ import isEqualWith from 'lodash.isequalwith'; import Repository from '../../lib/models/repository'; import CompositeGitStrategy from '../../lib/composite-git-strategy'; +import {LargeRepoError} from '../../lib/git-shell-out-strategy'; import {nullCommit} from '../../lib/models/commit'; import {nullOperationStates} from '../../lib/models/operation-states'; import Author from '../../lib/models/author'; @@ -444,6 +445,60 @@ describe('Repository', function() { }); }); + describe('getStatusBundle', function() { + it('transitions to the TooLarge state and returns empty status when too large', async function() { + const workdir = await cloneRepository(); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + + sinon.stub(repository.git, 'getStatusBundle').rejects(new LargeRepoError()); + + const result = await repository.getStatusBundle(); + + assert.isTrue(repository.isInState('TooLarge')); + assert.deepEqual(result.branch, {}); + assert.deepEqual(result.stagedFiles, {}); + assert.deepEqual(result.unstagedFiles, {}); + assert.deepEqual(result.mergeConflictFiles, {}); + }); + + it('propagates unrecognized git errors', async function() { + const workdir = await cloneRepository(); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + + sinon.stub(repository.git, 'getStatusBundle').rejects(new Error('oh no')); + + await assert.isRejected(repository.getStatusBundle(), /oh no/); + }); + + it('post-processes renamed files to an addition and a deletion', async function() { + const workdir = await cloneRepository(); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + + sinon.stub(repository.git, 'getStatusBundle').resolves({ + changedEntries: [], + untrackedEntries: [], + renamedEntries: [ + {stagedStatus: 'R', origFilePath: 'from0.txt', filePath: 'to0.txt'}, + {unstagedStatus: 'R', origFilePath: 'from1.txt', filePath: 'to1.txt'}, + {stagedStatus: 'C', filePath: 'c2.txt'}, + {unstagedStatus: 'C', filePath: 'c3.txt'}, + ], + unmergedEntries: [], + }); + + const result = await repository.getStatusBundle(); + assert.strictEqual(result.stagedFiles['from0.txt'], 'deleted'); + assert.strictEqual(result.stagedFiles['to0.txt'], 'added'); + assert.strictEqual(result.unstagedFiles['from1.txt'], 'deleted'); + assert.strictEqual(result.unstagedFiles['to1.txt'], 'added'); + assert.strictEqual(result.stagedFiles['c2.txt'], 'added'); + assert.strictEqual(result.unstagedFiles['c3.txt'], 'added'); + }); + }); + describe('getFilePatchForPath', function() { it('returns cached MultiFilePatch objects if they exist', async function() { const workingDirPath = await cloneRepository('multiple-commits'); @@ -931,6 +986,20 @@ describe('Repository', function() { }); }); + describe('unsetConfig', function() { + it('unsets a git config option', async function() { + const workingDirPath = await cloneRepository('three-files'); + const repository = new Repository(workingDirPath); + await repository.getLoadPromise(); + + await repository.setConfig('some.key', 'value'); + assert.strictEqual(await repository.getConfig('some.key'), 'value'); + + await repository.unsetConfig('some.key'); + assert.isNull(await repository.getConfig('some.key')); + }); + }); + describe('getCommitter', function() { it('returns user name and email if they exist', async function() { const workingDirPath = await cloneRepository('three-files'); @@ -1435,6 +1504,18 @@ describe('Repository', function() { }); }); + describe('getBlobContents(sha)', function() { + it('returns blob contents for sha', async function() { + const workingDirPath = await cloneRepository('three-files'); + const repository = new Repository(workingDirPath); + await repository.getLoadPromise(); + + const sha = await repository.createBlob({stdin: 'aa\nbb\ncc\n'}); + const contents = await repository.getBlobContents(sha); + assert.strictEqual(contents, 'aa\nbb\ncc\n'); + }); + }); + describe('discardWorkDirChangesForPaths()', function() { it('can discard working directory changes in modified files', async function() { const workingDirPath = await cloneRepository('three-files');