mirror of
https://github.com/atom/atom.git
synced 2026-01-23 13:58:08 -05:00
This avoids content being shifted over due to rendering and measuring the gutter on element creation and then subsequently hiding it.
420 lines
14 KiB
JavaScript
420 lines
14 KiB
JavaScript
/* global HTMLDivElement */
|
|
|
|
const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise, timeoutPromise} = require('./async-spec-helpers')
|
|
const TextEditor = require('../src/text-editor')
|
|
const TextEditorElement = require('../src/text-editor-element')
|
|
|
|
describe('TextEditorElement', () => {
|
|
let jasmineContent
|
|
|
|
beforeEach(() => {
|
|
jasmineContent = document.body.querySelector('#jasmine-content')
|
|
})
|
|
|
|
function buildTextEditorElement (options = {}) {
|
|
const element = new TextEditorElement()
|
|
element.setUpdatedSynchronously(false)
|
|
if (options.attach !== false) jasmine.attachToDOM(element)
|
|
return element
|
|
}
|
|
|
|
it("honors the 'mini' attribute", () => {
|
|
jasmineContent.innerHTML = '<atom-text-editor mini>'
|
|
const element = jasmineContent.firstChild
|
|
expect(element.getModel().isMini()).toBe(true)
|
|
|
|
element.removeAttribute('mini')
|
|
expect(element.getModel().isMini()).toBe(false)
|
|
expect(element.getComponent().getGutterContainerWidth()).toBe(0)
|
|
|
|
element.setAttribute('mini', '')
|
|
expect(element.getModel().isMini()).toBe(true)
|
|
})
|
|
|
|
it('sets the editor to mini if the model is accessed prior to attaching the element', () => {
|
|
const parent = document.createElement('div')
|
|
parent.innerHTML = '<atom-text-editor mini>'
|
|
const element = parent.firstChild
|
|
expect(element.getModel().isMini()).toBe(true)
|
|
})
|
|
|
|
it("honors the 'placeholder-text' attribute", () => {
|
|
jasmineContent.innerHTML = "<atom-text-editor placeholder-text='testing'>"
|
|
const element = jasmineContent.firstChild
|
|
expect(element.getModel().getPlaceholderText()).toBe('testing')
|
|
|
|
element.setAttribute('placeholder-text', 'placeholder')
|
|
expect(element.getModel().getPlaceholderText()).toBe('placeholder')
|
|
|
|
element.removeAttribute('placeholder-text')
|
|
expect(element.getModel().getPlaceholderText()).toBeNull()
|
|
})
|
|
|
|
it("only assigns 'placeholder-text' on the model if the attribute is present", () => {
|
|
const editor = new TextEditor({placeholderText: 'placeholder'})
|
|
editor.getElement()
|
|
expect(editor.getPlaceholderText()).toBe('placeholder')
|
|
})
|
|
|
|
it("honors the 'gutter-hidden' attribute", () => {
|
|
jasmineContent.innerHTML = '<atom-text-editor gutter-hidden>'
|
|
const element = jasmineContent.firstChild
|
|
expect(element.getModel().isLineNumberGutterVisible()).toBe(false)
|
|
|
|
element.removeAttribute('gutter-hidden')
|
|
expect(element.getModel().isLineNumberGutterVisible()).toBe(true)
|
|
|
|
element.setAttribute('gutter-hidden', '')
|
|
expect(element.getModel().isLineNumberGutterVisible()).toBe(false)
|
|
})
|
|
|
|
it('honors the text content', () => {
|
|
jasmineContent.innerHTML = '<atom-text-editor>testing</atom-text-editor>'
|
|
const element = jasmineContent.firstChild
|
|
expect(element.getModel().getText()).toBe('testing')
|
|
})
|
|
|
|
describe('when the model is assigned', () =>
|
|
it("adds the 'mini' attribute if .isMini() returns true on the model", function (done) {
|
|
const element = buildTextEditorElement()
|
|
element.getModel().update({mini: true})
|
|
atom.views.getNextUpdatePromise().then(() => {
|
|
expect(element.hasAttribute('mini')).toBe(true)
|
|
done()
|
|
})
|
|
})
|
|
)
|
|
|
|
describe('when the editor is attached to the DOM', () =>
|
|
it('mounts the component and unmounts when removed from the dom', () => {
|
|
const element = buildTextEditorElement()
|
|
|
|
const { component } = element
|
|
expect(component.attached).toBe(true)
|
|
element.remove()
|
|
expect(component.attached).toBe(false)
|
|
|
|
jasmine.attachToDOM(element)
|
|
expect(element.component.attached).toBe(true)
|
|
})
|
|
)
|
|
|
|
describe('when the editor is detached from the DOM and then reattached', () => {
|
|
it('does not render duplicate line numbers', () => {
|
|
const editor = new TextEditor()
|
|
editor.setText('1\n2\n3')
|
|
const element = editor.getElement()
|
|
jasmine.attachToDOM(element)
|
|
|
|
const initialCount = element.querySelectorAll('.line-number').length
|
|
|
|
element.remove()
|
|
jasmine.attachToDOM(element)
|
|
expect(element.querySelectorAll('.line-number').length).toBe(initialCount)
|
|
})
|
|
|
|
it('does not render duplicate decorations in custom gutters', () => {
|
|
const editor = new TextEditor()
|
|
editor.setText('1\n2\n3')
|
|
editor.addGutter({name: 'test-gutter'})
|
|
const marker = editor.markBufferRange([[0, 0], [2, 0]])
|
|
editor.decorateMarker(marker, {type: 'gutter', gutterName: 'test-gutter'})
|
|
const element = editor.getElement()
|
|
|
|
jasmine.attachToDOM(element)
|
|
const initialDecorationCount = element.querySelectorAll('.decoration').length
|
|
|
|
element.remove()
|
|
jasmine.attachToDOM(element)
|
|
expect(element.querySelectorAll('.decoration').length).toBe(initialDecorationCount)
|
|
})
|
|
|
|
it('can be re-focused using the previous `document.activeElement`', () => {
|
|
const editorElement = buildTextEditorElement()
|
|
editorElement.focus()
|
|
|
|
const { activeElement } = document
|
|
|
|
editorElement.remove()
|
|
jasmine.attachToDOM(editorElement)
|
|
activeElement.focus()
|
|
|
|
expect(editorElement.hasFocus()).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('focus and blur handling', () => {
|
|
it('proxies focus/blur events to/from the hidden input', () => {
|
|
const element = buildTextEditorElement()
|
|
jasmineContent.appendChild(element)
|
|
|
|
let blurCalled = false
|
|
element.addEventListener('blur', () => {
|
|
blurCalled = true
|
|
})
|
|
|
|
element.focus()
|
|
expect(blurCalled).toBe(false)
|
|
expect(element.hasFocus()).toBe(true)
|
|
expect(document.activeElement).toBe(element.querySelector('input'))
|
|
|
|
document.body.focus()
|
|
expect(blurCalled).toBe(true)
|
|
})
|
|
|
|
it("doesn't trigger a blur event on the editor element when focusing an already focused editor element", () => {
|
|
let blurCalled = false
|
|
const element = buildTextEditorElement()
|
|
element.addEventListener('blur', () => { blurCalled = true })
|
|
|
|
jasmineContent.appendChild(element)
|
|
expect(document.activeElement).toBe(document.body)
|
|
expect(blurCalled).toBe(false)
|
|
|
|
element.focus()
|
|
expect(document.activeElement).toBe(element.querySelector('input'))
|
|
expect(blurCalled).toBe(false)
|
|
|
|
element.focus()
|
|
expect(document.activeElement).toBe(element.querySelector('input'))
|
|
expect(blurCalled).toBe(false)
|
|
})
|
|
|
|
describe('when focused while a parent node is being attached to the DOM', () => {
|
|
class ElementThatFocusesChild extends HTMLDivElement {
|
|
attachedCallback () {
|
|
this.firstChild.focus()
|
|
}
|
|
}
|
|
|
|
document.registerElement('element-that-focuses-child',
|
|
{prototype: ElementThatFocusesChild.prototype}
|
|
)
|
|
|
|
it('proxies the focus event to the hidden input', () => {
|
|
const element = buildTextEditorElement()
|
|
const parentElement = document.createElement('element-that-focuses-child')
|
|
parentElement.appendChild(element)
|
|
jasmineContent.appendChild(parentElement)
|
|
expect(document.activeElement).toBe(element.querySelector('input'))
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('::onDidAttach and ::onDidDetach', () =>
|
|
it('invokes callbacks when the element is attached and detached', () => {
|
|
const element = buildTextEditorElement({attach: false})
|
|
|
|
const attachedCallback = jasmine.createSpy('attachedCallback')
|
|
const detachedCallback = jasmine.createSpy('detachedCallback')
|
|
|
|
element.onDidAttach(attachedCallback)
|
|
element.onDidDetach(detachedCallback)
|
|
|
|
jasmine.attachToDOM(element)
|
|
expect(attachedCallback).toHaveBeenCalled()
|
|
expect(detachedCallback).not.toHaveBeenCalled()
|
|
|
|
attachedCallback.reset()
|
|
element.remove()
|
|
|
|
expect(attachedCallback).not.toHaveBeenCalled()
|
|
expect(detachedCallback).toHaveBeenCalled()
|
|
})
|
|
)
|
|
|
|
describe('::setUpdatedSynchronously', () =>
|
|
it('controls whether the text editor is updated synchronously', () => {
|
|
spyOn(window, 'requestAnimationFrame').andCallFake(fn => fn())
|
|
|
|
const element = buildTextEditorElement()
|
|
jasmine.attachToDOM(element)
|
|
|
|
expect(element.isUpdatedSynchronously()).toBe(false)
|
|
|
|
element.getModel().setText('hello')
|
|
expect(window.requestAnimationFrame).toHaveBeenCalled()
|
|
|
|
expect(element.textContent).toContain('hello')
|
|
|
|
window.requestAnimationFrame.reset()
|
|
element.setUpdatedSynchronously(true)
|
|
element.getModel().setText('goodbye')
|
|
expect(window.requestAnimationFrame).not.toHaveBeenCalled()
|
|
expect(element.textContent).toContain('goodbye')
|
|
})
|
|
)
|
|
|
|
describe('::getDefaultCharacterWidth', () => {
|
|
it('returns 0 before the element is attached', () => {
|
|
const element = buildTextEditorElement({attach: false})
|
|
expect(element.getDefaultCharacterWidth()).toBe(0)
|
|
})
|
|
|
|
it('returns the width of a character in the root scope', () => {
|
|
const element = buildTextEditorElement()
|
|
jasmine.attachToDOM(element)
|
|
expect(element.getDefaultCharacterWidth()).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
describe('::getMaxScrollTop', () =>
|
|
it('returns the maximum scroll top that can be applied to the element', async () => {
|
|
const editor = new TextEditor()
|
|
editor.setText('1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16')
|
|
const element = editor.getElement()
|
|
element.style.lineHeight = '10px'
|
|
element.style.width = '200px'
|
|
jasmine.attachToDOM(element)
|
|
|
|
expect(element.getMaxScrollTop()).toBe(0)
|
|
await editor.update({autoHeight: false})
|
|
|
|
element.style.height = '100px'
|
|
await element.getNextUpdatePromise()
|
|
expect(element.getMaxScrollTop()).toBe(60)
|
|
|
|
element.style.height = '120px'
|
|
await element.getNextUpdatePromise()
|
|
expect(element.getMaxScrollTop()).toBe(40)
|
|
|
|
element.style.height = '200px'
|
|
await element.getNextUpdatePromise()
|
|
expect(element.getMaxScrollTop()).toBe(0)
|
|
})
|
|
)
|
|
|
|
describe('::setScrollTop and ::setScrollLeft', () => {
|
|
it('changes the scroll position', async () => {
|
|
element = buildTextEditorElement()
|
|
element.getModel().update({autoHeight: false})
|
|
element.getModel().setText('lorem\nipsum\ndolor\nsit\namet')
|
|
element.setHeight(20)
|
|
await element.getNextUpdatePromise()
|
|
element.setWidth(20)
|
|
await element.getNextUpdatePromise()
|
|
|
|
element.setScrollTop(22)
|
|
await element.getNextUpdatePromise()
|
|
expect(element.getScrollTop()).toBe(22)
|
|
|
|
element.setScrollLeft(32)
|
|
await element.getNextUpdatePromise()
|
|
expect(element.getScrollLeft()).toBe(32)
|
|
})
|
|
})
|
|
|
|
describe('on TextEditor::setMini', () =>
|
|
it("changes the element's 'mini' attribute", async () => {
|
|
const element = buildTextEditorElement()
|
|
expect(element.hasAttribute('mini')).toBe(false)
|
|
element.getModel().setMini(true)
|
|
await element.getNextUpdatePromise()
|
|
expect(element.hasAttribute('mini')).toBe(true)
|
|
element.getModel().setMini(false)
|
|
await element.getNextUpdatePromise()
|
|
expect(element.hasAttribute('mini')).toBe(false)
|
|
})
|
|
)
|
|
|
|
describe('::intersectsVisibleRowRange(start, end)', () => {
|
|
it('returns true if the given row range intersects the visible row range', async () => {
|
|
const element = buildTextEditorElement()
|
|
const editor = element.getModel()
|
|
editor.update({autoHeight: false})
|
|
element.getModel().setText('x\n'.repeat(20))
|
|
element.style.height = '120px'
|
|
await element.getNextUpdatePromise()
|
|
element.setScrollTop(80)
|
|
await element.getNextUpdatePromise()
|
|
expect(element.getVisibleRowRange()).toEqual([4, 11])
|
|
|
|
expect(element.intersectsVisibleRowRange(0, 4)).toBe(false)
|
|
expect(element.intersectsVisibleRowRange(0, 5)).toBe(true)
|
|
expect(element.intersectsVisibleRowRange(5, 8)).toBe(true)
|
|
expect(element.intersectsVisibleRowRange(11, 12)).toBe(false)
|
|
expect(element.intersectsVisibleRowRange(12, 13)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('::pixelRectForScreenRange(range)', () => {
|
|
it('returns a {top/left/width/height} object describing the rectangle between two screen positions, even if they are not on screen', async () => {
|
|
const element = buildTextEditorElement()
|
|
const editor = element.getModel()
|
|
editor.update({autoHeight: false})
|
|
element.getModel().setText('xxxxxxxxxxxxxxxxxxxxxx\n'.repeat(20))
|
|
element.style.height = '120px'
|
|
await element.getNextUpdatePromise()
|
|
element.setScrollTop(80)
|
|
await element.getNextUpdatePromise()
|
|
expect(element.getVisibleRowRange()).toEqual([4, 11])
|
|
|
|
expect(element.pixelRectForScreenRange([[2, 3], [13, 11]])).toEqual({top: 34, left: 22, height: 204, width: 57})
|
|
})
|
|
})
|
|
|
|
describe('events', () => {
|
|
let element = null
|
|
|
|
beforeEach(async () => {
|
|
element = buildTextEditorElement()
|
|
element.getModel().update({autoHeight: false})
|
|
element.getModel().setText('lorem\nipsum\ndolor\nsit\namet')
|
|
element.setHeight(20)
|
|
await element.getNextUpdatePromise()
|
|
element.setWidth(20)
|
|
await element.getNextUpdatePromise()
|
|
})
|
|
|
|
describe('::onDidChangeScrollTop(callback)', () =>
|
|
it('triggers even when subscribing before attaching the element', () => {
|
|
const positions = []
|
|
const subscription1 = element.onDidChangeScrollTop(p => positions.push(p))
|
|
element.onDidChangeScrollTop(p => positions.push(p))
|
|
|
|
positions.length = 0
|
|
element.setScrollTop(10)
|
|
expect(positions).toEqual([10, 10])
|
|
|
|
element.remove()
|
|
jasmine.attachToDOM(element)
|
|
|
|
positions.length = 0
|
|
element.setScrollTop(20)
|
|
expect(positions).toEqual([20, 20])
|
|
|
|
subscription1.dispose()
|
|
|
|
positions.length = 0
|
|
element.setScrollTop(30)
|
|
expect(positions).toEqual([30])
|
|
})
|
|
)
|
|
|
|
describe('::onDidChangeScrollLeft(callback)', () =>
|
|
it('triggers even when subscribing before attaching the element', () => {
|
|
const positions = []
|
|
const subscription1 = element.onDidChangeScrollLeft(p => positions.push(p))
|
|
element.onDidChangeScrollLeft(p => positions.push(p))
|
|
|
|
positions.length = 0
|
|
element.setScrollLeft(10)
|
|
expect(positions).toEqual([10, 10])
|
|
|
|
element.remove()
|
|
jasmine.attachToDOM(element)
|
|
|
|
positions.length = 0
|
|
element.setScrollLeft(20)
|
|
expect(positions).toEqual([20, 20])
|
|
|
|
subscription1.dispose()
|
|
|
|
positions.length = 0
|
|
element.setScrollLeft(30)
|
|
expect(positions).toEqual([30])
|
|
})
|
|
)
|
|
})
|
|
})
|