You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
604 lines
19 KiB
604 lines
19 KiB
/*
|
|
* 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)
|
|
})
|
|
})
|
|
})
|
|
})
|