mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-15 15:44:45 -04:00
Show warning if note is longer than configured maximum length (#534)
* Add maximum document length config option * Show remaining characters in tooltip of status-bar length-info * Remove unnecessary checkDocumentLength function * Add max-length warning * Update translation wording * Set dialog to medium size * Add coloring to status-bar length info * Improve wording in warning modal * Add cypress e2e tests I included the cypress-commands package and set the language level to ES6 to allow easier testing e.g. of element attributes. * Changed way how the modal-advice was styled and positioned * Show warning modal only on first length exceeding * Improved length tooltip by adding messages when exceeding or reaching limit
This commit is contained in:
parent
14dfb5f315
commit
79469c5ddc
14 changed files with 151 additions and 19 deletions
49
cypress/integration/maxLength.spec.ts
Normal file
49
cypress/integration/maxLength.spec.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
const tenChars: string = '0123456789'
|
||||||
|
|
||||||
|
describe('status-bar text-length info', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/n/test')
|
||||||
|
cy.get('.CodeMirror textarea')
|
||||||
|
.type('{ctrl}a', { force: true })
|
||||||
|
.type('{backspace}')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tooltip shows full remaining on empty text', () => {
|
||||||
|
cy.get('.status-bar div:nth-child(2) span:nth-child(2)')
|
||||||
|
.attribute('title')
|
||||||
|
.should('contain', ' 200 ')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('color is warning on <= 100 chars remaining', () => {
|
||||||
|
cy.get('.CodeMirror textarea')
|
||||||
|
.type(`${tenChars.repeat(10)}`)
|
||||||
|
cy.get('.status-bar div:nth-child(2) span:nth-child(2)')
|
||||||
|
.should('have.class', 'text-warning')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('color is danger on <= 0 chars remaining', () => {
|
||||||
|
cy.get('.CodeMirror textarea')
|
||||||
|
.type(`${tenChars.repeat(20)}`)
|
||||||
|
cy.get('.status-bar div:nth-child(2) span:nth-child(2)')
|
||||||
|
.should('have.class', 'text-danger')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('show warning if content length > configured max length', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/n/test')
|
||||||
|
cy.get('.CodeMirror textarea')
|
||||||
|
.type('{ctrl}a', { force: true })
|
||||||
|
.type('{backspace}')
|
||||||
|
.type(`${tenChars.repeat(20)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('show warning alert in renderer and as modal', () => {
|
||||||
|
cy.get('.CodeMirror textarea')
|
||||||
|
.type('a')
|
||||||
|
cy.get('.modal-body.limit-warning')
|
||||||
|
.should('be.visible')
|
||||||
|
cy.get('.splitter .alert-danger')
|
||||||
|
.should('be.visible')
|
||||||
|
})
|
||||||
|
})
|
|
@ -34,6 +34,7 @@ beforeEach(() => {
|
||||||
oauth2: 'Olaf2',
|
oauth2: 'Olaf2',
|
||||||
saml: 'aufSAMLn.de'
|
saml: 'aufSAMLn.de'
|
||||||
},
|
},
|
||||||
|
maxDocumentLength: 200,
|
||||||
specialLinks: {
|
specialLinks: {
|
||||||
privacy: 'https://example.com/privacy',
|
privacy: 'https://example.com/privacy',
|
||||||
termsOfUse: 'https://example.com/termsOfUse',
|
termsOfUse: 'https://example.com/termsOfUse',
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
// https://on.cypress.io/configuration
|
// https://on.cypress.io/configuration
|
||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
|
|
||||||
|
import 'cypress-commands'
|
||||||
import './checkLinks'
|
import './checkLinks'
|
||||||
import './config'
|
import './config'
|
||||||
import './login'
|
import './login'
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"baseUrl": "../node_modules",
|
"baseUrl": "../node_modules",
|
||||||
"target": "es5",
|
"target": "es6",
|
||||||
"lib": ["es5", "dom"],
|
"lib": ["es6", "dom"],
|
||||||
"types": ["cypress"]
|
"types": ["cypress-commands", "cypress"]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.ts"
|
"**/*.ts"
|
||||||
|
|
|
@ -147,6 +147,7 @@
|
||||||
"@types/redux-devtools": "3.0.47",
|
"@types/redux-devtools": "3.0.47",
|
||||||
"@types/redux-devtools-extension": "2.13.2",
|
"@types/redux-devtools-extension": "2.13.2",
|
||||||
"cypress": "5.1.0",
|
"cypress": "5.1.0",
|
||||||
|
"cypress-commands": "^1.1.0",
|
||||||
"eslint-plugin-chai-friendly": "0.6.0",
|
"eslint-plugin-chai-friendly": "0.6.0",
|
||||||
"eslint-plugin-cypress": "2.11.1",
|
"eslint-plugin-cypress": "2.11.1",
|
||||||
"http-server": "0.12.3",
|
"http-server": "0.12.3",
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
"oauth2": "Olaf2",
|
"oauth2": "Olaf2",
|
||||||
"saml": "aufSAMLn.de"
|
"saml": "aufSAMLn.de"
|
||||||
},
|
},
|
||||||
|
"maxDocumentLength": 100000,
|
||||||
"useImageProxy": false,
|
"useImageProxy": false,
|
||||||
"plantumlServer": "http://www.plantuml.com/plantuml",
|
"plantumlServer": "http://www.plantuml.com/plantuml",
|
||||||
"specialLinks": {
|
"specialLinks": {
|
||||||
|
|
|
@ -195,8 +195,8 @@
|
||||||
"description": "Sorry, only the owner can edit this note."
|
"description": "Sorry, only the owner can edit this note."
|
||||||
},
|
},
|
||||||
"limitReached": {
|
"limitReached": {
|
||||||
"title": "Reach the limit",
|
"title": "Reached the limit",
|
||||||
"description": "Sorry, you've reached the maximum length this note can be.",
|
"description": "Sorry, this note reached its maximum length. Notes can be up to {{maxLength}} characters long.",
|
||||||
"advice": "Please shorten the note."
|
"advice": "Please shorten the note."
|
||||||
},
|
},
|
||||||
"incompatible": {
|
"incompatible": {
|
||||||
|
@ -277,7 +277,11 @@
|
||||||
},
|
},
|
||||||
"lines": "{{lines}} Lines",
|
"lines": "{{lines}} Lines",
|
||||||
"length": "Length {{length}}",
|
"length": "Length {{length}}",
|
||||||
"lengthTooltip": "You can write up to 100000 characters in this document."
|
"lengthTooltip": {
|
||||||
|
"maximumReached": "You've reached the maximum length of this note.",
|
||||||
|
"remaining": "You may still write {{remaining}} characters until you reach the limit for this note.",
|
||||||
|
"exceeded": "You've exceeded the maximum length of this note by {{exceeded}} characters. Consider shortening it."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"rawHtml": "Raw HTML",
|
"rawHtml": "Raw HTML",
|
||||||
|
|
1
src/api/config/types.d.ts
vendored
1
src/api/config/types.d.ts
vendored
|
@ -9,6 +9,7 @@ export interface Config {
|
||||||
specialLinks: SpecialLinks,
|
specialLinks: SpecialLinks,
|
||||||
version: BackendVersion,
|
version: BackendVersion,
|
||||||
plantumlServer: string | null,
|
plantumlServer: string | null,
|
||||||
|
maxDocumentLength: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BrandingConfig {
|
export interface BrandingConfig {
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { Button, Modal } from 'react-bootstrap'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { CommonModal } from '../../common/modals/common-modal'
|
||||||
|
|
||||||
|
export interface MaxLengthWarningModalProps {
|
||||||
|
show: boolean
|
||||||
|
onHide: () => void
|
||||||
|
maxLength: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MaxLengthWarningModal: React.FC<MaxLengthWarningModalProps> = ({ show, onHide, maxLength }) => {
|
||||||
|
useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommonModal show={show} onHide={onHide} titleI18nKey={'editor.error.limitReached.title'} closeButton={true}>
|
||||||
|
<Modal.Body className={'limit-warning'}>
|
||||||
|
<Trans i18nKey={'editor.error.limitReached.description'} values={{ maxLength }} />
|
||||||
|
<strong className='mt-2 d-block'><Trans i18nKey={'editor.error.limitReached.advice'}/></strong>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button onClick={onHide}><Trans i18nKey={'common.close'}/></Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</CommonModal>
|
||||||
|
)
|
||||||
|
}
|
|
@ -23,6 +23,9 @@ import 'codemirror/mode/gfm/gfm'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
|
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { ApplicationState } from '../../../redux'
|
||||||
|
import { MaxLengthWarningModal } from '../editor-modals/max-length-warning-modal'
|
||||||
import { ScrollProps, ScrollState } from '../scroll/scroll-props'
|
import { ScrollProps, ScrollState } from '../scroll/scroll-props'
|
||||||
import { allHinters, findWordAtCursor } from './autocompletion'
|
import { allHinters, findWordAtCursor } from './autocompletion'
|
||||||
import './editor-pane.scss'
|
import './editor-pane.scss'
|
||||||
|
@ -52,6 +55,9 @@ const onChange = (editor: Editor) => {
|
||||||
|
|
||||||
export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentChange, content, scrollState, onScroll, onMakeScrollSource }) => {
|
export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentChange, content, scrollState, onScroll, onMakeScrollSource }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
||||||
|
const [showMaxLengthWarning, setShowMaxLengthWarning] = useState(false)
|
||||||
|
const maxLengthWarningAlreadyShown = useRef(false)
|
||||||
const [editor, setEditor] = useState<Editor>()
|
const [editor, setEditor] = useState<Editor>()
|
||||||
const [statusBarInfo, setStatusBarInfo] = useState<StatusBarInfo>(defaultState)
|
const [statusBarInfo, setStatusBarInfo] = useState<StatusBarInfo>(defaultState)
|
||||||
const [editorPreferences, setEditorPreferences] = useState<EditorConfiguration>({
|
const [editorPreferences, setEditorPreferences] = useState<EditorConfiguration>({
|
||||||
|
@ -99,15 +105,24 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
|
||||||
}, [editor, scrollState])
|
}, [editor, scrollState])
|
||||||
|
|
||||||
const onBeforeChange = useCallback((editor: Editor, data: EditorChange, value: string) => {
|
const onBeforeChange = useCallback((editor: Editor, data: EditorChange, value: string) => {
|
||||||
|
if (value.length > maxLength && !maxLengthWarningAlreadyShown.current) {
|
||||||
|
setShowMaxLengthWarning(true)
|
||||||
|
maxLengthWarningAlreadyShown.current = true
|
||||||
|
}
|
||||||
|
if (value.length <= maxLength) {
|
||||||
|
maxLengthWarningAlreadyShown.current = false
|
||||||
|
}
|
||||||
onContentChange(value)
|
onContentChange(value)
|
||||||
}, [onContentChange])
|
}, [onContentChange, maxLength, maxLengthWarningAlreadyShown])
|
||||||
const onEditorDidMount = useCallback(mountedEditor => {
|
const onEditorDidMount = useCallback(mountedEditor => {
|
||||||
setStatusBarInfo(createStatusInfo(mountedEditor))
|
setStatusBarInfo(createStatusInfo(mountedEditor, maxLength))
|
||||||
setEditor(mountedEditor)
|
setEditor(mountedEditor)
|
||||||
}, [])
|
}, [maxLength])
|
||||||
|
|
||||||
const onCursorActivity = useCallback((editorWithActivity) => {
|
const onCursorActivity = useCallback((editorWithActivity) => {
|
||||||
setStatusBarInfo(createStatusInfo(editorWithActivity))
|
setStatusBarInfo(createStatusInfo(editorWithActivity, maxLength))
|
||||||
}, [])
|
}, [maxLength])
|
||||||
|
|
||||||
const codeMirrorOptions: EditorConfiguration = useMemo<EditorConfiguration>(() => ({
|
const codeMirrorOptions: EditorConfiguration = useMemo<EditorConfiguration>(() => ({
|
||||||
...editorPreferences,
|
...editorPreferences,
|
||||||
mode: 'gfm',
|
mode: 'gfm',
|
||||||
|
@ -140,6 +155,7 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'d-flex flex-column h-100'} onMouseEnter={onMakeScrollSource}>
|
<div className={'d-flex flex-column h-100'} onMouseEnter={onMakeScrollSource}>
|
||||||
|
<MaxLengthWarningModal show={showMaxLengthWarning} onHide={() => setShowMaxLengthWarning(false)} maxLength={maxLength}/>
|
||||||
<ToolBar
|
<ToolBar
|
||||||
editor={editor}
|
editor={editor}
|
||||||
onPreferencesChange={config => setEditorPreferences(config)}
|
onPreferencesChange={config => setEditorPreferences(config)}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Editor, Position } from 'codemirror'
|
import { Editor, Position } from 'codemirror'
|
||||||
import React from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ShowIf } from '../../../common/show-if/show-if'
|
import { ShowIf } from '../../../common/show-if/show-if'
|
||||||
import './status-bar.scss'
|
import './status-bar.scss'
|
||||||
|
@ -10,6 +10,7 @@ export interface StatusBarInfo {
|
||||||
selectedLines: number
|
selectedLines: number
|
||||||
linesInDocument: number
|
linesInDocument: number
|
||||||
charactersInDocument: number
|
charactersInDocument: number
|
||||||
|
remainingCharacters: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultState: StatusBarInfo = {
|
export const defaultState: StatusBarInfo = {
|
||||||
|
@ -17,20 +18,32 @@ export const defaultState: StatusBarInfo = {
|
||||||
selectedColumns: 0,
|
selectedColumns: 0,
|
||||||
selectedLines: 0,
|
selectedLines: 0,
|
||||||
linesInDocument: 0,
|
linesInDocument: 0,
|
||||||
charactersInDocument: 0
|
charactersInDocument: 0,
|
||||||
|
remainingCharacters: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createStatusInfo = (editor: Editor): StatusBarInfo => ({
|
export const createStatusInfo = (editor: Editor, maxDocumentLength: number): StatusBarInfo => ({
|
||||||
position: editor.getCursor(),
|
position: editor.getCursor(),
|
||||||
charactersInDocument: editor.getValue().length,
|
charactersInDocument: editor.getValue().length,
|
||||||
|
remainingCharacters: maxDocumentLength - editor.getValue().length,
|
||||||
linesInDocument: editor.lineCount(),
|
linesInDocument: editor.lineCount(),
|
||||||
selectedColumns: editor.getSelection().length,
|
selectedColumns: editor.getSelection().length,
|
||||||
selectedLines: editor.getSelection().split('\n').length
|
selectedLines: editor.getSelection().split('\n').length
|
||||||
})
|
})
|
||||||
|
|
||||||
export const StatusBar: React.FC<StatusBarInfo> = ({ position, selectedColumns, selectedLines, charactersInDocument, linesInDocument }) => {
|
export const StatusBar: React.FC<StatusBarInfo> = ({ position, selectedColumns, selectedLines, charactersInDocument, linesInDocument, remainingCharacters }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const getLengthTooltip = useMemo(() => {
|
||||||
|
if (remainingCharacters === 0) {
|
||||||
|
return t('editor.statusBar.lengthTooltip.maximumReached')
|
||||||
|
}
|
||||||
|
if (remainingCharacters < 0) {
|
||||||
|
return t('editor.statusBar.lengthTooltip.exceeded', { exceeded: -remainingCharacters })
|
||||||
|
}
|
||||||
|
return t('editor.statusBar.lengthTooltip.remaining', { remaining: remainingCharacters })
|
||||||
|
}, [remainingCharacters, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="d-flex flex-row status-bar px-2">
|
<div className="d-flex flex-row status-bar px-2">
|
||||||
<div>
|
<div>
|
||||||
|
@ -46,7 +59,13 @@ export const StatusBar: React.FC<StatusBarInfo> = ({ position, selectedColumns,
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
<span>{t('editor.statusBar.lines', { lines: linesInDocument })}</span>
|
<span>{t('editor.statusBar.lines', { lines: linesInDocument })}</span>
|
||||||
<span title={t('editor.statusBar.lengthTooltip')}> – {t('editor.statusBar.length', { length: charactersInDocument })}</span>
|
–
|
||||||
|
<span
|
||||||
|
title={getLengthTooltip}
|
||||||
|
className={remainingCharacters <= 0 ? 'text-danger' : remainingCharacters <= 100 ? 'text-warning' : ''}
|
||||||
|
>
|
||||||
|
{t('editor.statusBar.length', { length: charactersInDocument })}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -127,6 +127,7 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||||
wide
|
wide
|
||||||
}) => {
|
}) => {
|
||||||
const [tocAst, setTocAst] = useState<TocAst>()
|
const [tocAst, setTocAst] = useState<TocAst>()
|
||||||
|
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
||||||
const lastTocAst = useRef<TocAst>()
|
const lastTocAst = useRef<TocAst>()
|
||||||
const [yamlError, setYamlError] = useState(false)
|
const [yamlError, setYamlError] = useState(false)
|
||||||
const rawMetaRef = useRef<RawYAMLMetadata>()
|
const rawMetaRef = useRef<RawYAMLMetadata>()
|
||||||
|
@ -364,13 +365,14 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||||
// This is used if the front-matter callback is never called, because the user deleted everything regarding metadata from the document
|
// This is used if the front-matter callback is never called, because the user deleted everything regarding metadata from the document
|
||||||
rawMetaRef.current = undefined
|
rawMetaRef.current = undefined
|
||||||
}
|
}
|
||||||
const html: string = markdownIt.render(content)
|
const trimmedContent = content.substr(0, maxLength)
|
||||||
const contentLines = content.split('\n')
|
const html: string = markdownIt.render(trimmedContent)
|
||||||
|
const contentLines = trimmedContent.split('\n')
|
||||||
const { lines: newLines, lastUsedLineId: newLastUsedLineId } = calculateNewLineNumberMapping(contentLines, oldMarkdownLineKeys.current ?? [], lastUsedLineId.current)
|
const { lines: newLines, lastUsedLineId: newLastUsedLineId } = calculateNewLineNumberMapping(contentLines, oldMarkdownLineKeys.current ?? [], lastUsedLineId.current)
|
||||||
oldMarkdownLineKeys.current = newLines
|
oldMarkdownLineKeys.current = newLines
|
||||||
lastUsedLineId.current = newLastUsedLineId
|
lastUsedLineId.current = newLastUsedLineId
|
||||||
return ReactHtmlParser(html, { transform: buildTransformer(newLines, allReplacers) })
|
return ReactHtmlParser(html, { transform: buildTransformer(newLines, allReplacers) })
|
||||||
}, [content, markdownIt, onMetaDataChange, onTaskCheckedChange])
|
}, [content, markdownIt, onMetaDataChange, onTaskCheckedChange, maxLength])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'bg-light flex-fill'}>
|
<div className={'bg-light flex-fill'}>
|
||||||
|
@ -382,6 +384,11 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||||
</Trans>
|
</Trans>
|
||||||
</Alert>
|
</Alert>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
|
<ShowIf condition={content.length > maxLength}>
|
||||||
|
<Alert variant='danger' dir={'auto'}>
|
||||||
|
<Trans i18nKey={'editor.error.limitReached.description'} values={{ maxLength }}/>
|
||||||
|
</Alert>
|
||||||
|
</ShowIf>
|
||||||
<div ref={documentElement} className={'markdown-body w-100 d-flex flex-column align-items-center'}>
|
<div ref={documentElement} className={'markdown-body w-100 d-flex flex-column align-items-center'}>
|
||||||
{markdownReactDom}
|
{markdownReactDom}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -31,6 +31,7 @@ export const initialState: Config = {
|
||||||
oauth2: '',
|
oauth2: '',
|
||||||
saml: ''
|
saml: ''
|
||||||
},
|
},
|
||||||
|
maxDocumentLength: 0,
|
||||||
useImageProxy: false,
|
useImageProxy: false,
|
||||||
plantumlServer: null,
|
plantumlServer: null,
|
||||||
specialLinks: {
|
specialLinks: {
|
||||||
|
|
|
@ -4344,6 +4344,11 @@ cyclist@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
|
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
|
||||||
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
|
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
|
||||||
|
|
||||||
|
cypress-commands@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cypress-commands/-/cypress-commands-1.1.0.tgz#9248190168783deb8ab27ae7c722e3e01d172c97"
|
||||||
|
integrity sha512-Q8Jr25pHJQFXwln6Hp8O+Hgs8Z506Y2wA9F1Te2cTajjc5L9gtt9WPOcw1Ogh+OgyqaMHF+uq31vdfImRTio5Q==
|
||||||
|
|
||||||
cypress@5.1.0:
|
cypress@5.1.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.1.0.tgz#979e9ff3e0acd792eefd365bf104046479a9643b"
|
resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.1.0.tgz#979e9ff3e0acd792eefd365bf104046479a9643b"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue