Skip to content

Commit

Permalink
test: add component tests
Browse files Browse the repository at this point in the history
  • Loading branch information
adamdehaven committed Jan 4, 2024
1 parent a420aa5 commit 55566b9
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 36 deletions.
104 changes: 86 additions & 18 deletions src/components/MarkdownUi.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Vitest unit test spec file

import { vi, describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { mount, flushPromises } from '@vue/test-utils'
import { ref } from 'vue'
import MarkdownUi from './MarkdownUi.vue'
import { EDITOR_DEBOUNCE_TIMEOUT } from '@/constants'

Check failure on line 7 in src/components/MarkdownUi.spec.ts

View workflow job for this annotation

GitHub Actions / Tests / Run Tests

'EDITOR_DEBOUNCE_TIMEOUT' is defined but never used
import { KUI_BREAKPOINT_PHABLET } from '@kong/design-tokens'
import type { Theme } from '@/types'

Expand All @@ -14,6 +15,7 @@ const defaultContent = `# ${defaultText}`
/**
* The markdown content takes roughly 400ms to initialize `markdown-it` and render, so await this function in each test immediately after calling `mount()` that is not in `edit` mode.
* @param wrapper The component wrapper
* @example await waitForMarkdownRender(wrapper)
*/
const waitForMarkdownRender = async (wrapper: any): Promise<void> => {
await vi.waitUntil(
Expand All @@ -27,6 +29,26 @@ const waitForMarkdownRender = async (wrapper: any): Promise<void> => {
expect(wrapper.findTestId('markdown-content').element.innerHTML.length).toBeGreaterThan(0)
}

/**
* When text is entered into the editor, the `update:modelValue` event is fired on a debounce. This function waits for the event to be emitted before continuing
* @param wrapper The component wrapper
* @param {string} emit The name of the emitted event to wait for
* @example await waitForEmittedEvent(wrapper, 'update:modelValue')
*/
const waitForEmittedEvent = async (wrapper: any, emit: string): Promise<void> => {
if (!emit) {
throw new Error('waitForEmittedEvent: `emit` string param is required')
}

await vi.waitFor(async () => {
expect(wrapper.emitted()).toHaveProperty(emit)
},
{
timeout: 1000,
interval: 50,
})
}

// Stub the return value for useMediaQuery to determine if browser is at least phablet width
const mediaQuerySpy = ({
isPhabletWidth = true,
Expand Down Expand Up @@ -67,7 +89,6 @@ describe('<MarkdownUi />', () => {

await waitForMarkdownRender(wrapper)

expect(wrapper.findTestId('markdown-ui').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-content').isVisible()).toBe(true)
// Ensure markdown is rendered into tags and content
expect(wrapper.findTestId('markdown-content').find('h1').isVisible()).toBe(true)
Expand All @@ -88,7 +109,6 @@ describe('<MarkdownUi />', () => {

await waitForMarkdownRender(wrapper)

expect(wrapper.findTestId('markdown-ui').isVisible()).toBe(true)
// Ensure markdown is rendered into tags and content
expect(wrapper.findTestId('markdown-content').find('h1').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-content').find('h1').text()).toEqual(defaultText)
Expand All @@ -111,7 +131,6 @@ describe('<MarkdownUi />', () => {

await waitForMarkdownRender(wrapper)

expect(wrapper.findTestId('markdown-ui').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-content').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-content').text()).toContain(defaultText)
// Elements should not exist
Expand All @@ -133,19 +152,30 @@ describe('<MarkdownUi />', () => {

// No need to wait in edit mode

expect(wrapper.findTestId('markdown-ui').isVisible()).toBe(true)
expect(wrapper.findTestId('toolbar').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-editor-textarea').isVisible()).toBe(true)
// Expect original text
expect((wrapper.findTestId('markdown-editor-textarea').element as HTMLTextAreaElement).value).toEqual(text)
expect(wrapper.findTestId<'textarea'>('markdown-editor-textarea').element.value).toEqual(text)
// Elements should not exist
expect(wrapper.findTestId('markdown-content').exists()).toBe(false)
expect(wrapper.findTestId('edit').exists()).toBe(false)

// Set the new text
await wrapper.findTestId('markdown-editor-textarea').setValue(newText)

await flushPromises()

const eventName = 'update:modelValue'

// Verify event is emitted
await waitForEmittedEvent(wrapper, eventName)

// Verify the emitted event
expect(wrapper.emitted(eventName) || []).toHaveLength(1)
expect(wrapper.emitted(eventName)![0]).toEqual([newText])

// Expect new text
expect((wrapper.findTestId('markdown-editor-textarea').element as HTMLTextAreaElement).value).toEqual(newText)
expect(wrapper.findTestId<'textarea'>('markdown-editor-textarea').element.value).toEqual(newText)
})
})

Expand All @@ -161,12 +191,11 @@ describe('<MarkdownUi />', () => {

await waitForMarkdownRender(wrapper)

expect(wrapper.findTestId('markdown-ui').isVisible()).toBe(true)
expect(wrapper.findTestId('toolbar').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-editor-textarea').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-content').isVisible()).toBe(true)
// Expect text
expect((wrapper.findTestId('markdown-editor-textarea').element as HTMLTextAreaElement).value).toEqual(defaultContent)
expect(wrapper.findTestId<'textarea'>('markdown-editor-textarea').element.value).toEqual(defaultContent)
// Ensure markdown is rendered into tags and content
expect(wrapper.findTestId('markdown-content').find('h1').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-content').find('h1').text()).toEqual(defaultText)
Expand All @@ -187,7 +216,6 @@ describe('<MarkdownUi />', () => {

await waitForMarkdownRender(wrapper)

expect(wrapper.findTestId('markdown-ui').isVisible()).toBe(true)
expect(wrapper.findTestId('toolbar').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-content').isVisible()).toBe(true)
// Ensure markdown is rendered into tags and content
Expand Down Expand Up @@ -224,7 +252,6 @@ describe('<MarkdownUi />', () => {

await waitForMarkdownRender(wrapper)

expect(wrapper.findTestId('markdown-ui').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-content').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-content').text()).toContain(codeContent)
// Copy button should be visible
Expand All @@ -249,7 +276,6 @@ describe('<MarkdownUi />', () => {

await waitForMarkdownRender(wrapper)

expect(wrapper.findTestId('markdown-ui').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-content').isVisible()).toBe(true)
// Ensure markdown is rendered into tags and content
expect(wrapper.findTestId('markdown-content').find('h1').isVisible()).toBe(true)
Expand All @@ -274,7 +300,7 @@ describe('<MarkdownUi />', () => {

// Both panes should be visible
expect(wrapper.findTestId('markdown-editor-textarea').isVisible()).toBe(true)
expect((wrapper.findTestId('markdown-editor-textarea').element as HTMLTextAreaElement).value).toEqual(defaultContent)
expect(wrapper.findTestId<'textarea'>('markdown-editor-textarea').element.value).toEqual(defaultContent)

expect(wrapper.findTestId('markdown-content').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-content').find('h1').isVisible()).toBe(true)
Expand All @@ -301,7 +327,7 @@ describe('<MarkdownUi />', () => {

expect(wrapper.findTestId('markdown-editor-textarea').isVisible()).toBe(true)
// Expect original text
expect((wrapper.findTestId('markdown-editor-textarea').element as HTMLTextAreaElement).value).toEqual(defaultContent)
expect(wrapper.findTestId<'textarea'>('markdown-editor-textarea').element.value).toEqual(defaultContent)
// Elements should not exist
expect(wrapper.findTestId('markdown-content').exists()).toBe(false)
expect(wrapper.findTestId('edit').exists()).toBe(false)
Expand Down Expand Up @@ -346,13 +372,12 @@ describe('<MarkdownUi />', () => {

await waitForMarkdownRender(wrapper)

expect(wrapper.findTestId('markdown-ui').isVisible()).toBe(true)
// Ensure the wrapper class does not exist
expect(wrapper.findTestId('markdown-ui').classes('fullscreen')).toBe(false)

// Both panes should be visible
expect(wrapper.findTestId('markdown-editor-textarea').isVisible()).toBe(true)
expect((wrapper.findTestId('markdown-editor-textarea').element as HTMLTextAreaElement).value).toEqual(defaultContent)
expect(wrapper.findTestId<'textarea'>('markdown-editor-textarea').element.value).toEqual(defaultContent)

expect(wrapper.findTestId('markdown-content').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-content').find('h1').isVisible()).toBe(true)
Expand Down Expand Up @@ -386,7 +411,6 @@ describe('<MarkdownUi />', () => {

await waitForMarkdownRender(wrapper)

expect(wrapper.findTestId('markdown-ui').isVisible()).toBe(true)
// Ensure the wrapper class does not exist
expect(wrapper.findTestId('markdown-content').classes('html-preview')).toBe(false)
// Ensure the HTML preview does not exist
Expand All @@ -395,7 +419,7 @@ describe('<MarkdownUi />', () => {

// Both panes should be visible
expect(wrapper.findTestId('markdown-editor-textarea').isVisible()).toBe(true)
expect((wrapper.findTestId('markdown-editor-textarea').element as HTMLTextAreaElement).value).toEqual(defaultContent)
expect(wrapper.findTestId<'textarea'>('markdown-editor-textarea').element.value).toEqual(defaultContent)

expect(wrapper.findTestId('markdown-content').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-content').find('h1').isVisible()).toBe(true)
Expand All @@ -415,5 +439,49 @@ describe('<MarkdownUi />', () => {
expect(wrapper.find('pre > code.language-html').isVisible()).toBe(true)
expect(wrapper.findTestId('copy-code-button').isVisible()).toBe(true)
})

describe('format buttons', () => {
it('formats text as bold', async () => {
const textStart = 'This is a sentence that needs '
const textMiddle = 'bold text'
const textEnd = ' in the middle.'
const sentence = textStart + textMiddle + textEnd

const wrapper = mount(MarkdownUi, {
props: {
mode: 'split',
editable: true,
modelValue: sentence,
},
})

await waitForMarkdownRender(wrapper)

expect(wrapper.findTestId('toolbar').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-editor-textarea').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-content').isVisible()).toBe(true)
// Expect text
expect(wrapper.findTestId<'textarea'>('markdown-editor-textarea').element.value).toEqual(sentence)
// Ensure markdown is rendered into tags and content
expect(wrapper.findTestId('markdown-content').find('p').isVisible()).toBe(true)
expect(wrapper.findTestId('markdown-content').find('p').text()).toEqual(sentence)

await wrapper.findTestId<'textarea'>('markdown-editor-textarea').element.focus()
// Start the text selection after the start text
wrapper.findTestId<'textarea'>('markdown-editor-textarea').element.selectionStart = textStart.length
// End the text selection after the middle text
wrapper.findTestId<'textarea'>('markdown-editor-textarea').element.selectionEnd = textStart.length + textMiddle.length

// Click the formatting button
await wrapper.findTestId('format-option-bold').trigger('click')

// Verify event is emitted
const eventName = 'update:modelValue'
await waitForEmittedEvent(wrapper, eventName)

expect(wrapper.emitted(eventName) || []).toHaveLength(1)
expect(wrapper.emitted(eventName)![0]).toEqual([`${textStart}**${textMiddle}**${textEnd}`])
})
})
})
})
10 changes: 6 additions & 4 deletions src/components/MarkdownUi.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
>
<textarea
:id="textareaId"
ref="textarea"
autocapitalize="off"
autocomplete="off"
autocorrect="off"
Expand Down Expand Up @@ -190,8 +191,9 @@ const emit = defineEmits<{
(e: 'fullscreen', active: boolean): void
}>()
// Initialize the template ref
const markdownComponent = ref(null)
// Initialize template refs
const textarea = ref<HTMLTextAreaElement | null>(null)
const markdownComponent = ref<HTMLDivElement | null>(null)
const { init: initMarkdownIt, md } = composables.useMarkdownIt()
Expand Down Expand Up @@ -265,7 +267,7 @@ const markdownHtml = ref<string>('')
// A ref to store the preview HTML (if user enables it in the toolbar)
const markdownPreviewHtml = ref<string>('')
const { toggleInlineFormatting, insertMarkdownTemplate } = composables.useMarkdownActions(textareaId.value, rawMarkdown)
const { toggleInlineFormatting, insertMarkdownTemplate } = composables.useMarkdownActions(textarea, rawMarkdown)
// When the user toggles inline formatting
const formatSelection = (format: InlineFormat): void => {
Expand Down Expand Up @@ -439,7 +441,7 @@ const copyCodeBlock = async (e: any): Promise<void> => {
}
// Initialize keyboard shortcuts; they will only fire in edit mode when the textarea is active
composables.useKeyboardShortcuts(textareaId.value, rawMarkdown, tabSize, emulateInputEvent)
composables.useKeyboardShortcuts(textarea, rawMarkdown, tabSize, emulateInputEvent)
onMounted(async () => {
// Initialize markdown-it
Expand Down
8 changes: 4 additions & 4 deletions src/composables/useKeyboardShortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ import type { InlineFormat } from '@/types'

/**
* Utilize keyboard shortcuts in the markdown editor. Must be called at the root of the `setup` function.
* @param {string} textareaId The `id` of the textarea
* @param {Ref} textareaRef The textarea Vue template ref
* @param {Ref<string>} rawMarkdown A Vue ref containing the raw markdown content from the textarea.
* @param {Ref<number>} tabSize The current tab size
* @param {Function} onEditCallback A function to call after toggling the inline text formatting.
* @returns
*/
export default function useKeyboardShortcuts(
textareaId: string,
textareaRef: Ref,
rawMarkdown: Ref<string>,
tabSize: Ref<number>,
onEditCallback: () => void,
) {
// The document.activeElement
const activeElement = useActiveElement()
const textareaIsActive = computed((): boolean => activeElement.value?.id === textareaId)
const { toggleInlineFormatting, insertNewLine, getTextSelection, selectedText, toggleTab } = useMarkdownActions(textareaId, rawMarkdown)
const textareaIsActive = computed((): boolean => activeElement.value?.id === textareaRef.value.id)
const { toggleInlineFormatting, insertNewLine, getTextSelection, selectedText, toggleTab } = useMarkdownActions(textareaRef, rawMarkdown)

const getFormatForKeyEvent = (evt: any): InlineFormat | undefined => {
let format: InlineFormat | undefined
Expand Down
16 changes: 6 additions & 10 deletions src/composables/useMarkdownActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import type { InlineFormat, MarkdownTemplate } from '@/types'

/**
* Utilize the markdown editor actions.
* @param {string} textareaId The `id` of the textarea
* @param {Ref} textareaRef The textarea Vue template ref
* @param {Ref<string>} rawMarkdown A Vue ref containing the raw markdown content from the textarea.
*/
export default function useMarkdownActions(
textareaId: string,
textareaRef: Ref,
rawMarkdown: Ref<string>,
) {
// A reactive object to keep track of the textarea's selection
Expand All @@ -19,17 +19,13 @@ export default function useMarkdownActions(
text: '',
})

/** Utilize the textareaId to obtain a reference to the underlying textarea element */
/** Utilize the textareaRef to obtain a reference to the textarea element */
const getTextarea = (): HTMLTextAreaElement | null => {
// Find the textarea within the component container
const selector = `#${textareaId}`
const textarea: HTMLTextAreaElement | null = document.querySelector(selector)

if (!textarea) {
throw new Error(`Could not find '${selector}'`)
if (!textareaRef.value) {
throw new Error('getTextarea: Could not find the textarea template ref')
}

return textarea
return textareaRef.value
}

/** Get the selection within the textarea */
Expand Down

0 comments on commit 55566b9

Please sign in to comment.