2015-11-07 03:29:12 +03:00
/** @babel */
2016-08-12 18:46:11 +03:00
import { it , fit , ffit , fffit , beforeEach , afterEach , conditionPromise } from './async-spec-helpers'
2015-11-07 03:29:12 +03:00
2017-02-24 01:25:02 +03:00
const TextEditorComponent = require ( '../src/text-editor-component' )
const TextEditor = require ( '../src/text-editor' )
2017-02-24 03:30:18 +03:00
const TextBuffer = require ( 'text-buffer' )
2017-02-24 01:25:02 +03:00
const fs = require ( 'fs' )
const path = require ( 'path' )
const SAMPLE _TEXT = fs . readFileSync ( path . join ( _ _dirname , 'fixtures' , 'sample.js' ) , 'utf8' )
const NBSP _CHARACTER = '\u00a0'
2017-03-01 02:47:15 +03:00
document . registerElement ( 'text-editor-component-test-element' , {
prototype : Object . create ( HTMLElement . prototype , {
attachedCallback : {
value : function ( ) {
this . didAttach ( )
}
}
} )
} )
2017-02-24 01:25:02 +03:00
describe ( 'TextEditorComponent' , ( ) => {
beforeEach ( ( ) => {
jasmine . useRealClock ( )
} )
2017-02-24 01:44:19 +03:00
function buildComponent ( params = { } ) {
2017-02-24 03:30:18 +03:00
const buffer = new TextBuffer ( { text : SAMPLE _TEXT } )
const editor = new TextEditor ( { buffer } )
2017-02-24 07:30:35 +03:00
const component = new TextEditorComponent ( {
model : editor ,
rowsPerTile : params . rowsPerTile ,
updatedSynchronously : false
} )
2017-02-24 01:25:02 +03:00
const { element } = component
2017-02-24 01:44:19 +03:00
element . style . width = params . width ? params . width + 'px' : '800px'
element . style . height = params . height ? params . height + 'px' : '600px'
2017-03-01 02:47:15 +03:00
if ( params . attach !== false ) jasmine . attachToDOM ( element )
2017-02-24 01:44:19 +03:00
return { component , element , editor }
}
it ( 'renders lines and line numbers for the visible region' , async ( ) => {
const { component , element , editor } = buildComponent ( { rowsPerTile : 3 } )
2017-02-24 01:25:02 +03:00
expect ( element . querySelectorAll ( '.line-number' ) . length ) . toBe ( 13 )
expect ( element . querySelectorAll ( '.line' ) . length ) . toBe ( 13 )
element . style . height = 4 * component . measurements . lineHeight + 'px'
await component . getNextUpdatePromise ( )
expect ( element . querySelectorAll ( '.line-number' ) . length ) . toBe ( 9 )
expect ( element . querySelectorAll ( '.line' ) . length ) . toBe ( 9 )
component . refs . scroller . scrollTop = 5 * component . measurements . lineHeight
await component . getNextUpdatePromise ( )
// After scrolling down beyond > 3 rows, the order of line numbers and lines
// in the DOM is a bit weird because the first tile is recycled to the bottom
// when it is scrolled out of view
expect ( Array . from ( element . querySelectorAll ( '.line-number' ) ) . map ( element => element . textContent . trim ( ) ) ) . toEqual ( [
'10' , '11' , '12' , '4' , '5' , '6' , '7' , '8' , '9'
] )
expect ( Array . from ( element . querySelectorAll ( '.line' ) ) . map ( element => element . textContent ) ) . toEqual ( [
editor . lineTextForScreenRow ( 9 ) ,
' ' , // this line is blank in the model, but we render a space to prevent the line from collapsing vertically
editor . lineTextForScreenRow ( 11 ) ,
editor . lineTextForScreenRow ( 3 ) ,
editor . lineTextForScreenRow ( 4 ) ,
editor . lineTextForScreenRow ( 5 ) ,
editor . lineTextForScreenRow ( 6 ) ,
editor . lineTextForScreenRow ( 7 ) ,
editor . lineTextForScreenRow ( 8 )
] )
component . refs . scroller . scrollTop = 2.5 * component . measurements . lineHeight
await component . getNextUpdatePromise ( )
expect ( Array . from ( element . querySelectorAll ( '.line-number' ) ) . map ( element => element . textContent . trim ( ) ) ) . toEqual ( [
'1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9'
] )
expect ( Array . from ( element . querySelectorAll ( '.line' ) ) . map ( element => element . textContent ) ) . toEqual ( [
editor . lineTextForScreenRow ( 0 ) ,
editor . lineTextForScreenRow ( 1 ) ,
editor . lineTextForScreenRow ( 2 ) ,
editor . lineTextForScreenRow ( 3 ) ,
editor . lineTextForScreenRow ( 4 ) ,
editor . lineTextForScreenRow ( 5 ) ,
editor . lineTextForScreenRow ( 6 ) ,
editor . lineTextForScreenRow ( 7 ) ,
editor . lineTextForScreenRow ( 8 )
] )
2015-11-07 03:29:12 +03:00
} )
2017-02-24 01:44:19 +03:00
2017-02-24 03:30:18 +03:00
it ( 'bases the width of the lines div on the width of the longest initially-visible screen line' , ( ) => {
const { component , element , editor } = buildComponent ( { rowsPerTile : 2 , height : 20 } )
expect ( editor . getApproximateLongestScreenRow ( ) ) . toBe ( 3 )
const expectedWidth = element . querySelectorAll ( '.line' ) [ 3 ] . offsetWidth
expect ( element . querySelector ( '.lines' ) . style . width ) . toBe ( expectedWidth + 'px' )
// TODO: Confirm that we'll update this value as indexing proceeds
} )
2017-02-24 01:44:19 +03:00
it ( 'gives the line number gutter an explicit width and height so its layout can be strictly contained' , ( ) => {
const { component , element , editor } = buildComponent ( { rowsPerTile : 3 } )
const gutterElement = element . querySelector ( '.gutter.line-numbers' )
expect ( gutterElement . style . width ) . toBe ( element . querySelector ( '.line-number' ) . offsetWidth + 'px' )
expect ( gutterElement . style . height ) . toBe ( editor . getScreenLineCount ( ) * component . measurements . lineHeight + 'px' )
expect ( gutterElement . style . contain ) . toBe ( 'strict' )
// Tile nodes also have explicit width and height assignment
expect ( gutterElement . firstChild . style . width ) . toBe ( element . querySelector ( '.line-number' ) . offsetWidth + 'px' )
expect ( gutterElement . firstChild . style . height ) . toBe ( 3 * component . measurements . lineHeight + 'px' )
expect ( gutterElement . firstChild . style . contain ) . toBe ( 'strict' )
} )
it ( 'translates the gutter so it is always visible when scrolling to the right' , async ( ) => {
const { component , element , editor } = buildComponent ( { width : 100 } )
expect ( component . refs . gutterContainer . style . transform ) . toBe ( 'translateX(0px)' )
component . refs . scroller . scrollLeft = 100
await component . getNextUpdatePromise ( )
expect ( component . refs . gutterContainer . style . transform ) . toBe ( 'translateX(100px)' )
} )
2017-02-26 02:53:21 +03:00
it ( 'renders cursors within the visible row range' , async ( ) => {
const { component , element , editor } = buildComponent ( { height : 40 , rowsPerTile : 2 } )
component . refs . scroller . scrollTop = 100
await component . getNextUpdatePromise ( )
expect ( component . getRenderedStartRow ( ) ) . toBe ( 4 )
expect ( component . getRenderedEndRow ( ) ) . toBe ( 10 )
2017-02-28 22:21:29 +03:00
editor . setCursorScreenPosition ( [ 0 , 0 ] , { autoscroll : false } ) // out of view
editor . addCursorAtScreenPosition ( [ 2 , 2 ] , { autoscroll : false } ) // out of view
editor . addCursorAtScreenPosition ( [ 4 , 0 ] , { autoscroll : false } ) // line start
editor . addCursorAtScreenPosition ( [ 4 , 4 ] , { autoscroll : false } ) // at token boundary
editor . addCursorAtScreenPosition ( [ 4 , 6 ] , { autoscroll : false } ) // within token
editor . addCursorAtScreenPosition ( [ 5 , Infinity ] , { autoscroll : false } ) // line end
editor . addCursorAtScreenPosition ( [ 10 , 2 ] , { autoscroll : false } ) // out of view
2017-02-26 02:53:21 +03:00
await component . getNextUpdatePromise ( )
let cursorNodes = Array . from ( element . querySelectorAll ( '.cursor' ) )
expect ( cursorNodes . length ) . toBe ( 4 )
verifyCursorPosition ( component , cursorNodes [ 0 ] , 4 , 0 )
verifyCursorPosition ( component , cursorNodes [ 1 ] , 4 , 4 )
verifyCursorPosition ( component , cursorNodes [ 2 ] , 4 , 6 )
verifyCursorPosition ( component , cursorNodes [ 3 ] , 5 , 30 )
2017-02-28 22:21:29 +03:00
editor . setCursorScreenPosition ( [ 8 , 11 ] , { autoscroll : false } )
2017-02-26 02:53:21 +03:00
await component . getNextUpdatePromise ( )
cursorNodes = Array . from ( element . querySelectorAll ( '.cursor' ) )
expect ( cursorNodes . length ) . toBe ( 1 )
verifyCursorPosition ( component , cursorNodes [ 0 ] , 8 , 11 )
2017-02-28 22:21:29 +03:00
editor . setCursorScreenPosition ( [ 0 , 0 ] , { autoscroll : false } )
2017-02-26 02:53:21 +03:00
await component . getNextUpdatePromise ( )
cursorNodes = Array . from ( element . querySelectorAll ( '.cursor' ) )
expect ( cursorNodes . length ) . toBe ( 0 )
} )
2017-02-28 02:34:41 +03:00
it ( 'places the hidden input element at the location of the last cursor if it is visible' , async ( ) => {
const { component , element , editor } = buildComponent ( { height : 60 , width : 120 , rowsPerTile : 2 } )
const { hiddenInput } = component . refs
component . refs . scroller . scrollTop = 100
component . refs . scroller . scrollLeft = 40
await component . getNextUpdatePromise ( )
expect ( component . getRenderedStartRow ( ) ) . toBe ( 4 )
expect ( component . getRenderedEndRow ( ) ) . toBe ( 12 )
// When out of view, the hidden input is positioned at 0, 0
expect ( editor . getCursorScreenPosition ( ) ) . toEqual ( [ 0 , 0 ] )
expect ( hiddenInput . offsetTop ) . toBe ( 0 )
expect ( hiddenInput . offsetLeft ) . toBe ( 0 )
// Otherwise it is positioned at the last cursor position
editor . addCursorAtScreenPosition ( [ 7 , 4 ] )
await component . getNextUpdatePromise ( )
expect ( hiddenInput . getBoundingClientRect ( ) . top ) . toBe ( clientTopForLine ( component , 7 ) )
expect ( Math . round ( hiddenInput . getBoundingClientRect ( ) . left ) ) . toBe ( clientLeftForCharacter ( component , 7 , 4 ) )
} )
2017-03-01 02:47:15 +03:00
describe ( 'focus' , ( ) => {
it ( 'focuses the hidden input element and adds the is-focused class when focused' , async ( ) => {
2017-03-03 02:15:43 +03:00
assertDocumentFocused ( )
2017-03-01 02:47:15 +03:00
const { component , element , editor } = buildComponent ( )
const { hiddenInput } = component . refs
2017-02-28 02:34:41 +03:00
2017-03-01 02:47:15 +03:00
expect ( document . activeElement ) . not . toBe ( hiddenInput )
element . focus ( )
expect ( document . activeElement ) . toBe ( hiddenInput )
await component . getNextUpdatePromise ( )
expect ( element . classList . contains ( 'is-focused' ) ) . toBe ( true )
2017-02-28 02:34:41 +03:00
2017-03-01 02:47:15 +03:00
element . focus ( ) // focusing back to the element does not blur
expect ( document . activeElement ) . toBe ( hiddenInput )
expect ( element . classList . contains ( 'is-focused' ) ) . toBe ( true )
2017-02-28 02:34:41 +03:00
2017-03-01 02:47:15 +03:00
document . body . focus ( )
expect ( document . activeElement ) . not . toBe ( hiddenInput )
await component . getNextUpdatePromise ( )
expect ( element . classList . contains ( 'is-focused' ) ) . toBe ( false )
} )
2017-03-01 06:17:53 +03:00
it ( 'updates the component when the hidden input is focused directly' , async ( ) => {
2017-03-03 02:15:43 +03:00
assertDocumentFocused ( )
2017-03-01 06:17:53 +03:00
const { component , element , editor } = buildComponent ( )
const { hiddenInput } = component . refs
expect ( element . classList . contains ( 'is-focused' ) ) . toBe ( false )
expect ( document . activeElement ) . not . toBe ( hiddenInput )
hiddenInput . focus ( )
await component . getNextUpdatePromise ( )
expect ( element . classList . contains ( 'is-focused' ) ) . toBe ( true )
} )
2017-03-01 02:47:15 +03:00
it ( 'gracefully handles a focus event that occurs prior to the attachedCallback of the element' , ( ) => {
2017-03-03 02:15:43 +03:00
assertDocumentFocused ( )
2017-03-01 02:47:15 +03:00
const { component , element , editor } = buildComponent ( { attach : false } )
const parent = document . createElement ( 'text-editor-component-test-element' )
parent . appendChild ( element )
parent . didAttach = ( ) => element . focus ( )
jasmine . attachToDOM ( parent )
expect ( document . activeElement ) . toBe ( component . refs . hiddenInput )
} )
2017-02-28 02:34:41 +03:00
} )
2017-02-28 22:21:29 +03:00
describe ( 'autoscroll' , ( ) => {
it ( 'automatically scrolls vertically when the cursor is within vertical scroll margin of the top or bottom' , async ( ) => {
const { component , element , editor } = buildComponent ( { height : 120 } )
const { scroller } = component . refs
expect ( component . getLastVisibleRow ( ) ) . toBe ( 8 )
editor . setCursorScreenPosition ( [ 6 , 0 ] )
await component . getNextUpdatePromise ( )
let scrollBottom = scroller . scrollTop + scroller . clientHeight
expect ( scrollBottom ) . toBe ( ( 6 + 1 + editor . verticalScrollMargin ) * component . measurements . lineHeight )
editor . setCursorScreenPosition ( [ 8 , 0 ] )
await component . getNextUpdatePromise ( )
scrollBottom = scroller . scrollTop + scroller . clientHeight
expect ( scrollBottom ) . toBe ( ( 8 + 1 + editor . verticalScrollMargin ) * component . measurements . lineHeight )
editor . setCursorScreenPosition ( [ 3 , 0 ] )
await component . getNextUpdatePromise ( )
expect ( scroller . scrollTop ) . toBe ( ( 3 - editor . verticalScrollMargin ) * component . measurements . lineHeight )
editor . setCursorScreenPosition ( [ 2 , 0 ] )
await component . getNextUpdatePromise ( )
expect ( scroller . scrollTop ) . toBe ( 0 )
} )
it ( 'does not vertically autoscroll by more than half of the visible lines if the editor is shorter than twice the scroll margin' , async ( ) => {
const { component , element , editor } = buildComponent ( )
const { scroller } = component . refs
element . style . height = 5.5 * component . measurements . lineHeight + 'px'
await component . getNextUpdatePromise ( )
expect ( component . getLastVisibleRow ( ) ) . toBe ( 6 )
const scrollMarginInLines = 2
editor . setCursorScreenPosition ( [ 6 , 0 ] )
await component . getNextUpdatePromise ( )
let scrollBottom = scroller . scrollTop + scroller . clientHeight
expect ( scrollBottom ) . toBe ( ( 6 + 1 + scrollMarginInLines ) * component . measurements . lineHeight )
editor . setCursorScreenPosition ( [ 6 , 4 ] )
await component . getNextUpdatePromise ( )
scrollBottom = scroller . scrollTop + scroller . clientHeight
expect ( scrollBottom ) . toBe ( ( 6 + 1 + scrollMarginInLines ) * component . measurements . lineHeight )
editor . setCursorScreenPosition ( [ 4 , 4 ] )
await component . getNextUpdatePromise ( )
expect ( scroller . scrollTop ) . toBe ( ( 4 - scrollMarginInLines ) * component . measurements . lineHeight )
} )
2017-03-03 00:59:41 +03:00
it ( 'automatically scrolls horizontally when the cursor is within horizontal scroll margin of the right edge of the gutter or right edge of the screen' , async ( ) => {
const { component , element , editor } = buildComponent ( )
const { scroller } = component . refs
element . style . width =
component . getGutterContainerWidth ( ) +
3 * editor . horizontalScrollMargin * component . measurements . baseCharacterWidth + 'px'
await component . getNextUpdatePromise ( )
editor . scrollToScreenRange ( [ [ 1 , 12 ] , [ 2 , 28 ] ] )
await component . getNextUpdatePromise ( )
let expectedScrollLeft = Math . floor (
clientLeftForCharacter ( component , 1 , 12 ) -
lineNodeForScreenRow ( component , 1 ) . getBoundingClientRect ( ) . left -
( editor . horizontalScrollMargin * component . measurements . baseCharacterWidth )
)
expect ( scroller . scrollLeft ) . toBe ( expectedScrollLeft )
editor . scrollToScreenRange ( [ [ 1 , 12 ] , [ 2 , 28 ] ] , { reversed : false } )
await component . getNextUpdatePromise ( )
expectedScrollLeft = Math . floor (
component . getGutterContainerWidth ( ) +
clientLeftForCharacter ( component , 2 , 28 ) -
lineNodeForScreenRow ( component , 2 ) . getBoundingClientRect ( ) . left +
( editor . horizontalScrollMargin * component . measurements . baseCharacterWidth ) -
scroller . clientWidth
)
expect ( scroller . scrollLeft ) . toBe ( expectedScrollLeft )
} )
it ( 'does not horizontally autoscroll by more than half of the visible "base-width" characters if the editor is narrower than twice the scroll margin' , async ( ) => {
const { component , element , editor } = buildComponent ( )
const { scroller , gutterContainer } = component . refs
element . style . width =
component . getGutterContainerWidth ( ) +
1.5 * editor . horizontalScrollMargin * component . measurements . baseCharacterWidth + 'px'
await component . getNextUpdatePromise ( )
const contentWidth = scroller . clientWidth - gutterContainer . offsetWidth
const contentWidthInCharacters = Math . floor ( contentWidth / component . measurements . baseCharacterWidth )
expect ( contentWidthInCharacters ) . toBe ( 9 )
editor . scrollToScreenRange ( [ [ 6 , 10 ] , [ 6 , 15 ] ] )
await component . getNextUpdatePromise ( )
let expectedScrollLeft = Math . floor (
clientLeftForCharacter ( component , 6 , 10 ) -
lineNodeForScreenRow ( component , 1 ) . getBoundingClientRect ( ) . left -
( 4 * component . measurements . baseCharacterWidth )
)
expect ( scroller . scrollLeft ) . toBe ( expectedScrollLeft )
} )
2017-02-28 22:21:29 +03:00
} )
2015-11-07 03:29:12 +03:00
} )
2017-02-26 02:53:21 +03:00
function verifyCursorPosition ( component , cursorNode , row , column ) {
const rect = cursorNode . getBoundingClientRect ( )
expect ( Math . round ( rect . top ) ) . toBe ( clientTopForLine ( component , row ) )
expect ( Math . round ( rect . left ) ) . toBe ( clientLeftForCharacter ( component , row , column ) )
}
function clientTopForLine ( component , row ) {
return lineNodeForScreenRow ( component , row ) . getBoundingClientRect ( ) . top
}
function clientLeftForCharacter ( component , row , column ) {
const textNodes = textNodesForScreenRow ( component , row )
let textNodeStartColumn = 0
for ( const textNode of textNodes ) {
const textNodeEndColumn = textNodeStartColumn + textNode . textContent . length
if ( column <= textNodeEndColumn ) {
const range = document . createRange ( )
range . setStart ( textNode , column - textNodeStartColumn )
range . setEnd ( textNode , column - textNodeStartColumn )
return range . getBoundingClientRect ( ) . left
}
textNodeStartColumn = textNodeEndColumn
}
}
function lineNodeForScreenRow ( component , row ) {
const screenLine = component . getModel ( ) . screenLineForScreenRow ( row )
2017-02-28 02:34:41 +03:00
return component . lineNodesByScreenLineId . get ( screenLine . id )
2017-02-26 02:53:21 +03:00
}
function textNodesForScreenRow ( component , row ) {
const screenLine = component . getModel ( ) . screenLineForScreenRow ( row )
2017-02-28 02:34:41 +03:00
return component . textNodesByScreenLineId . get ( screenLine . id )
2017-02-26 02:53:21 +03:00
}
2017-03-03 02:15:43 +03:00
function assertDocumentFocused ( ) {
if ( ! document . hasFocus ( ) ) {
throw new Error ( 'The document needs to be focused to run this test' )
}
}