123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- const TextEditor = require('../src/text-editor');
- const TextEditorElement = require('../src/text-editor-element');
- describe('TextEditorElement', () => {
- let jasmineContent;
- beforeEach(() => {
- jasmineContent = document.body.querySelector('#jasmine-content');
- // Force scrollbars to be visible regardless of local system configuration
- const scrollbarStyle = document.createElement('style');
- scrollbarStyle.textContent =
- 'atom-text-editor ::-webkit-scrollbar { -webkit-appearance: none }';
- jasmine.attachToDOM(scrollbarStyle);
- });
- function buildTextEditorElement(options = {}) {
- const element = TextEditorElement.createTextEditorElement();
- 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 'readonly' attribute", async function() {
- jasmineContent.innerHTML = '<atom-text-editor readonly>';
- const element = jasmineContent.firstChild;
- expect(element.getComponent().isInputEnabled()).toBe(false);
- element.removeAttribute('readonly');
- expect(element.getComponent().isInputEnabled()).toBe(true);
- element.setAttribute('readonly', true);
- expect(element.getComponent().isInputEnabled()).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('tabIndex', () => {
- it('uses a default value of -1', () => {
- jasmineContent.innerHTML = '<atom-text-editor />';
- const element = jasmineContent.firstChild;
- expect(element.tabIndex).toBe(-1);
- expect(element.querySelector('input').tabIndex).toBe(-1);
- });
- it('uses the custom value when given', () => {
- jasmineContent.innerHTML = '<atom-text-editor tabIndex="42" />';
- const element = jasmineContent.firstChild;
- expect(element.tabIndex).toBe(-1);
- expect(element.querySelector('input').tabIndex).toBe(42);
- });
- });
- describe('when the model is assigned', () =>
- it("adds the 'mini' attribute if .isMini() returns true on the model", async () => {
- const element = buildTextEditorElement();
- element.getModel().update({ mini: true });
- await atom.views.getNextUpdatePromise();
- expect(element.hasAttribute('mini')).toBe(true);
- }));
- 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 HTMLElement {
- connectedCallback() {
- this.firstChild.focus();
- }
- }
- window.customElements.define(
- 'element-that-focuses-child',
- ElementThatFocusesChild
- );
- 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('if focused when invisible due to a zero height and width', () => {
- it('focuses the hidden input and does not throw an exception', () => {
- const parentElement = document.createElement('div');
- parentElement.style.position = 'absolute';
- parentElement.style.width = '0px';
- parentElement.style.height = '0px';
- const element = buildTextEditorElement({ attach: false });
- parentElement.appendChild(element);
- jasmineContent.appendChild(parentElement);
- element.focus();
- expect(document.activeElement).toBe(element.component.getHiddenInput());
- });
- });
- });
- describe('::setModel', () => {
- describe('when the element does not have an editor yet', () => {
- it('uses the supplied one', () => {
- const element = buildTextEditorElement({ attach: false });
- const editor = new TextEditor();
- element.setModel(editor);
- jasmine.attachToDOM(element);
- expect(editor.element).toBe(element);
- expect(element.getModel()).toBe(editor);
- });
- });
- describe('when the element already has an editor', () => {
- it('unbinds it and then swaps it with the supplied one', async () => {
- const element = buildTextEditorElement({ attach: true });
- const previousEditor = element.getModel();
- expect(previousEditor.element).toBe(element);
- const newEditor = new TextEditor();
- element.setModel(newEditor);
- expect(previousEditor.element).not.toBe(element);
- expect(newEditor.element).toBe(element);
- expect(element.getModel()).toBe(newEditor);
- });
- });
- });
- 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();
- 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);
- const horizontalScrollbarHeight = element.component.getHorizontalScrollbarHeight();
- expect(element.getMaxScrollTop()).toBe(0);
- await editor.update({ autoHeight: false });
- element.style.height = 100 + horizontalScrollbarHeight + 'px';
- await element.getNextUpdatePromise();
- expect(element.getMaxScrollTop()).toBe(60);
- element.style.height = 120 + horizontalScrollbarHeight + 'px';
- await element.getNextUpdatePromise();
- expect(element.getMaxScrollTop()).toBe(40);
- element.style.height = 200 + horizontalScrollbarHeight + 'px';
- await element.getNextUpdatePromise();
- expect(element.getMaxScrollTop()).toBe(0);
- }));
- describe('::setScrollTop and ::setScrollLeft', () => {
- it('changes the scroll position', async () => {
- const 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();
- const horizontalScrollbarHeight = element.component.getHorizontalScrollbarHeight();
- editor.update({ autoHeight: false });
- element.getModel().setText('x\n'.repeat(20));
- element.style.height = 120 + horizontalScrollbarHeight + 'px';
- 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();
- const horizontalScrollbarHeight = element.component.getHorizontalScrollbarHeight();
- editor.update({ autoHeight: false });
- element.getModel().setText('xxxxxxxxxxxxxxxxxxxxxx\n'.repeat(20));
- element.style.height = 120 + horizontalScrollbarHeight + 'px';
- await element.getNextUpdatePromise();
- element.setScrollTop(80);
- await element.getNextUpdatePromise();
- expect(element.getVisibleRowRange()).toEqual([4, 11]);
- const top = 2 * editor.getLineHeightInPixels();
- const bottom = 13 * editor.getLineHeightInPixels();
- const left = Math.round(3 * editor.getDefaultCharWidth());
- const right = Math.round(11 * editor.getDefaultCharWidth());
- const pixelRect = element.pixelRectForScreenRange([[2, 3], [13, 11]]);
- expect(pixelRect.top).toEqual(top);
- expect(pixelRect.left).toEqual(left);
- expect(pixelRect.height).toEqual(
- bottom + editor.getLineHeightInPixels() - top
- );
- expect(pixelRect.width).toBeNear(right - left);
- });
- });
- 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]);
- }));
- });
- });
|