|
|
/* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ const path = require('path') const fs = require('fs-plus') const temp = require('temp').track() const url = require('url') const { TextEditor } = require('atom') const MarkdownPreviewView = require('../lib/markdown-preview-view') const TextMateLanguageMode = new TextEditor().getBuffer().getLanguageMode() .constructor
describe('MarkdownPreviewView', function () { let preview = null
beforeEach(function () { // Makes _.debounce work
jasmine.useRealClock()
jasmine.unspy(TextMateLanguageMode.prototype, 'tokenizeInBackground')
spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true)
const filePath = atom.project .getDirectories()[0] .resolve('subdir/file.markdown') preview = new MarkdownPreviewView({ filePath }) jasmine.attachToDOM(preview.element)
waitsForPromise(() => atom.packages.activatePackage('language-ruby'))
waitsForPromise(() => atom.packages.activatePackage('language-javascript'))
waitsForPromise(() => atom.packages.activatePackage('markdown-preview')) })
afterEach(() => preview.destroy())
describe('::constructor', function () { it('shows a loading spinner and renders the markdown', function () { preview.showLoading() expect(preview.element.querySelector('.markdown-spinner')).toBeDefined()
waitsForPromise(() => preview.renderMarkdown())
runs(() => expect(preview.element.querySelector('.emoji')).toBeDefined()) })
it('shows an error message when there is an error', function () { preview.showError('Not a real file') expect(preview.element.textContent).toMatch('Failed') })
it('rerenders the markdown and the scrollTop stays the same', function () { waitsForPromise(() => preview.renderMarkdown())
runs(function () { preview.element.style.maxHeight = '10px' preview.element.scrollTop = 24 expect(preview.element.scrollTop).toBe(24) })
waitsForPromise(() => preview.renderMarkdown())
runs(() => expect(preview.element.scrollTop).toBe(24)) }) })
describe('serialization', function () { let newPreview = null
afterEach(function () { if (newPreview) { newPreview.destroy() } })
it('recreates the preview when serialized/deserialized', function () { newPreview = atom.deserializers.deserialize(preview.serialize()) jasmine.attachToDOM(newPreview.element) expect(newPreview.getPath()).toBe(preview.getPath()) })
it('does not recreate a preview when the file no longer exists', function () { const filePath = path.join(temp.mkdirSync('markdown-preview-'), 'foo.md') fs.writeFileSync(filePath, '# Hi')
preview.destroy() preview = new MarkdownPreviewView({ filePath }) const serialized = preview.serialize() fs.removeSync(filePath)
newPreview = atom.deserializers.deserialize(serialized) expect(newPreview).toBeUndefined() })
it('serializes the editor id when opened for an editor', function () { preview.destroy()
waitsForPromise(() => atom.workspace.open('new.markdown'))
runs(function () { preview = new MarkdownPreviewView({ editorId: atom.workspace.getActiveTextEditor().id })
jasmine.attachToDOM(preview.element) expect(preview.getPath()).toBe( atom.workspace.getActiveTextEditor().getPath() )
newPreview = atom.deserializers.deserialize(preview.serialize()) jasmine.attachToDOM(newPreview.element) expect(newPreview.getPath()).toBe(preview.getPath()) }) }) })
describe('code block conversion to atom-text-editor tags', function () { beforeEach(function () { waitsForPromise(() => preview.renderMarkdown()) })
it('removes line decorations on rendered code blocks', function () { const editor = preview.element.querySelector( "atom-text-editor[data-grammar='text plain null-grammar']" ) const decorations = editor .getModel() .getDecorations({ class: 'cursor-line', type: 'line' }) expect(decorations.length).toBe(0) })
it('sets the editors as read-only', function () { preview.element .querySelectorAll('atom-text-editor') .forEach(editorElement => expect(editorElement.getAttribute('tabindex')).toBeNull() ) })
describe("when the code block's fence name has a matching grammar", function () { it('assigns the grammar on the atom-text-editor', function () { const rubyEditor = preview.element.querySelector( "atom-text-editor[data-grammar='source ruby']" ) expect(rubyEditor.getModel().getText()).toBe(`\
def func x = 1 end\ `)
// nested in a list item
const jsEditor = preview.element.querySelector( "atom-text-editor[data-grammar='source js']" ) expect(jsEditor.getModel().getText()).toBe(`\
if a === 3 { b = 5 }\ `)
}) })
describe("when the code block's fence name doesn't have a matching grammar", function () { it('does not assign a specific grammar', function () { const plainEditor = preview.element.querySelector( "atom-text-editor[data-grammar='text plain null-grammar']" ) expect(plainEditor.getModel().getText()).toBe(`\
function f(x) { return x++; }\ `)
}) })
describe('when an editor cannot find the grammar that is later loaded', function () { it('updates the editor grammar', function () { let renderSpy = null
if (typeof atom.grammars.onDidRemoveGrammar !== 'function') { // TODO: Remove once atom.grammars.onDidRemoveGrammar is released
waitsForPromise(() => atom.packages.activatePackage('language-gfm')) }
runs( () => (renderSpy = spyOn(preview, 'renderMarkdown').andCallThrough()) )
waitsForPromise(() => atom.packages.deactivatePackage('language-ruby'))
waitsFor( 'renderMarkdown to be called after disabling a language', () => renderSpy.callCount === 1 )
runs(function () { const rubyEditor = preview.element.querySelector( "atom-text-editor[data-grammar='source ruby']" ) expect(rubyEditor).toBeNull() })
waitsForPromise(() => atom.packages.activatePackage('language-ruby'))
waitsFor( 'renderMarkdown to be called after enabling a language', () => renderSpy.callCount === 2 )
runs(function () { const rubyEditor = preview.element.querySelector( "atom-text-editor[data-grammar='source ruby']" ) expect(rubyEditor.getModel().getText()).toBe(`\
def func x = 1 end\ `)
}) }) }) })
describe('image resolving', function () { beforeEach(function () { waitsForPromise(() => preview.renderMarkdown()) })
describe('when the image uses a relative path', function () { it('resolves to a path relative to the file', function () { const image = preview.element.querySelector('img[alt=Image1]') expect(image.getAttribute('src')).toBe( atom.project.getDirectories()[0].resolve('subdir/image1.png') ) }) })
describe('when the image uses an absolute path that does not exist', function () { it('resolves to a path relative to the project root', function () { const image = preview.element.querySelector('img[alt=Image2]') expect(image.src).toMatch( url.parse(atom.project.getDirectories()[0].resolve('tmp/image2.png')) ) }) })
describe('when the image uses an absolute path that exists', function () { it("doesn't change the URL when allowUnsafeProtocols is true", function () { preview.destroy()
atom.config.set('markdown-preview.allowUnsafeProtocols', true)
const filePath = path.join(temp.mkdirSync('atom'), 'foo.md') fs.writeFileSync(filePath, `![absolute](${filePath})`) preview = new MarkdownPreviewView({ filePath }) jasmine.attachToDOM(preview.element)
waitsForPromise(() => preview.renderMarkdown())
runs(() => expect( preview.element.querySelector('img[alt=absolute]').src ).toMatch(url.parse(filePath)) ) }) })
it('removes the URL when allowUnsafeProtocols is false', function () { preview.destroy()
atom.config.set('markdown-preview.allowUnsafeProtocols', false)
const filePath = path.join(temp.mkdirSync('atom'), 'foo.md') fs.writeFileSync(filePath, `![absolute](${filePath})`) preview = new MarkdownPreviewView({ filePath }) jasmine.attachToDOM(preview.element)
waitsForPromise(() => preview.renderMarkdown())
runs(() => expect(preview.element.querySelector('img[alt=absolute]').src).toMatch( '' ) ) })
describe('when the image uses a web URL', function () { it("doesn't change the URL", function () { const image = preview.element.querySelector('img[alt=Image3]') expect(image.src).toBe('http://github.com/image3.png') }) }) })
describe('gfm newlines', function () { describe('when gfm newlines are not enabled', function () { it('creates a single paragraph with <br>', function () { atom.config.set('markdown-preview.breakOnSingleNewline', false)
waitsForPromise(() => preview.renderMarkdown())
runs(() => expect( preview.element.querySelectorAll('p:last-child br').length ).toBe(0) ) }) })
describe('when gfm newlines are enabled', function () { it('creates a single paragraph with no <br>', function () { atom.config.set('markdown-preview.breakOnSingleNewline', true)
waitsForPromise(() => preview.renderMarkdown())
runs(() => expect( preview.element.querySelectorAll('p:last-child br').length ).toBe(1) ) }) }) })
describe('yaml front matter', function () { it('creates a table with the YAML variables', function () { atom.config.set('markdown-preview.breakOnSingleNewline', true)
waitsForPromise(() => preview.renderMarkdown())
runs(() => { expect( [...preview.element.querySelectorAll('table th')].map( el => el.textContent ) ).toEqual(['variable1', 'array']) expect( [...preview.element.querySelectorAll('table td')].map( el => el.textContent ) ).toEqual(['value1', 'foo,bar']) }) }) })
describe('text selections', function () { it('adds the `has-selection` class to the preview depending on if there is a text selection', function () { expect(preview.element.classList.contains('has-selection')).toBe(false)
const selection = window.getSelection() selection.removeAllRanges() selection.selectAllChildren(document.querySelector('atom-text-editor'))
waitsFor( () => preview.element.classList.contains('has-selection') === true )
runs(() => selection.removeAllRanges())
waitsFor( () => preview.element.classList.contains('has-selection') === false ) }) })
describe('when core:save-as is triggered', function () { beforeEach(function () { preview.destroy() const filePath = atom.project .getDirectories()[0] .resolve('subdir/code-block.md') preview = new MarkdownPreviewView({ filePath }) // Add to workspace for core:save-as command to be propagated up to the workspace
waitsForPromise(() => atom.workspace.open(preview)) runs(() => jasmine.attachToDOM(atom.views.getView(atom.workspace))) })
it('saves the rendered HTML and opens it', function () { const outputPath = fs.realpathSync(temp.mkdirSync()) + 'output.html'
const createRule = (selector, css) => ({ selectorText: selector, cssText: `${selector} ${css}` }) const markdownPreviewStyles = [ { rules: [createRule('.markdown-preview', '{ color: orange; }')] }, { rules: [ createRule('.not-included', '{ color: green; }'), createRule('.markdown-preview :host', '{ color: purple; }') ] } ]
const atomTextEditorStyles = [ 'atom-text-editor .line { color: brown; }\natom-text-editor .number { color: cyan; }', 'atom-text-editor :host .something { color: black; }', 'atom-text-editor .hr { background: url(atom://markdown-preview/assets/hr.png); }' ]
waitsForPromise(() => preview.renderMarkdown())
runs(() => { expect(fs.isFileSync(outputPath)).toBe(false) spyOn(preview, 'getSaveDialogOptions').andReturn({ defaultPath: outputPath }) spyOn(atom.applicationDelegate, 'showSaveDialog').andCallFake(function ( options, callback ) { if (typeof callback === 'function') { callback(options.defaultPath) } // TODO: When https://github.com/atom/atom/pull/16245 lands remove the return
// and the existence check on the callback
return options.defaultPath }) spyOn(preview, 'getDocumentStyleSheets').andReturn( markdownPreviewStyles ) spyOn(preview, 'getTextEditorStyles').andReturn(atomTextEditorStyles) })
waitsForPromise(() => atom.commands.dispatch(preview.element, 'core:save-as') )
waitsFor(() => { const activeEditor = atom.workspace.getActiveTextEditor() return activeEditor && activeEditor.getPath() === outputPath })
runs(() => { const element = document.createElement('div') element.innerHTML = fs.readFileSync(outputPath) expect(element.querySelector('h1').innerText).toBe('Code Block') expect( element.querySelector( '.line .syntax--source.syntax--js .syntax--constant.syntax--numeric' ).innerText ).toBe('3') expect( element.querySelector( '.line .syntax--source.syntax--js .syntax--keyword.syntax--control' ).innerText ).toBe('if') expect( element.querySelector( '.line .syntax--source.syntax--js .syntax--constant.syntax--numeric' ).innerText ).toBe('3') }) })
describe('text editor style extraction', function () { let [extractedStyles] = []
const textEditorStyle = '.editor-style .extraction-test { color: blue; }' const unrelatedStyle = '.something else { color: red; }'
beforeEach(function () { atom.styles.addStyleSheet(textEditorStyle, { context: 'atom-text-editor' })
atom.styles.addStyleSheet(unrelatedStyle, { context: 'unrelated-context' })
return (extractedStyles = preview.getTextEditorStyles()) })
it('returns an array containing atom-text-editor css style strings', function () { expect(extractedStyles.indexOf(textEditorStyle)).toBeGreaterThan(-1) })
it('does not return other styles', function () { expect(extractedStyles.indexOf(unrelatedStyle)).toBe(-1) }) }) })
describe('when core:copy is triggered', function () { beforeEach(function () { preview.destroy() preview.element.remove()
const filePath = atom.project .getDirectories()[0] .resolve('subdir/code-block.md') preview = new MarkdownPreviewView({ filePath }) jasmine.attachToDOM(preview.element)
waitsForPromise(() => preview.renderMarkdown()) })
describe('when there is no text selected', function () { it('copies the rendered HTML of the entire Markdown document to the clipboard', function () { expect(atom.clipboard.read()).toBe('initial clipboard content')
waitsForPromise(() => atom.commands.dispatch(preview.element, 'core:copy') )
runs(() => { const element = document.createElement('div') element.innerHTML = atom.clipboard.read() expect(element.querySelector('h1').innerText).toBe('Code Block') expect( element.querySelector( '.line .syntax--source.syntax--js .syntax--constant.syntax--numeric' ).innerText ).toBe('3') expect( element.querySelector( '.line .syntax--source.syntax--js .syntax--keyword.syntax--control' ).innerText ).toBe('if') expect( element.querySelector( '.line .syntax--source.syntax--js .syntax--constant.syntax--numeric' ).innerText ).toBe('3') }) }) })
describe('when there is a text selection', function () { it('directly copies the selection to the clipboard', function () { const selection = window.getSelection() selection.removeAllRanges() const range = document.createRange() range.setStart(document.querySelector('atom-text-editor'), 0) range.setEnd(document.querySelector('p').firstChild, 3) selection.addRange(range)
atom.commands.dispatch(preview.element, 'core:copy') const clipboardText = atom.clipboard.read()
expect(clipboardText).toBe(`\
if a === 3 { b = 5 }
enc\ `)
}) }) })
describe('when markdown-preview:select-all is triggered', function () { it('selects the entire Markdown preview', function () { const filePath = atom.project .getDirectories()[0] .resolve('subdir/code-block.md') const preview2 = new MarkdownPreviewView({ filePath }) jasmine.attachToDOM(preview2.element)
waitsForPromise(() => preview.renderMarkdown())
runs(function () { atom.commands.dispatch(preview.element, 'markdown-preview:select-all') const { commonAncestorContainer } = window.getSelection().getRangeAt(0) expect(commonAncestorContainer).toEqual(preview.element) })
waitsForPromise(() => preview2.renderMarkdown())
runs(() => { atom.commands.dispatch(preview2.element, 'markdown-preview:select-all') const selection = window.getSelection() expect(selection.rangeCount).toBe(1) const { commonAncestorContainer } = selection.getRangeAt(0) expect(commonAncestorContainer).toEqual(preview2.element) }) }) })
describe('when markdown-preview:zoom-in or markdown-preview:zoom-out are triggered', function () { it('increases or decreases the zoom level of the markdown preview element', function () { jasmine.attachToDOM(preview.element)
waitsForPromise(() => preview.renderMarkdown())
runs(function () { const originalZoomLevel = getComputedStyle(preview.element).zoom atom.commands.dispatch(preview.element, 'markdown-preview:zoom-in') expect(getComputedStyle(preview.element).zoom).toBeGreaterThan( originalZoomLevel ) atom.commands.dispatch(preview.element, 'markdown-preview:zoom-out') expect(getComputedStyle(preview.element).zoom).toBe(originalZoomLevel) }) }) }) })
|