mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-03-25 14:12:28 +00:00
Improve stability and speed of E2E tests (#1319)
* Change cypress settings in CI job * Catch error from highlight js chunk loading in auto completion * Refactor code * Show notification if highlightjs loading failed Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
a4d6de9555
commit
93722f4161
6 changed files with 132 additions and 50 deletions
.github/workflows
public/locales
src/components
editor-page/editor-pane/autocompletion
notifications
11
.github/workflows/e2e.yml
vendored
11
.github/workflows/e2e.yml
vendored
|
@ -18,16 +18,16 @@ jobs:
|
|||
- name: Check out repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Cache build
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: build
|
||||
key: build
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
|
@ -57,6 +57,9 @@ jobs:
|
|||
name: Perform E2E Test in ${{ matrix.browser }}
|
||||
needs: build-frontend
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: cypress/browsers:node14.16.0-chrome90-ff88
|
||||
options: --user 1001 --shm-size=2g
|
||||
strategy:
|
||||
matrix:
|
||||
browser: [ 'chrome', 'firefox' ]
|
||||
|
|
|
@ -461,6 +461,7 @@
|
|||
"successfullyCopied": "Copied!",
|
||||
"copyError": "Error while copying!",
|
||||
"errorOccurred": "An error occurred",
|
||||
"errorWhileLoadingLibrary": "An unexpected error occurred while loading '{{name}}'.\nCheck the browser console for more information.\nReport this error only if it comes up again.",
|
||||
"readForMoreInfo": "Read here for more information"
|
||||
},
|
||||
"login": {
|
||||
|
|
|
@ -5,45 +5,111 @@
|
|||
*/
|
||||
|
||||
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
||||
import { findWordAtCursor, Hinter, search } from './index'
|
||||
import { findWordAtCursor, generateHintListByPrefix, Hinter } from './index'
|
||||
import { DEFAULT_DURATION_IN_SECONDS, dispatchUiNotification } from '../../../../redux/ui-notifications/methods'
|
||||
import i18n from 'i18next'
|
||||
|
||||
type highlightJsImport = typeof import('../../../common/hljs/hljs')
|
||||
|
||||
const wordRegExp = /^```((\w|-|_|\+)*)$/
|
||||
let allSupportedLanguages: string[] = []
|
||||
|
||||
const codeBlockHint = (editor: Editor): Promise<Hints | null> => {
|
||||
return import(/* webpackChunkName: "highlight.js" */ '../../../common/hljs/hljs').then(
|
||||
(hljs) =>
|
||||
new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const term = searchResult[1]
|
||||
if (allSupportedLanguages.length === 0) {
|
||||
allSupportedLanguages = hljs.default
|
||||
.listLanguages()
|
||||
.concat('csv', 'flow', 'html', 'js', 'markmap', 'abc', 'graphviz', 'mermaid', 'vega-lite')
|
||||
}
|
||||
const suggestions = search(term, allSupportedLanguages)
|
||||
const cursor = editor.getCursor()
|
||||
if (!suggestions) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: suggestions.map(
|
||||
(suggestion: string): Hint => ({
|
||||
text: '```' + suggestion + '\n\n```\n',
|
||||
displayText: suggestion
|
||||
})
|
||||
),
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end)
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Fetches the highlight js chunk.
|
||||
* @return the retrieved highlight js api
|
||||
*/
|
||||
const loadHighlightJs = async (): Promise<highlightJsImport | null> => {
|
||||
try {
|
||||
return await import('../../../common/hljs/hljs')
|
||||
} catch (error) {
|
||||
dispatchUiNotification(
|
||||
i18n.t('common.errorOccurred'),
|
||||
i18n.t('common.errorWhileLoadingLibrary', { name: 'highlight.js' }),
|
||||
DEFAULT_DURATION_IN_SECONDS,
|
||||
'exclamation-circle'
|
||||
)
|
||||
console.error("can't load highlight js", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the language from the current line in the editor.
|
||||
*
|
||||
* @param editor The editor that contains the search time
|
||||
* @return null if no search term could be found or the found word and the cursor position.
|
||||
*/
|
||||
const extractSearchTerm = (
|
||||
editor: Editor
|
||||
): null | {
|
||||
searchTerm: string
|
||||
startIndex: number
|
||||
endIndex: number
|
||||
} => {
|
||||
const searchTerm = findWordAtCursor(editor)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
searchTerm: searchResult[1],
|
||||
startIndex: searchTerm.start,
|
||||
endIndex: searchTerm.end
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the list of languages that are supported by highlight js or custom embeddings.
|
||||
* @return An array of language names
|
||||
*/
|
||||
const buildLanguageList = async (): Promise<string[]> => {
|
||||
const highlightJs = await loadHighlightJs()
|
||||
|
||||
if (highlightJs === null) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (allSupportedLanguages.length === 0) {
|
||||
allSupportedLanguages = highlightJs.default
|
||||
.listLanguages()
|
||||
.concat('csv', 'flow', 'html', 'js', 'markmap', 'abc', 'graphviz', 'mermaid', 'vega-lite')
|
||||
}
|
||||
|
||||
return allSupportedLanguages
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a codemirror autocompletion hint with supported highlight js languages.
|
||||
*
|
||||
* @param editor The codemirror editor that requested the autocompletion
|
||||
* @return The generated {@link Hints} or null if no hints exist.
|
||||
*/
|
||||
const codeBlockHint = async (editor: Editor): Promise<Hints | null> => {
|
||||
const searchResult = extractSearchTerm(editor)
|
||||
if (!searchResult) {
|
||||
return null
|
||||
}
|
||||
|
||||
const languages = await buildLanguageList()
|
||||
if (languages.length === 0) {
|
||||
return null
|
||||
}
|
||||
const suggestions = generateHintListByPrefix(searchResult.searchTerm, languages)
|
||||
if (!suggestions) {
|
||||
return null
|
||||
}
|
||||
const lineIndex = editor.getCursor().line
|
||||
return {
|
||||
list: suggestions.map(
|
||||
(suggestion: string): Hint => ({
|
||||
text: '```' + suggestion + '\n\n```\n',
|
||||
displayText: suggestion
|
||||
})
|
||||
)
|
||||
),
|
||||
from: Pos(lineIndex, searchResult.startIndex),
|
||||
to: Pos(lineIndex, searchResult.endIndex)
|
||||
}
|
||||
}
|
||||
|
||||
export const CodeBlockHinter: Hinter = {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
||||
import { findWordAtCursor, Hinter, search } from './index'
|
||||
import { findWordAtCursor, generateHintListByPrefix, Hinter } from './index'
|
||||
|
||||
const wordRegExp = /^(\s{0,3})(#{1,6})$/
|
||||
const allSupportedHeaders = ['# h1', '## h2', '### h3', '#### h4', '##### h5', '###### h6', '###### tags: `example`']
|
||||
|
@ -24,7 +24,7 @@ const headerHint = (editor: Editor): Promise<Hints | null> => {
|
|||
resolve(null)
|
||||
return
|
||||
}
|
||||
const suggestions = search(term, allSupportedHeaders)
|
||||
const suggestions = generateHintListByPrefix(term, allSupportedHeaders)
|
||||
const cursor = editor.getCursor()
|
||||
if (!suggestions) {
|
||||
resolve(null)
|
||||
|
|
|
@ -46,14 +46,15 @@ export const findWordAtCursor = (editor: Editor): findWordAtCursorResponse => {
|
|||
}
|
||||
}
|
||||
|
||||
export const search = (term: string, list: string[]): string[] => {
|
||||
const suggestions: string[] = []
|
||||
list.forEach((item) => {
|
||||
if (item.toLowerCase().startsWith(term.toLowerCase())) {
|
||||
suggestions.push(item)
|
||||
}
|
||||
})
|
||||
return suggestions.slice(0, 7)
|
||||
/**
|
||||
* Generates a list (with max 8 entries) of hints for the autocompletion.
|
||||
*
|
||||
* @param prefix This is the case insensitive prefix that every hint must have
|
||||
* @param hintCandidates The list of hint candidates
|
||||
*/
|
||||
export const generateHintListByPrefix = (prefix: string, hintCandidates: string[]): string[] => {
|
||||
const searchTerm = prefix.toLowerCase()
|
||||
return hintCandidates.filter((item) => item.toLowerCase().startsWith(searchTerm)).slice(0, 7)
|
||||
}
|
||||
|
||||
export const allHinters: Hinter[] = [
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import React, { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Button, ProgressBar, Toast } from 'react-bootstrap'
|
||||
import { UiNotification } from '../../redux/ui-notifications/types'
|
||||
import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
|
||||
|
@ -88,6 +88,17 @@ export const UiNotificationToast: React.FC<UiNotificationProps> = ({
|
|||
[buttons, dismissThisNotification]
|
||||
)
|
||||
|
||||
const contentDom = useMemo(() => {
|
||||
return content.split('\n').map((value) => {
|
||||
return (
|
||||
<Fragment>
|
||||
{value}
|
||||
<br />
|
||||
</Fragment>
|
||||
)
|
||||
})
|
||||
}, [content])
|
||||
|
||||
return (
|
||||
<Toast show={!dismissed && eta !== undefined} onClose={dismissThisNotification}>
|
||||
<Toast.Header>
|
||||
|
@ -99,7 +110,7 @@ export const UiNotificationToast: React.FC<UiNotificationProps> = ({
|
|||
</strong>
|
||||
<small>{date.toRelative({ style: 'short' })}</small>
|
||||
</Toast.Header>
|
||||
<Toast.Body>{content}</Toast.Body>
|
||||
<Toast.Body>{contentDom}</Toast.Body>
|
||||
<ProgressBar variant={'info'} now={eta} max={durationInSecond * STEPS_PER_SECOND} min={0} />
|
||||
<div>{buttonsDom}</div>
|
||||
</Toast>
|
||||
|
|
Loading…
Reference in a new issue