mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-19 09:45:37 -04: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
11
.github/workflows/e2e.yml
vendored
11
.github/workflows/e2e.yml
vendored
|
@ -18,16 +18,16 @@ jobs:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@v2
|
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
|
- name: Cache build
|
||||||
uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: build
|
path: build
|
||||||
key: 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
|
- name: Cache node_modules
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
id: yarn-cache
|
id: yarn-cache
|
||||||
|
@ -57,6 +57,9 @@ jobs:
|
||||||
name: Perform E2E Test in ${{ matrix.browser }}
|
name: Perform E2E Test in ${{ matrix.browser }}
|
||||||
needs: build-frontend
|
needs: build-frontend
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: cypress/browsers:node14.16.0-chrome90-ff88
|
||||||
|
options: --user 1001 --shm-size=2g
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
browser: [ 'chrome', 'firefox' ]
|
browser: [ 'chrome', 'firefox' ]
|
||||||
|
|
|
@ -461,6 +461,7 @@
|
||||||
"successfullyCopied": "Copied!",
|
"successfullyCopied": "Copied!",
|
||||||
"copyError": "Error while copying!",
|
"copyError": "Error while copying!",
|
||||||
"errorOccurred": "An error occurred",
|
"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"
|
"readForMoreInfo": "Read here for more information"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
|
|
|
@ -5,45 +5,111 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
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|-|_|\+)*)$/
|
const wordRegExp = /^```((\w|-|_|\+)*)$/
|
||||||
let allSupportedLanguages: string[] = []
|
let allSupportedLanguages: string[] = []
|
||||||
|
|
||||||
const codeBlockHint = (editor: Editor): Promise<Hints | null> => {
|
/**
|
||||||
return import(/* webpackChunkName: "highlight.js" */ '../../../common/hljs/hljs').then(
|
* Fetches the highlight js chunk.
|
||||||
(hljs) =>
|
* @return the retrieved highlight js api
|
||||||
new Promise((resolve) => {
|
*/
|
||||||
const searchTerm = findWordAtCursor(editor)
|
const loadHighlightJs = async (): Promise<highlightJsImport | null> => {
|
||||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
try {
|
||||||
if (searchResult === null) {
|
return await import('../../../common/hljs/hljs')
|
||||||
resolve(null)
|
} catch (error) {
|
||||||
return
|
dispatchUiNotification(
|
||||||
}
|
i18n.t('common.errorOccurred'),
|
||||||
const term = searchResult[1]
|
i18n.t('common.errorWhileLoadingLibrary', { name: 'highlight.js' }),
|
||||||
if (allSupportedLanguages.length === 0) {
|
DEFAULT_DURATION_IN_SECONDS,
|
||||||
allSupportedLanguages = hljs.default
|
'exclamation-circle'
|
||||||
.listLanguages()
|
)
|
||||||
.concat('csv', 'flow', 'html', 'js', 'markmap', 'abc', 'graphviz', 'mermaid', 'vega-lite')
|
console.error("can't load highlight js", error)
|
||||||
}
|
return null
|
||||||
const suggestions = search(term, allSupportedLanguages)
|
}
|
||||||
const cursor = editor.getCursor()
|
}
|
||||||
if (!suggestions) {
|
|
||||||
resolve(null)
|
/**
|
||||||
} else {
|
* Extracts the language from the current line in the editor.
|
||||||
resolve({
|
*
|
||||||
list: suggestions.map(
|
* @param editor The editor that contains the search time
|
||||||
(suggestion: string): Hint => ({
|
* @return null if no search term could be found or the found word and the cursor position.
|
||||||
text: '```' + suggestion + '\n\n```\n',
|
*/
|
||||||
displayText: suggestion
|
const extractSearchTerm = (
|
||||||
})
|
editor: Editor
|
||||||
),
|
): null | {
|
||||||
from: Pos(cursor.line, searchTerm.start),
|
searchTerm: string
|
||||||
to: Pos(cursor.line, searchTerm.end)
|
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 = {
|
export const CodeBlockHinter: Hinter = {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
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 wordRegExp = /^(\s{0,3})(#{1,6})$/
|
||||||
const allSupportedHeaders = ['# h1', '## h2', '### h3', '#### h4', '##### h5', '###### h6', '###### tags: `example`']
|
const allSupportedHeaders = ['# h1', '## h2', '### h3', '#### h4', '##### h5', '###### h6', '###### tags: `example`']
|
||||||
|
@ -24,7 +24,7 @@ const headerHint = (editor: Editor): Promise<Hints | null> => {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const suggestions = search(term, allSupportedHeaders)
|
const suggestions = generateHintListByPrefix(term, allSupportedHeaders)
|
||||||
const cursor = editor.getCursor()
|
const cursor = editor.getCursor()
|
||||||
if (!suggestions) {
|
if (!suggestions) {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
|
|
|
@ -46,14 +46,15 @@ export const findWordAtCursor = (editor: Editor): findWordAtCursorResponse => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const search = (term: string, list: string[]): string[] => {
|
/**
|
||||||
const suggestions: string[] = []
|
* Generates a list (with max 8 entries) of hints for the autocompletion.
|
||||||
list.forEach((item) => {
|
*
|
||||||
if (item.toLowerCase().startsWith(term.toLowerCase())) {
|
* @param prefix This is the case insensitive prefix that every hint must have
|
||||||
suggestions.push(item)
|
* @param hintCandidates The list of hint candidates
|
||||||
}
|
*/
|
||||||
})
|
export const generateHintListByPrefix = (prefix: string, hintCandidates: string[]): string[] => {
|
||||||
return suggestions.slice(0, 7)
|
const searchTerm = prefix.toLowerCase()
|
||||||
|
return hintCandidates.filter((item) => item.toLowerCase().startsWith(searchTerm)).slice(0, 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const allHinters: Hinter[] = [
|
export const allHinters: Hinter[] = [
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 { Button, ProgressBar, Toast } from 'react-bootstrap'
|
||||||
import { UiNotification } from '../../redux/ui-notifications/types'
|
import { UiNotification } from '../../redux/ui-notifications/types'
|
||||||
import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
|
import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
|
||||||
|
@ -88,6 +88,17 @@ export const UiNotificationToast: React.FC<UiNotificationProps> = ({
|
||||||
[buttons, dismissThisNotification]
|
[buttons, dismissThisNotification]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const contentDom = useMemo(() => {
|
||||||
|
return content.split('\n').map((value) => {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{value}
|
||||||
|
<br />
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, [content])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toast show={!dismissed && eta !== undefined} onClose={dismissThisNotification}>
|
<Toast show={!dismissed && eta !== undefined} onClose={dismissThisNotification}>
|
||||||
<Toast.Header>
|
<Toast.Header>
|
||||||
|
@ -99,7 +110,7 @@ export const UiNotificationToast: React.FC<UiNotificationProps> = ({
|
||||||
</strong>
|
</strong>
|
||||||
<small>{date.toRelative({ style: 'short' })}</small>
|
<small>{date.toRelative({ style: 'short' })}</small>
|
||||||
</Toast.Header>
|
</Toast.Header>
|
||||||
<Toast.Body>{content}</Toast.Body>
|
<Toast.Body>{contentDom}</Toast.Body>
|
||||||
<ProgressBar variant={'info'} now={eta} max={durationInSecond * STEPS_PER_SECOND} min={0} />
|
<ProgressBar variant={'info'} now={eta} max={durationInSecond * STEPS_PER_SECOND} min={0} />
|
||||||
<div>{buttonsDom}</div>
|
<div>{buttonsDom}</div>
|
||||||
</Toast>
|
</Toast>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue