Skip to content

Commit

Permalink
Add custom Id tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ejimsan committed Jan 7, 2025
1 parent 5a25cc0 commit cce4a85
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 93 deletions.
66 changes: 33 additions & 33 deletions src/bibliography/domain/BibliographyEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,88 +28,88 @@ export default class BibliographyEntry {
constructor(cslData?: CslData | null | undefined) {
this.cslData = cslData
? produce(cslData, (draft) => {
_.keys(draft)
// Remove properties starting with '_'
Object.keys(draft)
.filter((key) => key.startsWith('_'))
.forEach(_.partial(_.unset, draft))
.forEach((key) => delete draft[key])

// Normalize author data
if (draft.author) {
draft.author = draft.author.map(
_.partialRight(_.pick, authorProperties)
draft.author = draft.author.map((author) =>
_.pick(author, authorProperties)
)
}
})
: {}
}

get id(): string {
return _.get(this.cslData, 'id', '')
return this.cslData.id || ''
}

get primaryAuthor(): string {
return _.head(this.authors) || ''
return this.authors[0] || ''
}

get authors(): string[] {
return _.get(this.cslData, 'author', []).map(getName)
return (this.cslData.author || []).map(getName)
}

get year(): string {
const start = _.get(this.cslData, 'issued.date-parts.0.0', '')
const end = _.get(this.cslData, 'issued.date-parts.1.0', '')
const dates = _.get(this.cslData, 'issued.date-parts', [])
const start = dates[0]?.[0] || ''
const end = dates[1]?.[0] || ''
return end ? `${start}${end}` : String(start)
}

get title(): string {
return _.get(this.cslData, 'title', '')
return this.cslData.title || ''
}

get shortContainerTitle(): string {
return _.get(this.cslData, 'container-title-short', '')
return this.cslData['container-title-short'] || ''
}

get shortTitle(): string {
return _.get(this.cslData, 'title-short', '')
return this.cslData['title-short'] || ''
}

get collectionNumber(): string {
return _.get(this.cslData, 'collection-number', '')
return this.cslData['collection-number'] || ''
}

get volume(): string {
return _.get(this.cslData, 'volume', '')
return this.cslData.volume || ''
}

get link(): string {
const url = _.get(this.cslData, 'URL', '')
const doi = _.get(this.cslData, 'DOI', '')
return url || (doi ? `https://doi.org/${doi}` : '')
return (
this.cslData.URL ||
(this.cslData.DOI ? `https://doi.org/${this.cslData.DOI}` : '')
)
}

get authorYearTitle(): string {
return `${this.primaryAuthor} ${this.year} ${this.title}`
}

get abberviationContainer(): string | undefined {
const containerTitleShort = this.shortContainerTitle
const collectionNumber = this.collectionNumber
? ` ${this.collectionNumber}`
: ''
return containerTitleShort
? `${containerTitleShort}${collectionNumber}`
: undefined
get abbreviationContainer(): string | undefined {
const container = this.shortContainerTitle
const number = this.collectionNumber ? ` ${this.collectionNumber}` : ''
return container ? `${container}${number}` : undefined
}

get abbreviationTitle(): string | undefined {
const { shortTitle } = this
const volume = this.volume ? ` ${this.volume}` : ''
return shortTitle ? `${shortTitle}${volume}` : undefined
const title = this.shortTitle
const vol = this.volume ? ` ${this.volume}` : ''
return title ? `${title}${vol}` : undefined
}

get abbreviations(): string | undefined {
const { abberviationContainer, abbreviationTitle } = this
if (abberviationContainer && abbreviationTitle) {
return `${abberviationContainer} = ${abbreviationTitle}`
}
return abberviationContainer ?? abbreviationTitle ?? undefined
const container = this.abbreviationContainer
const title = this.abbreviationTitle
if (container && title) return `${container} = ${title}`
return container || title
}

get label(): string {
Expand Down
90 changes: 72 additions & 18 deletions src/bibliography/ui/BibliographyEntryForm.test.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,88 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import _ from 'lodash'

import { render, screen, waitFor } from '@testing-library/react'
import { changeValueByLabel, clickNth } from 'test-support/utils'
import BibliographyEntryForm from './BibliographyEntryForm'
import { bibliographyEntryFactory } from 'test-support/bibliography-fixtures'
import BibliographyEntry from 'bibliography/domain/BibliographyEntry'

let json: string
let entry: BibliographyEntry
let onSubmit: () => void
let mockJson: string
let mockEntry: BibliographyEntry
let onSubmitMock: jest.Mock

beforeEach(() => {
entry = bibliographyEntryFactory.build()
json = JSON.stringify(entry.toCslData(), null, 2)
onSubmit = jest.fn()
mockEntry = bibliographyEntryFactory.build()
mockJson = JSON.stringify(mockEntry.toCslData(), null, 2)
onSubmitMock = jest.fn()
})

test('Form updates and submits entry with correct data', async () => {
render(<BibliographyEntryForm onSubmit={onSubmitMock} />)
changeValueByLabel(screen, 'Data', mockJson)
await screen.findByText(new RegExp(`\\(${mockEntry.year}\\)`))
clickNth(screen, 'Save', 0)

expect(onSubmitMock).toHaveBeenCalledWith(mockEntry)
})

test(`Changing document calls onChange with updated value.`, async () => {
render(<BibliographyEntryForm onSubmit={onSubmit} />)
changeValueByLabel(screen, 'Data', json)
await screen.findByText(new RegExp(_.escapeRegExp(`(${entry.year})`)))
test('Displays CSL-JSON input correctly', async () => {
render(<BibliographyEntryForm value={mockEntry} onSubmit={onSubmitMock} />)
const textarea = screen.getByLabelText('Data') as HTMLTextAreaElement
await waitFor(() => {
expect(textarea.value.replace(/\s/g, '')).toContain(
JSON.stringify(mockEntry.toCslData()).replace(/\s/g, '')
)
})
})

test('Applies custom ID when no ID exists', async () => {
const entryWithoutId = bibliographyEntryFactory.build({
toCslData: () => ({ ...mockEntry.toCslData(), id: undefined }),
})
const jsonWithoutId = JSON.stringify(entryWithoutId.toCslData(), null, 2)

render(<BibliographyEntryForm onSubmit={onSubmitMock} />)
changeValueByLabel(screen, 'Data', jsonWithoutId)

// Wait for debounce to complete before submitting
await waitFor(
() => expect(screen.getByRole('button', { name: /Save/i })).toBeEnabled(),
{ timeout: 1000 }
)

clickNth(screen, 'Save', 0)

expect(onSubmit).toHaveBeenCalledWith(entry)
await waitFor(() => {
expect(onSubmitMock).toHaveBeenCalled()

const submittedEntry = onSubmitMock.mock.calls[0][0]

expect(submittedEntry.id).not.toBeUndefined()
expect(submittedEntry.id).not.toMatch(/^temp_id/)
})
})

test(`Shows value as CSL-JSON.`, async () => {
render(<BibliographyEntryForm value={entry} onSubmit={onSubmit} />)
await screen.findByDisplayValue(
new RegExp(_.escapeRegExp(json).replace(/\s+/g, '\\s*'))
test('Preserves existing ID', async () => {
const entryWithValidId = bibliographyEntryFactory.build({
toCslData: () => ({ ...mockEntry.toCslData(), id: 'validId123' }),
})
const jsonWithValidId = JSON.stringify(entryWithValidId.toCslData(), null, 2)

render(<BibliographyEntryForm onSubmit={onSubmitMock} />)
changeValueByLabel(screen, 'Data', jsonWithValidId)

// Wait for debounce to complete before submitting
await waitFor(
() => expect(screen.getByRole('button', { name: /Save/i })).toBeEnabled(),
{ timeout: 1000 }
)

clickNth(screen, 'Save', 0)

await waitFor(() => {
expect(onSubmitMock).toHaveBeenCalled()

const submittedEntry = onSubmitMock.mock.calls[0][0]

expect(submittedEntry.id).toEqual('validId123')
})
})
85 changes: 43 additions & 42 deletions src/bibliography/ui/BibliographyEntryForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,19 @@ import { generateIds } from 'bibliography/domain/GenerateIds'

import './BibliographyEntryForm.css'

function BibliographyHelp() {
return (
<p>
You can enter a DOI, CSL-JSON, BibTeX, or any{' '}
<ExternalLink href="https://citation.js.org/api/tutorial-input_formats.html">
supported input format
</ExternalLink>
. BibTeX can be generated with{' '}
<ExternalLink href="https://truben.no/latex/bibtex/">
BibTeX Online Editor
</ExternalLink>
.
</p>
)
}
const BibliographyHelp = () => (
<p>
You can enter a DOI, CSL-JSON, BibTeX, or any{' '}
<ExternalLink href="https://citation.js.org/api/tutorial-input_formats.html">
supported input format
</ExternalLink>
. BibTeX can be generated with{' '}
<ExternalLink href="https://truben.no/latex/bibtex/">
BibTeX Online Editor
</ExternalLink>
.
</p>
)

interface Props {
value?: BibliographyEntry | null
Expand All @@ -46,17 +44,24 @@ interface State {
}

export default class BibliographyEntryForm extends Component<Props, State> {
static defaultProps: { value: null; disabled: false }
static defaultProps = { value: null, disabled: false }

private promise: Promise<void>
private doLoad: (value: string) => Promise<void> | undefined

constructor(props: Props) {
super(props)
this.state = props.value
this.state = this.getInitialState(props.value)
this.promise = Promise.resolve()
this.doLoad = _.debounce(this.load, 500, { leading: false, trailing: true })
}

private getInitialState(value?: BibliographyEntry | null): State {
return value
? {
citation: props.value.toHtml(),
cslData: [props.value.toCslData()],
value: JSON.stringify(props.value.toCslData(), null, 2),
citation: value.toHtml(),
cslData: [value.toCslData()],
value: JSON.stringify(value.toCslData(), null, 2),
loading: false,
customId: '',
isInvalid: false,
Expand All @@ -69,30 +74,25 @@ export default class BibliographyEntryForm extends Component<Props, State> {
customId: '',
isInvalid: false,
}
this.promise = Promise.resolve()
this.doLoad = _.debounce(this.load, 500, {
leading: false,
trailing: true,
})
}

get isValid(): boolean {
private get isValid(): boolean {
return _.isArray(this.state.cslData) && this.state.cslData.length === 1
}

get isInvalid(): boolean {
private get isInvalid(): boolean {
return (
!this.state.loading &&
!_.isEmpty(this.state.value) &&
(this.state.isInvalid || !this.isValid)
)
}

get isDisabled(): boolean {
private get isDisabled(): boolean {
return !this.isValid || this.props.disabled
}

handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
private handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({
...this.state,
value: event.target.value,
Expand All @@ -102,16 +102,11 @@ export default class BibliographyEntryForm extends Component<Props, State> {
this.promise = this.doLoad(event.target.value) || this.promise
}

load = (value: string): Promise<void> => {
private load = (value: string): Promise<void> => {
this.promise.cancel()

const handleSuccess = (cite: Cite): void => {
const cslData = cite.get({
format: 'real',
type: 'json',
style: 'csl',
})

const cslData = cite.get({ format: 'real', type: 'json', style: 'csl' })
const customId = generateIds(cslData[0])

this.setState({
Expand Down Expand Up @@ -146,26 +141,32 @@ export default class BibliographyEntryForm extends Component<Props, State> {
})
}

formatCitation = (cite: Cite): string => {
private formatCitation = (cite: Cite): string => {
return cite.format('bibliography', {
format: 'html',
template: 'citation-apa',
lang: 'de-DE',
})
}

handleSubmit = (event: React.FormEvent<HTMLElement>): void => {
private handleSubmit = (event: React.FormEvent<HTMLElement>): void => {
event.preventDefault()
if (this.state.cslData && this.state.cslData[0]) {
const entryData = {
...this.state.cslData[0],
id: this.state.customId,
} as CslData & { id: string }
const entryData = this.applyCustomIdIfNeeded(this.state.cslData[0])
const entry = new BibliographyEntry(entryData)
this.props.onSubmit(entry)
}
}

private applyCustomIdIfNeeded = (
cslData: CslData
): CslData & { id: string } => {
const id = cslData.id?.trim()
return !id || id.startsWith('temp_id')
? { ...cslData, id: this.state.customId }
: { ...cslData, id }
}

render(): JSX.Element {
const parsed = new Parser().parse(this.state.citation)
return (
Expand Down

0 comments on commit cce4a85

Please sign in to comment.