fix(frontend): accessibility issues and other linting problems

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson
2026-05-06 20:54:28 +02:00
committed by Philip Molares
parent 50d0585ccb
commit 8988e3868f
43 changed files with 180 additions and 267 deletions
+2 -1
View File
@@ -20,7 +20,8 @@
"jest/valid-expect": "error",
"jest/expect-expect": ["error", {
"assertFunctionNames": ["expect*", "screen.*", "test*"]
}]
}],
"nextjs/no-img-element": "off"
},
"env": {
"browser": true,
+6
View File
@@ -76,3 +76,9 @@
.overflow-y-hidden {
overflow-y: hidden !important;
}
.unstyled-button {
appearance: none;
background: transparent;
border: none;
}
@@ -38,7 +38,8 @@ const mockedCommonAppState = {
canEdit: false
}
] as NoteGroupPermissionEntryInterface[],
sharedToUsers: [] as NoteUserPermissionEntryInterface[]
sharedToUsers: [] as NoteUserPermissionEntryInterface[],
publiclyVisible: false
}
},
user: {
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Copy to clipboard button show an error text if clipboard api isn't available 1`] = `
exports[`Copy to clipboard button show an error text if clipboard api isn't available: renders copy button 1`] = `
<div>
<button
class="copy-button btn btn-dark btn-sm"
@@ -12,7 +12,7 @@ exports[`Copy to clipboard button show an error text if clipboard api isn't avai
</div>
`;
exports[`Copy to clipboard button show an error text if clipboard api isn't available 2`] = `
exports[`Copy to clipboard button show an error text if clipboard api isn't available: renders copy button with tooltip 1`] = `
<div>
<button
aria-describedby="copied_35a35a31-c259-48c4-b75a-8da99859dcdb"
@@ -25,7 +25,7 @@ exports[`Copy to clipboard button show an error text if clipboard api isn't avai
</div>
`;
exports[`Copy to clipboard button shows an error text if writing failed 1`] = `
exports[`Copy to clipboard button shows an error text if writing failed: renders copy button 1`] = `
<div>
<button
class="copy-button btn btn-dark btn-sm"
@@ -37,7 +37,7 @@ exports[`Copy to clipboard button shows an error text if writing failed 1`] = `
</div>
`;
exports[`Copy to clipboard button shows an error text if writing failed 2`] = `
exports[`Copy to clipboard button shows an error text if writing failed: renders copy button with tooltip 1`] = `
<div>
<button
aria-describedby="copied_35a35a31-c259-48c4-b75a-8da99859dcdb"
@@ -50,7 +50,7 @@ exports[`Copy to clipboard button shows an error text if writing failed 2`] = `
</div>
`;
exports[`Copy to clipboard button shows an success text if writing succeeded 1`] = `
exports[`Copy to clipboard button shows an success text if writing succeeded: renders copy button 1`] = `
<div>
<button
class="copy-button btn btn-dark btn-sm"
@@ -62,7 +62,7 @@ exports[`Copy to clipboard button shows an success text if writing succeeded 1`]
</div>
`;
exports[`Copy to clipboard button shows an success text if writing succeeded 2`] = `
exports[`Copy to clipboard button shows an success text if writing succeeded: renders copy button with tooltip 1`] = `
<div>
<button
aria-describedby="copied_35a35a31-c259-48c4-b75a-8da99859dcdb"
@@ -31,9 +31,9 @@ describe('Copy to clipboard button', () => {
jest.resetModules()
})
const testButton = async (expectSuccess: boolean) => {
const expectButtonSnapshot = async (expectSuccess: boolean) => {
const view = render(<CopyToClipboardButton content={copyContent} />)
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('renders copy button')
const button = await screen.findByTitle('renderer.highlightCode.copyCode')
act(() => {
button.click()
@@ -41,7 +41,7 @@ describe('Copy to clipboard button', () => {
const tooltip = await screen.findByRole('tooltip')
expect(tooltip).toHaveTextContent(expectSuccess ? 'copyOverlay.success' : 'copyOverlay.error')
expect(tooltip).toHaveAttribute('id', overlayId)
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('renders copy button with tooltip')
}
const mockClipboard = (copyIsSuccessful: boolean): jest.Mock => {
@@ -58,13 +58,13 @@ describe('Copy to clipboard button', () => {
it('shows an success text if writing succeeded', async () => {
const writeTextToClipboardSpy = mockClipboard(true)
await testButton(true)
await expectButtonSnapshot(true)
expect(writeTextToClipboardSpy).toHaveBeenCalledWith(copyContent)
})
it('shows an error text if writing failed', async () => {
const writeTextToClipboardSpy = mockClipboard(false)
await testButton(false)
await expectButtonSnapshot(false)
expect(writeTextToClipboardSpy).toHaveBeenCalledWith(copyContent)
})
@@ -73,6 +73,6 @@ describe('Copy to clipboard button', () => {
clipboard: undefined
})
await testButton(false)
await expectButtonSnapshot(false)
})
})
@@ -72,7 +72,7 @@ export const ProfilePictureSelectField: React.FC<ProfilePictureSelectFieldProps>
onChange={onSetProviderPicture}
/>
<Form.Check.Label>
{/* oxlint-disable-next-line @next/next/no-img-element */}
{/* oxlint-disable-next-line jsx_a11y/img-redundant-alt */}
<img src={photoUrl} alt={'Profile picture provided by the identity provider'} height={48} width={48} />
</Form.Check.Label>
</Form.Check>
@@ -84,7 +84,7 @@ export const ProfilePictureSelectField: React.FC<ProfilePictureSelectFieldProps>
onChange={onSetFallbackPicture}
/>
<Form.Check.Label>
{/* oxlint-disable-next-line @next/next/no-img-element */}
{/* oxlint-disable-next-line jsx_a11y/img-redundant-alt */}
<img alt={'Fallback profile picture'} src={fallbackUrl} height={48} width={48} />
</Form.Check.Label>
</Form.Check>
@@ -5,7 +5,7 @@
*/
import { createNumberRangeArray } from './number-range'
describe('number range', () => {
describe('createNumberRangeArray', () => {
it('creates an empty number range', () => {
expect(createNumberRangeArray(0)).toEqual([])
})
@@ -15,6 +15,6 @@ describe('number range', () => {
})
it('fails with a negative range', () => {
expect(() => createNumberRangeArray(-1)).toThrow()
expect(() => createNumberRangeArray(-1)).toThrow('Invalid array length')
})
})
@@ -1,27 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
export interface PageItemProps {
onClick: (index: number) => void
index: number
}
/**
* Renders a number and adds an onClick handler to it.
*
* @param index The number to render
* @param onClick The onClick Handler
*/
export const PagerItem: React.FC<PageItemProps> = ({ index, onClick }) => {
return (
<li className='page-item'>
<span className='page-link' role='button' onClick={() => onClick(index)}>
{index + 1}
</span>
</li>
)
}
@@ -1,89 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PagerItem } from './pager-item'
import React, { useEffect, useMemo, useState } from 'react'
import { Pagination } from 'react-bootstrap'
export interface PaginationProps {
numberOfPageButtonsToShowAfterAndBeforeCurrent: number
onPageChange: (pageIndex: number) => void
lastPageIndex: number
}
/**
* Renders a pagination menu to move back and forth between pages.
*
* @param numberOfPageButtonsToShowAfterAndBeforeCurrent The number of buttons that should be shown before and after the current button.
* @param onPageChange The callback when one of the buttons is clicked
* @param lastPageIndex The index of the last page
*/
export const PagerPagination: React.FC<PaginationProps> = ({
numberOfPageButtonsToShowAfterAndBeforeCurrent,
onPageChange,
lastPageIndex
}) => {
if (numberOfPageButtonsToShowAfterAndBeforeCurrent % 2 !== 0) {
throw new Error('number of pages to show must be even!')
}
const [pageIndex, setPageIndex] = useState(0)
const correctedPageIndex = Math.min(pageIndex, lastPageIndex)
const wantedUpperPageIndex = correctedPageIndex + numberOfPageButtonsToShowAfterAndBeforeCurrent
const wantedLowerPageIndex = correctedPageIndex - numberOfPageButtonsToShowAfterAndBeforeCurrent
useEffect(() => {
onPageChange(pageIndex)
}, [onPageChange, pageIndex])
const correctedLowerPageIndex = useMemo(
() =>
Math.min(
Math.max(Math.min(wantedLowerPageIndex, wantedLowerPageIndex + lastPageIndex - wantedUpperPageIndex), 0),
lastPageIndex
),
[wantedLowerPageIndex, lastPageIndex, wantedUpperPageIndex]
)
const correctedUpperPageIndex = useMemo(
() =>
Math.max(Math.min(Math.max(wantedUpperPageIndex, wantedUpperPageIndex - wantedLowerPageIndex), lastPageIndex), 0),
[wantedUpperPageIndex, lastPageIndex, wantedLowerPageIndex]
)
const paginationItemsBefore = useMemo(() => {
return new Array(correctedPageIndex - correctedLowerPageIndex).map((k, index) => {
const itemIndex = correctedLowerPageIndex + index
return <PagerItem key={itemIndex} index={itemIndex} onClick={setPageIndex} />
})
}, [correctedPageIndex, correctedLowerPageIndex, setPageIndex])
const paginationItemsAfter = useMemo(() => {
return new Array(correctedUpperPageIndex - correctedPageIndex).map((k, index) => {
const itemIndex = correctedPageIndex + index + 1
return <PagerItem key={itemIndex} index={itemIndex} onClick={setPageIndex} />
})
}, [correctedUpperPageIndex, correctedPageIndex, setPageIndex])
return (
<Pagination dir='ltr'>
{correctedLowerPageIndex > 0 && (
<>
<PagerItem key={0} index={0} onClick={setPageIndex} />
<Pagination.Ellipsis disabled />
</>
)}
{paginationItemsBefore}
<Pagination.Item active>{correctedPageIndex + 1}</Pagination.Item>
{paginationItemsAfter}
{correctedUpperPageIndex < lastPageIndex && (
<>
<Pagination.Ellipsis disabled />
<PagerItem key={lastPageIndex} index={lastPageIndex} onClick={setPageIndex} />
</>
)}
</Pagination>
)
}
@@ -1,44 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { PropsWithChildren } from 'react'
import React, { Fragment, useEffect, useMemo } from 'react'
export interface PagerPageProps {
pageIndex: number
numberOfElementsPerPage: number
onLastPageIndexChange: (lastPageIndex: number) => void
}
/**
* Renders a limited number of the given children.
*
* @param children The children to render
* @param numberOfElementsPerPage The number of elements per page
* @param pageIndex Which page of the children to render
* @param onLastPageIndexChange A callback to notify about changes to the maximal page number
*/
export const Pager: React.FC<PropsWithChildren<PagerPageProps>> = ({
children,
numberOfElementsPerPage,
pageIndex,
onLastPageIndexChange
}) => {
const maxPageIndex = Math.ceil(React.Children.count(children) / numberOfElementsPerPage) - 1
const correctedPageIndex = Math.min(maxPageIndex, Math.max(0, pageIndex))
useEffect(() => {
onLastPageIndexChange(maxPageIndex)
}, [children, maxPageIndex, numberOfElementsPerPage, onLastPageIndexChange])
const filteredChildren = useMemo(() => {
return React.Children.toArray(children).filter((value, index) => {
const pageOfElement = Math.floor(index / numberOfElementsPerPage)
return pageOfElement === correctedPageIndex
})
}, [children, numberOfElementsPerPage, correctedPageIndex])
return <Fragment>{filteredChildren}</Fragment>
}
@@ -165,7 +165,11 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ scrollState, onScroll, o
}, [dispatchUiNotification, userMayEdit])
return (
// The pane has event handlers for tracking the active scroll source. It is not directly interactive.
// oxlint-disable-next-line jsx_a11y/no-static-element-interactions
<div
role={'region'}
aria-label={'Editor pane'}
className={`d-flex flex-column h-100 position-relative`}
onTouchStart={onMakeScrollSource}
onMouseEnter={onMakeScrollSource}
@@ -5,9 +5,9 @@
*/
import { mockI18n } from '../../../../test-utils/mock-i18n'
import { FrontmatterLinter } from './frontmatter-linter'
import { mockEditorView } from './single-line-regex-linter.spec'
import type { Diagnostic } from '@codemirror/lint'
import { t } from 'i18next'
import { mockEditorView } from './mock-editor-view'
const testFrontmatterLinter = (
editorContent: string,
@@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { EditorState, Text } from '@codemirror/state'
import type { EditorView } from '@codemirror/view'
import { Mock } from 'ts-mockery'
/**
* Mocks a CodeMirror editor view instance.
* @param content The mocked content of the editor view
* @returns A mocked editor view instance
*/
export const mockEditorView = (content: string): EditorView => {
const docMock = Mock.of<Text>()
docMock.toString = () => content
return Mock.of<EditorView>({
state: Mock.of<EditorState>({
doc: docMock
}),
dispatch: jest.fn()
})
}
@@ -6,22 +6,16 @@
import { mockI18n } from '../../../../test-utils/mock-i18n'
import { SingleLineRegexLinter } from './single-line-regex-linter'
import type { Diagnostic } from '@codemirror/lint'
import type { EditorState, Text } from '@codemirror/state'
import type { EditorView } from '@codemirror/view'
import { Mock } from 'ts-mockery'
import { mockEditorView } from './mock-editor-view'
export const mockEditorView = (content: string): EditorView => {
const docMock = Mock.of<Text>()
docMock.toString = () => content
return Mock.of<EditorView>({
state: Mock.of<EditorState>({
doc: docMock
}),
dispatch: jest.fn()
})
}
const testSingleLineRegexLinter = (
/**
* Expects that the linter returns the expected diagnostics for the given regex and content.
* @param regex The regex that should be used to find the expected diagnostics
* @param replace The function that should be used to replace the matched text
* @param content The content that should be used to find the expected diagnostics
* @param expectedDiagnostics Array of expected diagnostics that should be returned by the linter
*/
const expectSingleLineRegexLinterResult = (
regex: RegExp,
replace: (match: string) => string,
content: string,
@@ -48,7 +42,7 @@ describe('SingleLineRegexLinter', () => {
await mockI18n()
})
it('works for a simple regex', () => {
testSingleLineRegexLinter(/^foo$/, () => 'bar', 'This\nis\na\ntest\nfoo\nbar\n123', [
expectSingleLineRegexLinterResult(/^foo$/, () => 'bar', 'This\nis\na\ntest\nfoo\nbar\n123', [
{
from: 15,
to: 18
@@ -56,7 +50,7 @@ describe('SingleLineRegexLinter', () => {
])
})
it('works for a multiple hits', () => {
testSingleLineRegexLinter(/^foo$/, () => 'bar', 'This\nfoo\na\ntest\nfoo\nbar\n123', [
expectSingleLineRegexLinterResult(/^foo$/, () => 'bar', 'This\nfoo\na\ntest\nfoo\nbar\n123', [
{
from: 5,
to: 8
@@ -68,6 +62,6 @@ describe('SingleLineRegexLinter', () => {
])
})
it('work if there are no hits', () => {
testSingleLineRegexLinter(/^nothing$/, () => 'bar', 'This\nfoo\na\ntest\nfoo\nbar\n123', [])
expectSingleLineRegexLinterResult(/^nothing$/, () => 'bar', 'This\nfoo\na\ntest\nfoo\nbar\n123', [])
})
})
@@ -54,7 +54,7 @@ export const TableSizePickerPopover = React.forwardRef<HTMLDivElement, TableSize
createNumberRangeArray(10).map((col: number) => {
const selected = tableSize && row < tableSize.rows && col < tableSize.columns
return (
<div
<button
key={`${row}_${col}`}
className={concatCssClasses(styles.cell, { [styles.selected]: selected })}
{...cypressAttribute('selected', selected ? 'true' : 'false')}
@@ -17,15 +17,12 @@ jest.mock('../../../../../../redux/note-details/methods')
jest.mock('../../../../../notifications/ui-notification-boundary')
jest.mock('../../../../../../hooks/common/use-application-state')
const deletePromise = Promise.resolve()
const markAsPrimaryPromise = Promise.resolve({ name: 'mock-alias', isPrimaryAlias: true })
describe('AliasesListEntry', () => {
beforeEach(async () => {
await mockI18n()
mockUiNotifications()
jest.spyOn(AliasModule, 'deleteAlias').mockImplementation(() => deletePromise)
jest.spyOn(AliasModule, 'markAliasAsPrimary').mockImplementation(() => markAsPrimaryPromise)
jest.spyOn(AliasModule, 'deleteAlias').mockImplementation(() => Promise.resolve())
jest.spyOn(AliasModule, 'markAliasAsPrimary').mockImplementation(() => Promise.resolve())
jest.spyOn(NoteDetailsReduxModule, 'updateMetadata').mockImplementation(() => Promise.resolve())
})
@@ -67,7 +64,6 @@ describe('AliasesListEntry', () => {
buttonRemove.click()
})
expect(AliasModule.deleteAlias).toBeCalledWith('mock-alias-other')
await deletePromise
expect(NoteDetailsReduxModule.updateMetadata).toBeCalled()
})
@@ -82,7 +78,6 @@ describe('AliasesListEntry', () => {
buttonMakePrimary.click()
})
expect(AliasModule.markAliasAsPrimary).toBeCalledWith('mock-alias-other')
await markAsPrimaryPromise
expect(NoteDetailsReduxModule.updateMetadata).toBeCalled()
})
@@ -5,7 +5,6 @@
*/
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
import { useIsNotePinned } from '../../../../../hooks/common/use-is-note-pinned'
import { concatCssClasses } from '../../../../../utils/concat-css-classes'
import { SidebarButton } from '../../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../../types'
import React, { useCallback, useState } from 'react'
@@ -34,7 +34,10 @@ exports[`Splitter resize can change size with mouse 1`] = `
BootstrapIconMock_ArrowLeft
</button>
<span
aria-orientation="vertical"
class="grabber"
role="separator"
tabindex="0"
>
BootstrapIconMock_ArrowLeftRight
</span>
@@ -62,7 +65,7 @@ exports[`Splitter resize can change size with mouse 1`] = `
</div>
`;
exports[`Splitter resize can change size with touch 1`] = `
exports[`Splitter resize can change size with touch: touch initial 1`] = `
<div>
<div
class="flex-fill flex-row d-flex "
@@ -96,7 +99,10 @@ exports[`Splitter resize can change size with touch 1`] = `
BootstrapIconMock_ArrowLeft
</button>
<span
aria-orientation="vertical"
class="grabber"
role="separator"
tabindex="0"
>
BootstrapIconMock_ArrowLeftRight
</span>
@@ -124,7 +130,7 @@ exports[`Splitter resize can change size with touch 1`] = `
</div>
`;
exports[`Splitter resize can change size with touch 2`] = `
exports[`Splitter resize can change size with touch: touch move to left 1`] = `
<div>
<div
class="flex-fill flex-row d-flex "
@@ -158,7 +164,10 @@ exports[`Splitter resize can change size with touch 2`] = `
BootstrapIconMock_ArrowLeft
</button>
<span
aria-orientation="vertical"
class="grabber"
role="separator"
tabindex="0"
>
BootstrapIconMock_ArrowLeftRight
</span>
@@ -186,7 +195,7 @@ exports[`Splitter resize can change size with touch 2`] = `
</div>
`;
exports[`Splitter resize can change size with touch 3`] = `
exports[`Splitter resize can change size with touch: touch move to middle 1`] = `
<div>
<div
class="flex-fill flex-row d-flex "
@@ -220,7 +229,10 @@ exports[`Splitter resize can change size with touch 3`] = `
BootstrapIconMock_ArrowLeft
</button>
<span
aria-orientation="vertical"
class="grabber"
role="separator"
tabindex="0"
>
BootstrapIconMock_ArrowLeftRight
</span>
@@ -248,7 +260,7 @@ exports[`Splitter resize can change size with touch 3`] = `
</div>
`;
exports[`Splitter resize can change size with touch 4`] = `
exports[`Splitter resize can change size with touch: touch move to right 1`] = `
<div>
<div
class="flex-fill flex-row d-flex "
@@ -282,7 +294,10 @@ exports[`Splitter resize can change size with touch 4`] = `
BootstrapIconMock_ArrowLeft
</button>
<span
aria-orientation="vertical"
class="grabber"
role="separator"
tabindex="0"
>
BootstrapIconMock_ArrowLeftRight
</span>
@@ -66,7 +66,14 @@ export const SplitDivider: React.FC<SplitDividerProps> = ({
<Button variant={focusLeft ? 'secondary' : 'light'} onClick={onLeftButtonClick}>
<UiIcon icon={IconArrowLeft} />
</Button>
<span onMouseDown={onGrab} onTouchStart={onGrab} className={styles['grabber']}>
{/* oxlint-disable-next-line jsx_a11y/no-static-element-interactions */}
<span
role={'separator'}
aria-orientation={'vertical'}
tabIndex={0}
onMouseDown={onGrab}
onTouchStart={onGrab}
className={styles['grabber']}>
<UiIcon icon={IconArrowLeftRight} />
</span>
<Button variant={focusRight ? 'secondary' : 'light'} onClick={onRightButtonClick}>
@@ -59,7 +59,7 @@ describe('Splitter', () => {
it('can change size with touch', async () => {
const view = render(<Splitter left={<>left</>} right={<>right</>} />)
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('touch initial')
const divider = await screen.findByTestId('splitter-divider')
const target: EventTarget = Mock.of<EventTarget>()
const defaultTouchEvent: Omit<Touch, 'clientX'> = {
@@ -87,7 +87,7 @@ describe('Splitter', () => {
})
)
fireEvent.touchEnd(window)
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('touch move to left')
fireEvent.touchStart(divider, {})
fireEvent.touchMove(
@@ -100,7 +100,7 @@ describe('Splitter', () => {
})
)
fireEvent.touchCancel(window)
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('touch move to right')
fireEvent.touchMove(
window,
@@ -111,7 +111,7 @@ describe('Splitter', () => {
]
})
)
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('touch move to middle')
})
})
})
@@ -142,7 +142,13 @@ export const Splitter: React.FC<SplitterProps> = ({ additionalContainerClassName
resizingInProgress ? ' ' + styles.resizing : ''
}`}>
{resizingInProgress && (
// oxlint-disable-next-line jsx_a11y/no-static-element-interactions
<div
role={'separator'}
aria-orientation={'vertical'}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={adjustedRelativeSplitValue}
className={styles['move-overlay']}
onTouchMove={onMove}
onMouseMove={onMove}
@@ -13,6 +13,7 @@ import {
} from 'react-bootstrap-icons'
import styles from './caret.module.scss'
import { UiIcon } from '../../common/icons/ui-icon'
import { concatCssClasses } from '../../../utils/concat-css-classes'
interface CaretProps {
left: boolean
@@ -32,8 +33,13 @@ export const Caret: React.FC<CaretProps> = ({ active, left, onClick }) => {
const inactiveIcon = useMemo(() => (left ? IconCaretLeftEmpty : IconCaretRightEmpty), [left])
return (
<div onClick={active ? onClick : undefined} className={`${active ? styles.active : undefined}`}>
<button
onClick={active ? onClick : undefined}
disabled={!active}
className={concatCssClasses('unstyled-button', {
[styles.active]: active
})}>
<UiIcon icon={active ? activeIcon : inactiveIcon} size={2} />
</div>
</button>
)
}
@@ -33,7 +33,7 @@ export const PinnedNoteCard: React.FC<NoteExploreEntryInterface> = ({ title, las
const lastChangedString = useMemo(() => formatChangedAt(lastChangedAt), [lastChangedAt])
const onClickUnpin = useCallback(
(event: MouseEvent<HTMLDivElement>) => {
(event: MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
unpinNote(primaryAlias).catch(
showErrorNotificationBuilder('explore.pinnedNotes.unpinError', { name: primaryAlias })
@@ -46,10 +46,10 @@ export const PinnedNoteCard: React.FC<NoteExploreEntryInterface> = ({ title, las
<Card className={`${styles.card}`} as={Link} href={`/n/${primaryAlias}`}>
<Card.Body className={`${styles.cardBody}`}>
<div className={'d-flex align-items-center'}>
<div onClick={onClickUnpin} title={labelUnpinNote}>
<button type={'button'} onClick={onClickUnpin} title={labelUnpinNote} className={'unstyled-button'}>
<UiIcon icon={IconPinned} size={1.5} className={`${styles.bookmark}`} />
<div className={`${styles.star}`} />
</div>
</button>
<span className={'me-2'}>
<NoteTypeIcon noteType={type} size={3} />
</span>
@@ -8,7 +8,16 @@ exports[`motd modal doesn't render a modal if no motd has been fetched 1`] = `
</div>
`;
exports[`motd modal renders a modal if a motd was fetched and can dismiss it 1`] = `
exports[`motd modal renders a modal if a motd was fetched and can dismiss it: modal is dismissed 1`] = `
<div>
<span>
This is a mock implementation of a Modal:
Modal is invisible
</span>
</div>
`;
exports[`motd modal renders a modal if a motd was fetched and can dismiss it: renders motd modal 1`] = `
<div>
<span>
This is a mock implementation of a Modal:
@@ -38,12 +47,3 @@ exports[`motd modal renders a modal if a motd was fetched and can dismiss it 1`]
</span>
</div>
`;
exports[`motd modal renders a modal if a motd was fetched and can dismiss it 2`] = `
<div>
<span>
This is a mock implementation of a Modal:
Modal is invisible
</span>
</div>
`;
@@ -58,13 +58,13 @@ describe('motd modal', () => {
</MotdProvider>
)
await screen.findByTestId('motd-renderer')
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('renders motd modal')
const button = await screen.findByTestId('motd-dismiss')
await act<void>(() => {
button.click()
})
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('modal is dismissed')
})
it("doesn't render a modal if no motd has been fetched", async () => {
@@ -28,7 +28,7 @@ exports[`Settings On-Off Button Group accepts custom labels 1`] = `
</div>
`;
exports[`Settings On-Off Button Group can switch value 1`] = `
exports[`Settings On-Off Button Group can switch value: defaults to off 1`] = `
<div>
<div
class="btn-group"
@@ -56,7 +56,7 @@ exports[`Settings On-Off Button Group can switch value 1`] = `
</div>
`;
exports[`Settings On-Off Button Group can switch value 2`] = `
exports[`Settings On-Off Button Group can switch value: is set to on 1`] = `
<div>
<div
class="btn-group"
@@ -15,7 +15,7 @@ describe('Settings On-Off Button Group', () => {
const onSelect = (newValue: boolean) => (value = newValue)
const view = render(<OnOffButtonGroup name={'test'} value={value} onSelect={onSelect} />)
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('defaults to off')
const onButton = await screen.findByTestId('onOffButtonGroupOn')
await act<void>(() => {
onButton.click()
@@ -23,7 +23,7 @@ describe('Settings On-Off Button Group', () => {
expect(value).toBeTruthy()
view.rerender(<OnOffButtonGroup name={'test'} value={value} onSelect={onSelect} />)
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('is set to on')
const offButton = await screen.findByTestId('onOffButtonGroupOff')
await act<void>(() => {
offButton.click()
@@ -34,7 +34,7 @@ export const JumpAnchor: React.FC<JumpAnchorProps> = ({ jumpTargetId, children,
)
return (
<a {...props} onClick={jumpToTargetId}>
<a {...props} onClick={jumpToTargetId} href={`#${jumpTargetId}`}>
{children}
</a>
)
@@ -37,6 +37,7 @@ export interface ClickShieldProps extends PropsWithChildren<PropsWithDataCypress
* @param targetDescription The name of the target service
* @param hoverIcon The name of an icon that should be shown in the preview
* @param fallbackBackgroundColor A color that should be used if no background image was provided or could be loaded.
* @param fallbackLink The link that should be shown in the print preview.
* @param children The children element that should be shielded.
*/
export const ClickShield: React.FC<ClickShieldProps> = ({
@@ -130,7 +131,7 @@ export const ClickShield: React.FC<ClickShieldProps> = ({
return (
<Fragment>
<span className={containerClassName} {...cypressId(props['data-cypress-id'])}>
<span className={`d-inline-block ratio ratio-16x9 ${styles['click-shield']}`} onClick={doShowChildren}>
<button className={`d-inline-block ratio ratio-16x9 ${styles['click-shield']}`} onClick={doShowChildren}>
{previewBackground}
<span className={`${styles['preview-hover']}`}>
<span>
@@ -138,7 +139,7 @@ export const ClickShield: React.FC<ClickShieldProps> = ({
</span>
{icon}
</span>
</span>
</button>
</span>
<PrintLink link={fallbackLink} />
</Fragment>
@@ -87,7 +87,7 @@ export const UiNotificationBoundary: React.FC<PropsWithChildren> = ({ children }
(messageI18nKey: string, messageI18nOptions: Record<string, unknown> = {}, showErrorMessage = false) =>
(error: Error): void => {
log.error(t(messageI18nKey, messageI18nOptions), error)
dispatchUiNotification('common.errorOccurred', messageI18nKey, {
dispatchUiNotification('common.errorOccurred', messageI18nKey, {
contentI18nOptions: showErrorMessage
? { ...messageI18nOptions, errorMessage: error.message }
: messageI18nOptions,
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AbcFrame renders a music sheet 1`] = `
exports[`AbcFrame renders a music sheet: before rendering abcjs 1`] = `
<div>
<div
class="m-3 d-flex align-items-center justify-content-center"
@@ -10,7 +10,7 @@ exports[`AbcFrame renders a music sheet 1`] = `
</div>
`;
exports[`AbcFrame renders a music sheet 2`] = `
exports[`AbcFrame renders a music sheet: with rendered abcjs 1`] = `
<div>
<div
class="abcjs-score bg-white text-black svg-container"
@@ -2444,7 +2444,7 @@ exports[`AbcFrame renders a music sheet 2`] = `
</div>
`;
exports[`AbcFrame renders an error if abcjs file can't be loaded 1`] = `
exports[`AbcFrame renders an error if abcjs file can't be loaded: before rendering abcjs 1`] = `
<div>
<div
class="m-3 d-flex align-items-center justify-content-center"
@@ -2454,7 +2454,7 @@ exports[`AbcFrame renders an error if abcjs file can't be loaded 1`] = `
</div>
`;
exports[`AbcFrame renders an error if abcjs file can't be loaded 2`] = `
exports[`AbcFrame renders an error if abcjs file can't be loaded: with loading error message shown 1`] = `
<div>
<div
class="fade alert alert-danger show"
@@ -2465,7 +2465,7 @@ exports[`AbcFrame renders an error if abcjs file can't be loaded 2`] = `
</div>
`;
exports[`AbcFrame renders an error if abcjs render function crashes 1`] = `
exports[`AbcFrame renders an error if abcjs render function crashes: before rendering abcjs 1`] = `
<div>
<div
class="m-3 d-flex align-items-center justify-content-center"
@@ -2475,13 +2475,13 @@ exports[`AbcFrame renders an error if abcjs render function crashes 1`] = `
</div>
`;
exports[`AbcFrame renders an error if abcjs render function crashes 2`] = `
exports[`AbcFrame renders an error if abcjs render function crashes: with rendering error message shown 1`] = `
<div>
<div>
<h3>
This is a mock for ApplicationErrorAlert.
</h3>
Props:
Props:
<code>
{}
</code>
@@ -36,9 +36,9 @@ describe('AbcFrame', () => {
/>
)
const view = render(element)
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('before rendering abcjs')
expect(await screen.findByText('Sheet Music for "Speed the Plough"')).toBeInTheDocument()
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('with rendered abcjs')
})
it("renders an error if abcjs file can't be loaded", async () => {
@@ -53,9 +53,9 @@ describe('AbcFrame', () => {
/>
)
const view = render(element)
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('before rendering abcjs')
expect(await screen.findByText('common.errorWhileLoading')).toBeInTheDocument()
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('with loading error message shown')
})
it('renders an error if abcjs render function crashes', async () => {
@@ -72,8 +72,8 @@ describe('AbcFrame', () => {
/>
)
const view = render(element)
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('before rendering abcjs')
expect(await screen.findByText('editor.embeddings.abcJs.errorWhileRendering')).toBeInTheDocument()
expect(view.container).toMatchSnapshot()
expect(view.container).toMatchSnapshot('with rendering error message shown')
})
})
@@ -44,7 +44,14 @@ export const GistFrame: React.FC<IdProps> = ({ id }) => {
src={`https://gist.github.com/${id}.pibb`}
/>
<span className={`${styles['gist-resizer-row']} d-print-none`}>
<span className={styles['gist-resizer']} onMouseDown={onStart} onTouchStart={onStart} />
{/* oxlint-disable-next-line jsx_a11y/no-static-element-interactions */}
<span
role={'separator'}
aria-orientation={'horizontal'}
className={styles['gist-resizer']}
onMouseDown={onStart}
onTouchStart={onStart}
/>
</span>
</ClickShield>
)
@@ -6,7 +6,7 @@
import { replaceVimeoLinkMarkdownItPlugin } from './replace-vimeo-link'
import MarkdownIt from 'markdown-it'
describe('Replace youtube link', () => {
describe('Replace vimeo link', () => {
let markdownIt: MarkdownIt
beforeEach(() => {
@@ -22,7 +22,7 @@ describe('Replace youtube link', () => {
;['player.', ''].forEach((subdomain) => {
;['vimeo.com'].forEach((domain) => {
const origin = `${protocol}${subdomain}${domain}/`
describe(origin, () => {
describe(`Replacer for ${origin}`, () => {
const validUrl = `${origin}23237102`
it(`can detect a correct vimeo video url`, () => {
expect(markdownIt.renderInline(validUrl)).toBe("<app-vimeo id='23237102'></app-vimeo>")
@@ -22,7 +22,7 @@ describe('Replace youtube link', () => {
;['www.', ''].forEach((subdomain) => {
;['youtube.com', 'youtube-nocookie.com'].forEach((domain) => {
const origin = `${protocol}${subdomain}${domain}/`
describe(origin, () => {
describe(`Replacer for ${origin}`, () => {
const validUrl = `${origin}?v=12312312312`
it(`can detect a correct youtube video url`, () => {
expect(markdownIt.renderInline(validUrl)).toBe('<app-youtube id="12312312312"></app-youtube>')
+2 -2
View File
@@ -14,14 +14,14 @@ const handler = (req: NextApiRequest, res: NextApiResponse) => {
createdAt: '2022-03-20T20:36:32Z',
uuid: '5355ed83-7e12-4db0-95ed-837e124db08c',
fileName: 'dummy.png',
noteId: 'features'
noteAlias: 'features'
},
{
username: 'tilman',
createdAt: '2022-03-20T20:36:57+0000',
uuid: '656745ab-fbf9-47f1-a745-abfbf9a7f10c',
fileName: 'dummy2.png',
noteId: null
noteAlias: null
}
])
}
+1 -1
View File
@@ -22,7 +22,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse): Promise<void>
{
uuid: 'e81f57cd-5866-4253-9f57-cd5866a253ca',
fileName: 'avatar.png',
noteId: null,
noteAlias: null,
username: 'test',
createdAt: '2022-02-27T21:54:23.856Z'
},
@@ -40,7 +40,8 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => {
groupName: '_EVERYONE',
canEdit: false
}
]
],
publiclyVisible: true
}
},
editedByAtPosition: []
@@ -38,7 +38,8 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => {
groupName: '_LOGGED_IN',
canEdit: false
}
]
],
publiclyVisible: true
}
},
editedByAtPosition: []
@@ -47,7 +47,8 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => {
groupName: 'hedgedoc-devs',
canEdit: true
}
]
],
publiclyVisible: true
}
},
editedByAtPosition: []
@@ -16,7 +16,8 @@ describe('build state from server permissions', () => {
permissions: {
owner: null,
sharedToGroups: [],
sharedToUsers: []
sharedToUsers: [],
publiclyVisible: false
},
editedBy: [],
primaryAlias: 'test-id',
@@ -24,7 +24,8 @@ describe('build state from server permissions', () => {
groupName: 'test-group',
canEdit: false
}
]
],
publiclyVisible: false
}
expect(buildStateFromServerPermissions(state, permissions)).toStrictEqual({ ...state, permissions: permissions })
})
@@ -50,7 +50,8 @@ describe('build state from set note data from server', () => {
canEdit: true,
username: 'shareusername'
}
]
],
publiclyVisible: false
},
tags: ['tag'],
title: 'title',
@@ -118,7 +119,8 @@ describe('build state from set note data from server', () => {
canEdit: true,
username: 'shareusername'
}
]
],
publiclyVisible: false
}
}