diff --git a/app/react/App/scss/elements/_pdfViewer.scss b/app/react/App/scss/elements/_pdfViewer.scss new file mode 100644 index 0000000000..e5d50dc24a --- /dev/null +++ b/app/react/App/scss/elements/_pdfViewer.scss @@ -0,0 +1,22 @@ +#pdf-container { + margin: auto; + width: fit-content; + + .pdf-page { + position: relative; + } + + & .canvasWrapper { + margin: 0; + display: block; + width: 100%; + height: 100%; + + & canvas { + margin: 0; + display: block; + width: 100%; + height: 100%; + } + } +} diff --git a/app/react/App/scss/styles.scss b/app/react/App/scss/styles.scss index 29d68d6e20..9b76d1da0b 100644 --- a/app/react/App/scss/styles.scss +++ b/app/react/App/scss/styles.scss @@ -13,6 +13,7 @@ @import 'elements/item'; @import 'elements/linkField'; @import 'elements/panel'; +@import 'elements/pdfViewer'; @import 'elements/breadcrumbs'; @import 'elements/draggable'; @import 'elements/dropdown'; diff --git a/app/react/Documents/components/ShowToc.js b/app/react/Documents/components/ShowToc.js index 2cd28e3433..65478a5393 100644 --- a/app/react/Documents/components/ShowToc.js +++ b/app/react/Documents/components/ShowToc.js @@ -2,23 +2,20 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { scrollToToc } from 'app/Viewer/actions/uiActions'; -import Immutable from 'immutable'; import ShowIf from 'app/App/ShowIf'; import { t } from 'app/I18N'; import { Icon } from 'UI'; - +import { selectionHandlers } from 'V2/Components/PDFViewer'; import './scss/showToc.scss'; -export class ShowToc extends Component { +class ShowToc extends Component { scrollTo(tocElement, e) { e.preventDefault(); - this.props.scrollToToc(tocElement.toJS()); + this.props.scrollToToc(tocElement); } render() { - const toc = Immutable.fromJS(this.props.toc); - - if (!toc.size) { + if (!this.props.toc.length) { return (
@@ -28,29 +25,34 @@ export class ShowToc extends Component { ); } + const { documentScale } = this.props; + return (
); @@ -65,10 +67,14 @@ ShowToc.propTypes = { toc: PropTypes.array, readOnly: PropTypes.bool, scrollToToc: PropTypes.func, + documentScale: PropTypes.number, }; function mapDispatchToProps() { return { scrollToToc }; } -export default connect(null, mapDispatchToProps)(ShowToc); +const mapStateToProps = store => ({ documentScale: store.documentViewer.documentScale }); + +export { ShowToc }; +export default connect(mapStateToProps, mapDispatchToProps)(ShowToc); diff --git a/app/react/Documents/components/SnippetList.js b/app/react/Documents/components/SnippetList.js index 5e9be23de3..114e3850c6 100644 --- a/app/react/Documents/components/SnippetList.js +++ b/app/react/Documents/components/SnippetList.js @@ -49,11 +49,7 @@ const DocumentContentSnippets = ({ {documentSnippets.map((snippet, index) => { const selected = snippet.get('text') === selectedSnippet.get('text') ? 'selected' : ''; const filename = snippet.get('filename'); - console.log('filename', filename); const page = snippet.get('page'); - console.log( - `${documentViewUrl}?page=${page}&searchTerm=${searchTerm || ''}${filename ? `&file=${filename}` : ''}` - ); return (
  • ({ selection: state.documentViewer.uiState .get('reference') - .get('sourceRange') as unknown as selection, + .get('sourceRange') as unknown as Selection, }); const mapDispatchToProps = (dispatch: Dispatch<{}>, ownProps: OwnPropTypes) => { @@ -60,7 +60,9 @@ const MetadataExtractorComponent = ({ 'warning' ); } + setSelection(selection); + updateField(selection.text); }; @@ -79,5 +81,5 @@ const MetadataExtractorComponent = ({ const container = connector(MetadataExtractorComponent); -export type { selection }; +export type { Selection }; export { container as MetadataExtractor }; diff --git a/app/react/Metadata/components/specs/MetadataExtractor.spec.tsx b/app/react/Metadata/components/specs/MetadataExtractor.spec.tsx index 8b6cf10836..474d0d0566 100644 --- a/app/react/Metadata/components/specs/MetadataExtractor.spec.tsx +++ b/app/react/Metadata/components/specs/MetadataExtractor.spec.tsx @@ -7,10 +7,10 @@ import { screen, act, fireEvent } from '@testing-library/react'; import { notificationActions } from 'app/Notifications'; import { defaultState, renderConnectedContainer } from 'app/utils/test/renderConnected'; import * as actions from '../../actions/metadataExtractionActions'; -import { MetadataExtractor, selection } from '../MetadataExtractor'; +import { MetadataExtractor, Selection } from '../MetadataExtractor'; describe('MetadataExtractor', () => { - let selected: selection | undefined; + let selected: Selection | undefined; beforeEach(() => { spyOn(actions, 'updateSelection').and.returnValue(() => {}); diff --git a/app/react/PDF/components/PDF.js b/app/react/PDF/components/PDF.js index a578a2a081..e2b73635aa 100644 --- a/app/react/PDF/components/PDF.js +++ b/app/react/PDF/components/PDF.js @@ -32,6 +32,7 @@ class PDF extends Component { this.pageLoading = this.pageLoading.bind(this); this.onPageVisible = this.onPageVisible.bind(this); this.onPageHidden = this.onPageHidden.bind(this); + this.containerWidth = 0; } componentDidMount() { @@ -42,6 +43,8 @@ class PDF extends Component { }); document.addEventListener('textlayerrendered', this.props.onPageLoaded, { once: true }); } + + this.containerWidth = this.props.parentRef.current?.clientWidth; } shouldComponentUpdate(nextProps, nextState) { @@ -148,21 +151,22 @@ class PDF extends Component { render() { return ( -
    { - this.pdfContainer = ref; - }} - style={this.props.style} + - { + this.pdfContainer = ref; + }} + style={this.props.style} + id="pdf-container" > {(() => { const pages = []; for (let page = 1; page <= this.state.pdf.numPages; page += 1) { pages.push( -
    +
    @@ -179,8 +184,8 @@ class PDF extends Component { } return pages; })()} - -
    +
    + ); } } @@ -207,6 +212,7 @@ PDF.propTypes = { onLoad: PropTypes.func.isRequired, style: PropTypes.object, highlightReference: PropTypes.func, + parentRef: PropTypes.object.isRequired, }; export default PDF; diff --git a/app/react/PDF/components/PDFPage.js b/app/react/PDF/components/PDFPage.js index 0b980ff9a7..466e677091 100644 --- a/app/react/PDF/components/PDFPage.js +++ b/app/react/PDF/components/PDFPage.js @@ -3,6 +3,8 @@ import React, { Component } from 'react'; import { isClient } from 'app/utils'; import { PageReferences } from 'app/Viewer/components/PageReferences'; import { PageSelections } from 'app/Viewer/components/PageSelections'; +import { calculateScaling } from 'V2/Components/PDFViewer'; +import { atomStore, pdfScaleAtom } from 'V2/atoms'; import PDFJS, { EventBus } from '../PDFJS'; class PDFPage extends Component { @@ -112,17 +114,24 @@ class PDFPage extends Component { this.props.onLoading(this.props.page); this.setState({ rendered: true }); this.props.pdf.getPage(this.props.page).then(page => { - const scale = 1; + const originalViewport = page.getViewport({ scale: 1 }); + const scale = calculateScaling( + originalViewport.width * PDFJS.PixelsPerInch.PDF_TO_CSS_UNITS, + this.props.containerWidth + ); + const defaultViewport = page.getViewport({ scale }); this.pdfPageView = new PDFJS.PDFPageView({ container: this.pageContainer, id: this.props.page, scale, - defaultViewport: page.getViewport({ scale }), + defaultViewport, textLayerMode: 1, eventBus: new EventBus(), }); + atomStore.set(pdfScaleAtom, scale); + this.pdfPageView.setPdfPage(page); this.pdfPageView .draw() @@ -144,7 +153,7 @@ class PDFPage extends Component { return (
    { this.pageContainer = ref; }} @@ -172,6 +181,7 @@ PDFPage.propTypes = { onUnload: PropTypes.func.isRequired, pdf: PropTypes.object.isRequired, highlightReference: PropTypes.func, + containerWidth: PropTypes.number.isRequired, }; export default PDFPage; diff --git a/app/react/PDF/components/specs/PDF.spec.js b/app/react/PDF/components/specs/PDF.spec.js index 3732f8226d..9a67cc4b83 100644 --- a/app/react/PDF/components/specs/PDF.spec.js +++ b/app/react/PDF/components/specs/PDF.spec.js @@ -30,6 +30,7 @@ describe('PDF', () => { file: 'file_url', filename: 'original.pdf', onLoad: jasmine.createSpy('onLoad'), + parentRef: { current: { clientWidth: 500 } }, }; }); diff --git a/app/react/PDF/components/specs/PDFPage.spec.js b/app/react/PDF/components/specs/PDFPage.spec.js index 3016114c18..9877a98c4d 100644 --- a/app/react/PDF/components/specs/PDFPage.spec.js +++ b/app/react/PDF/components/specs/PDFPage.spec.js @@ -1,6 +1,7 @@ /** * @jest-environment jsdom */ +/* eslint-disable max-statements */ import React from 'react'; import { shallow } from 'enzyme'; @@ -13,6 +14,7 @@ jest.mock('../../PDFJS', () => ({ default: { getDocument: jest.fn().mockReturnValue(Promise.resolve(pdfObject)), }, + PixelsPerInch: { PDF_TO_CSS_UNITS: 0.5 }, EventBus: function () {}, })); @@ -20,6 +22,7 @@ describe('PDFPage', () => { let component; let instance; let container; + let pdfPageWidth = 100; let props; @@ -32,8 +35,11 @@ describe('PDFPage', () => { page: 2, getViewportContainer: () => container, pdf: { - getPage: jest.fn().mockReturnValueOnce(Promise.resolve({ getViewport: jest.fn() })), + getPage: jest + .fn() + .mockReturnValueOnce(Promise.resolve({ getViewport: () => ({ width: pdfPageWidth }) })), }, + containerWidth: 100, }; }); @@ -103,13 +109,14 @@ describe('PDFPage', () => { }); describe('when its not rendered and no pdfPageView exists', () => { + const pdfPageViewPrototype = { + setPdfPage: jest.fn(), + draw: jest.fn().mockReturnValue(Promise.resolve()), + }; + it('should create pdfPageView object and render the page', done => { render(); instance.state.rendered = false; - const pdfPageViewPrototype = { - setPdfPage: jest.fn(), - draw: jest.fn().mockReturnValueOnce(Promise.resolve()), - }; PDFJS.PDFPageView = function pdfPageView() {}; PDFJS.DefaultTextLayerFactory = function pdfPageView() {}; @@ -125,6 +132,27 @@ describe('PDFPage', () => { done(); }); }); + + it('should set the scale of the pdf', done => { + render(); + instance.state.rendered = false; + + PDFJS.PDFPageView = jest.fn().mockImplementation(() => pdfPageViewPrototype); + + instance.renderPage(); + + setTimeout(() => { + expect(PDFJS.PDFPageView).toHaveBeenCalledWith( + expect.objectContaining({ + defaultViewport: { + width: 100, + }, + scale: 2, + }) + ); + done(); + }); + }); }); }); diff --git a/app/react/PDF/components/specs/__snapshots__/PDFPage.spec.js.snap b/app/react/PDF/components/specs/__snapshots__/PDFPage.spec.js.snap index ceb9475349..a865b07bc3 100644 --- a/app/react/PDF/components/specs/__snapshots__/PDFPage.spec.js.snap +++ b/app/react/PDF/components/specs/__snapshots__/PDFPage.spec.js.snap @@ -2,7 +2,7 @@ exports[`PDFPage render should pass pass proper height when state.height 1`] = `
    import(/* webpackChunkName: "LazyLoadPDFPage" */ './PDFPage')); +const eventBus = new EventBus(); + interface PDFProps { fileUrl: string; highlights?: { [page: string]: TextHighlight[] }; @@ -35,9 +38,18 @@ const PDF = ({ size, }: PDFProps) => { const scrollToRef = useRef(null); + const pdfContainerRef = useRef(null); const [pdf, setPDF] = useState(); const [error, setError] = useState(); + const containerStyles = { + height: size?.height || '100%', + width: size?.width || '100%', + overflow: size?.overflow || 'auto', + paddingLeft: '10px', + paddingRight: '10px', + }; + useEffect(() => { getPDFFile(fileUrl) .then(pdfFile => { @@ -50,25 +62,9 @@ const PDF = ({ useEffect(() => { let animationFrameId = 0; - let attempts = 0; - - const triggerScroll = () => { - if (attempts > 10) { - return; - } - - if (scrollToRef.current && scrollToRef.current.clientHeight > 0) { - scrollToRef.current.scrollIntoView({ behavior: 'instant' }); - attempts = 0; - return; - } - - attempts += 1; - animationFrameId = requestAnimationFrame(triggerScroll); - }; if (pdf && scrollToPage) { - triggerScroll(); + animationFrameId = triggerScroll(scrollToRef, animationFrameId); } return () => { @@ -82,29 +78,29 @@ const PDF = ({ return ( -
    +
    {pdf ? ( Array.from({ length: pdf.numPages }, (_, index) => index + 1).map(number => { const regionId = number.toString(); const pageHighlights = highlights ? highlights[regionId] : undefined; const shouldScrollToPage = scrollToPage === regionId; + const containerWidth = + pdfContainerRef.current?.offsetWidth && pdfContainerRef.current.offsetWidth - 20; + return (
    - +
    ); diff --git a/app/react/V2/Components/PDFViewer/PDFPage.tsx b/app/react/V2/Components/PDFViewer/PDFPage.tsx index 66b789acb3..36aa95a25c 100644 --- a/app/react/V2/Components/PDFViewer/PDFPage.tsx +++ b/app/react/V2/Components/PDFViewer/PDFPage.tsx @@ -1,98 +1,111 @@ /* eslint-disable max-statements */ import React, { useEffect, useRef, useState } from 'react'; -import { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist'; +import { PDFDocumentProxy } from 'pdfjs-dist'; import { Highlight } from '@huridocs/react-text-selection-handler'; -import { EventBus, PDFJSViewer } from './pdfjs'; +import { useAtom } from 'jotai'; +import { pdfScaleAtom } from 'V2/atoms'; +import { EventBus, PDFJSViewer, PDFJS } from './pdfjs'; import { TextHighlight } from './types'; +import { calculateScaling } from './functions/calculateScaling'; +import { adjustSelectionsToScale } from './functions/handleTextSelection'; interface PDFPageProps { pdf: PDFDocumentProxy; page: number; + eventBus: typeof EventBus.prototype; highlights?: TextHighlight[]; + containerWidth?: number; } -const PDFPage = ({ pdf, page, highlights }: PDFPageProps) => { - const pageContainerRef = useRef(null); - const [isVisible, setIsVisible] = useState(false); - const [pdfPage, setPdfPage] = useState(); +const PDFPage = ({ pdf, page, eventBus, containerWidth, highlights }: PDFPageProps) => { const [error, setError] = useState(); + const [pdfScale, setPdfScale] = useAtom(pdfScaleAtom); + const pageContainerRef = useRef(null); useEffect(() => { + const currentContainer = pageContainerRef.current; + let observer: IntersectionObserver; + pdf .getPage(page) - .then(result => setPdfPage(result)) - .catch((e: Error) => setError(e.message)); - }, [page, pdf]); - - useEffect(() => { - if (pageContainerRef.current && pdfPage) { - const currentContainer = pageContainerRef.current; - - const handleIntersection: IntersectionObserverCallback = entries => { - const [entry] = entries; - if (!isVisible) { - setIsVisible(entry.isIntersecting); + .then(pdfPage => { + if (currentContainer && pdfPage) { + const originalViewport = pdfPage.getViewport({ scale: 1 }); + const scale = calculateScaling( + originalViewport.width * PDFJS.PixelsPerInch.PDF_TO_CSS_UNITS, + containerWidth + ); + const defaultViewport = pdfPage.getViewport({ scale }); + + setPdfScale(scale); + + const pageViewer = new PDFJSViewer.PDFPageView({ + container: currentContainer, + id: page, + scale, + defaultViewport, + annotationMode: 0, + eventBus, + }); + + pageViewer.setPdfPage(pdfPage); + + const handleIntersection: IntersectionObserverCallback = entries => { + const [entry] = entries; + if (entry.isIntersecting) { + if (pageViewer.renderingState === PDFJSViewer.RenderingStates.INITIAL) { + pageViewer.draw().catch(e => { + setError(e.message); + }); + } + } else { + if (pageViewer.renderingState === PDFJSViewer.RenderingStates.INITIAL) { + pageViewer.cancelRendering(); + } + if (pageViewer.renderingState === PDFJSViewer.RenderingStates.FINISHED) { + pageViewer.destroy(); + } + } + }; + + observer = new IntersectionObserver(handleIntersection, { + root: null, + threshold: 0.1, + }); + + observer.observe(currentContainer); } - }; - - const observer = new IntersectionObserver(handleIntersection, { - root: null, - rootMargin: '100px', - threshold: 0.1, + }) + .catch((e: Error) => { + setError(e.message); }); - observer.observe(currentContainer); - - return () => observer.unobserve(currentContainer); - } - - return () => {}; - }); - - useEffect(() => { - if (pageContainerRef.current && pdfPage) { - const currentContainer = pageContainerRef.current; - const defaultViewport = pdfPage.getViewport({ scale: 1 }); - - const handlePlaceHolder = () => { - currentContainer.style.height = `${defaultViewport.height}px`; - }; - - if (isVisible) { - const pageViewer = new PDFJSViewer.PDFPageView({ - container: currentContainer, - id: page, - scale: 1, - defaultViewport, - annotationMode: 0, - eventBus: new EventBus(), - }); - - pageViewer.setPdfPage(pdfPage); - currentContainer.style.height = 'auto'; - pageViewer.draw().catch((e: Error) => setError(e.message)); - } - - if (!isVisible) { - handlePlaceHolder(); - } - } - }, [isVisible, page, pdfPage]); + return () => { + if (currentContainer) observer.unobserve(currentContainer); + }; + //pdf rendering is expensive and we want to make sure there's a single effect that runs only on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); if (error) { return
    {error}
    ; } return ( -
    - {isVisible && - highlights?.map(highlight => ( +
    + {highlights?.map(highlight => { + const scaledHightlight = { + ...highlight, + textSelection: adjustSelectionsToScale(highlight.textSelection, pdfScale), + }; + return ( - ))} + ); + })}
    ); }; diff --git a/app/react/V2/Components/PDFViewer/functions/calculateScaling.ts b/app/react/V2/Components/PDFViewer/functions/calculateScaling.ts new file mode 100644 index 0000000000..22061edc6b --- /dev/null +++ b/app/react/V2/Components/PDFViewer/functions/calculateScaling.ts @@ -0,0 +1,7 @@ +const calculateScaling = (pageWidth: number, containerWidth: number | undefined) => { + const container = containerWidth || pageWidth; + const scale = container / pageWidth; + return scale; +}; + +export { calculateScaling }; diff --git a/app/react/V2/Components/PDFViewer/functions/handleTextSelection.ts b/app/react/V2/Components/PDFViewer/functions/handleTextSelection.ts index bac1c10025..7aae61b395 100644 --- a/app/react/V2/Components/PDFViewer/functions/handleTextSelection.ts +++ b/app/react/V2/Components/PDFViewer/functions/handleTextSelection.ts @@ -184,9 +184,40 @@ const deleteFileSelection = ( return updatedSelections; }; +const adjustSelectionsToScale = ( + selection: TextSelection, + scalingFactor: number, + normalize?: boolean +): TextSelection => { + if (scalingFactor === 1 || scalingFactor === 0) { + return selection; + } + + const scaledSelection = { ...selection }; + + if (scaledSelection.selectionRectangles?.length) { + scaledSelection.selectionRectangles = selection.selectionRectangles.map(rectangle => { + const left = rectangle.left || 0; + const top = rectangle.top || 0; + const width = rectangle.width || 0; + const height = rectangle.height || 0; + return { + ...rectangle, + left: normalize ? left / scalingFactor : left * scalingFactor, + top: normalize ? top / scalingFactor : top * scalingFactor, + width: normalize ? width / scalingFactor : width * scalingFactor, + height: normalize ? height / scalingFactor : height * scalingFactor, + }; + }); + } + + return scaledSelection; +}; + export { getHighlightsFromFile, getHighlightsFromSelection, updateFileSelection, deleteFileSelection, + adjustSelectionsToScale, }; diff --git a/app/react/V2/Components/PDFViewer/functions/helpers.ts b/app/react/V2/Components/PDFViewer/functions/helpers.ts new file mode 100644 index 0000000000..e886116b9c --- /dev/null +++ b/app/react/V2/Components/PDFViewer/functions/helpers.ts @@ -0,0 +1,23 @@ +const triggerScroll = (ref: React.RefObject, frameId: number): number => { + let attempts = 0; + let id = frameId; + + const attemptScroll = () => { + if (attempts > 9) { + return; + } + + if (ref.current && ref.current.clientHeight > 0) { + ref.current.scrollIntoView({ behavior: 'instant' }); + return; + } + + attempts += 1; + id = requestAnimationFrame(attemptScroll); + }; + + attemptScroll(); + return id; +}; + +export { triggerScroll }; diff --git a/app/react/V2/Components/PDFViewer/functions/specs/handleTextSelection.spec.ts b/app/react/V2/Components/PDFViewer/functions/specs/handleTextSelection.spec.ts index 9077bd9969..8a228526d0 100644 --- a/app/react/V2/Components/PDFViewer/functions/specs/handleTextSelection.spec.ts +++ b/app/react/V2/Components/PDFViewer/functions/specs/handleTextSelection.spec.ts @@ -3,6 +3,7 @@ import { getHighlightsFromSelection, updateFileSelection, deleteFileSelection, + adjustSelectionsToScale, } from '../handleTextSelection'; import { selectionsFromFile, @@ -307,4 +308,73 @@ describe('PDF selections handlers', () => { ]); }); }); + + describe('adjust selections to pdf scale', () => { + it('should return the original selection if there is no valid scaling', () => { + expect(adjustSelectionsToScale(selections[0], 1)).toEqual(selections[0]); + expect(adjustSelectionsToScale(selections[0], 0)).toEqual(selections[0]); + }); + + it('should return the same selections if the rectangles are empty', () => { + expect( + adjustSelectionsToScale( + { + text: 'no rectangles', + selectionRectangles: [], + }, + 1.5 + ) + ).toEqual({ + text: 'no rectangles', + selectionRectangles: [], + }); + }); + + it('should scale selections', () => { + expect(adjustSelectionsToScale(selections[0], 1.5)).toEqual({ + text: 'selection 1', + selectionRectangles: [ + { + left: 1.5, + top: 1.5, + width: 1.5, + height: 1.5, + regionId: '1', + }, + ], + }); + }); + + it('should normalize selections', () => { + expect( + adjustSelectionsToScale( + { + text: 'selection in scaled pdf', + selectionRectangles: [ + { + left: 10, + top: 10, + width: 10, + height: 2, + regionId: '1', + }, + ], + }, + 2, + true + ) + ).toEqual({ + text: 'selection in scaled pdf', + selectionRectangles: [ + { + left: 5, + top: 5, + width: 5, + height: 1, + regionId: '1', + }, + ], + }); + }); + }); }); diff --git a/app/react/V2/Components/PDFViewer/functions/specs/helpers.spec.ts b/app/react/V2/Components/PDFViewer/functions/specs/helpers.spec.ts new file mode 100644 index 0000000000..7f3511427a --- /dev/null +++ b/app/react/V2/Components/PDFViewer/functions/specs/helpers.spec.ts @@ -0,0 +1,52 @@ +/** + * @jest-environment jsdom + */ +/* eslint-disable max-statements */ +import { triggerScroll } from '../helpers'; + +describe('triggerScroll', () => { + //defined as any since the correct definition of the react ref type has no impact on the test + let mockRef: any; + let requestAnimationFrameSpy: jest.SpyInstance; + + jest.useFakeTimers(); + + beforeEach(() => { + mockRef = { + current: { + clientHeight: 100, + scrollIntoView: jest.fn(), + }, + }; + + requestAnimationFrameSpy = jest + .spyOn(window, 'requestAnimationFrame') + .mockImplementation(cb => { + setTimeout(cb, 0); + return 1; + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); // Restore original implementations + }); + + it('should call scrollIntoView if clientHeight is greater than 0 and return the animation id', () => { + const frameId = triggerScroll(mockRef, 20); + expect(mockRef.current!.scrollIntoView).toHaveBeenCalledWith({ behavior: 'instant' }); + expect(frameId).toBe(20); + }); + + it('should call scrollIntoView if clientHeight changes from 0 to a positive value after a failed attempt', () => { + mockRef.current!.clientHeight = 0; + const frameId = triggerScroll(mockRef, 0); + expect(mockRef.current!.scrollIntoView).not.toHaveBeenCalledWith(); + expect(requestAnimationFrameSpy).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(0.5); + expect(requestAnimationFrameSpy).toHaveBeenCalledTimes(2); + mockRef.current!.clientHeight = 200; + jest.advanceTimersByTime(1); + expect(mockRef.current!.scrollIntoView).toHaveBeenCalledWith({ behavior: 'instant' }); + expect(frameId).toBe(1); + }); +}); diff --git a/app/react/V2/Components/PDFViewer/index.ts b/app/react/V2/Components/PDFViewer/index.ts index ccd0678c99..ced8ba4ff1 100644 --- a/app/react/V2/Components/PDFViewer/index.ts +++ b/app/react/V2/Components/PDFViewer/index.ts @@ -4,3 +4,4 @@ import * as selectionHandlers from './functions/handleTextSelection'; const PDF = loadable(async () => import(/* webpackChunkName: "LazyLoadPDF" */ './PDF')); export { PDF, selectionHandlers }; +export { calculateScaling } from './functions/calculateScaling'; diff --git a/app/react/V2/Components/PDFViewer/specs/PDF.spec.tsx b/app/react/V2/Components/PDFViewer/specs/PDF.spec.tsx new file mode 100644 index 0000000000..2e6ea167ff --- /dev/null +++ b/app/react/V2/Components/PDFViewer/specs/PDF.spec.tsx @@ -0,0 +1,174 @@ +/** + * @jest-environment jsdom + */ + +import React from 'react'; +import { render, act, queryAllByAttribute, cleanup, RenderResult } from '@testing-library/react'; +import { configMocks, mockIntersectionObserver } from 'jsdom-testing-mocks'; +import { Provider } from 'react-redux'; +import { pdfScaleAtom } from 'V2/atoms'; +import { TestAtomStoreProvider, LEGACY_createStore as createStore } from 'V2/testing'; +import PDF, { PDFProps } from '../PDF'; +import * as helpers from '../functions/helpers'; + +configMocks({ act }); +const oberserverMock = mockIntersectionObserver(); + +const highlights: PDFProps['highlights'] = { + 2: [ + { + key: '2', + textSelection: { selectionRectangles: [{ top: 20, width: 100, left: 0, height: 30 }] }, + color: 'red', + }, + ], +}; + +const mockPageRender = jest.fn(); +const mockPageDestroy = jest.fn(); +const mockPageViewer = jest.fn(); +const mockGetDocument = jest.fn(); + +const renderingStates = { + INITIAL: 0, + RUNNING: 1, + PAUSED: 2, + FINISHED: 3, +}; + +jest.mock('../pdfjs.ts', () => ({ + EventBus: jest.fn(), + PDFJS: { + getDocument: jest.fn(args => { + mockGetDocument(args); + return { + promise: Promise.resolve({ + numPages: 5, + getPage: jest.fn(async () => + Promise.resolve({ + getViewport: () => ({ width: 100, height: 300 }), + }) + ), + }), + }; + }), + PixelsPerInch: { PDF_TO_CSS_UNITS: 0.5 }, + }, + PDFJSViewer: { + PDFPageView: jest.fn().mockImplementation(args => { + mockPageViewer(args); + return { + setPdfPage: jest.fn(), + draw: jest.fn().mockImplementation(async () => { + mockPageRender(); + return Promise.resolve(); + }), + destroy: mockPageDestroy, + renderingState: 0, + cancelRendering: jest.fn(), + }; + }), + RenderingStates: renderingStates, + }, + CMAP_URL: 'legacy_character_maps', +})); + +describe('PDF', () => { + let renderResult: RenderResult; + + const renderComponet = (scrollToPage?: PDFProps['scrollToPage']) => { + renderResult = render( + + + + + + ); + }; + + beforeAll(() => { + Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { + configurable: true, + value: 100, + }); + }); + + beforeEach(() => { + jest.spyOn(helpers, 'triggerScroll'); + jest.spyOn(window, 'requestAnimationFrame'); + jest.spyOn(pdfScaleAtom, 'write'); + }); + + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + afterAll(() => { + oberserverMock.cleanup(); + }); + + it('should render the pdf file', async () => { + await act(() => { + renderComponet(); + }); + const { container } = renderResult; + const page1 = queryAllByAttribute('class', container, 'pdf-page')[0]; + await act(() => { + oberserverMock.enterNode(page1); + }); + expect(mockGetDocument).toHaveBeenCalledWith({ + cMapPacked: true, + cMapUrl: 'legacy_character_maps', + isEvalSupported: false, + url: 'url/of/file.pdf', + }); + expect(mockPageViewer).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + annotationMode: 0, + defaultViewport: { + height: 300, + width: 100, + }, + eventBus: {}, + id: 1, + scale: 1.6, + }) + ); + expect(mockPageRender).toHaveBeenCalled(); + expect(pdfScaleAtom.write).toHaveBeenCalled(); + expect(container).toMatchSnapshot(); + }); + + it('should scroll to page', async () => { + await act(() => { + renderComponet('2'); + }); + expect(helpers.triggerScroll).toHaveBeenCalledTimes(1); + }); + + describe('intersection observer', () => { + const observerMock = jest.fn(); + const unobserveMock = jest.fn(); + + beforeEach(() => { + window.IntersectionObserver = jest.fn().mockImplementation(() => ({ + observe: observerMock, + unobserve: unobserveMock, + })); + }); + + it('should set the observers on mount and clear them on unmount', async () => { + await act(() => { + renderComponet(); + }); + + expect(observerMock).toHaveBeenCalledTimes(5); + + cleanup(); + + expect(unobserveMock).toHaveBeenCalledTimes(5); + }); + }); +}); diff --git a/app/react/V2/Components/PDFViewer/specs/__snapshots__/PDF.spec.tsx.snap b/app/react/V2/Components/PDFViewer/specs/__snapshots__/PDF.spec.tsx.snap new file mode 100644 index 0000000000..fced12cb80 --- /dev/null +++ b/app/react/V2/Components/PDFViewer/specs/__snapshots__/PDF.spec.tsx.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PDF should render the pdf file 1`] = ` +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +`; diff --git a/app/react/V2/Routes/Settings/IX/components/PDFSidepanel.tsx b/app/react/V2/Routes/Settings/IX/components/PDFSidepanel.tsx index 9123e1e242..d29d27c8ec 100644 --- a/app/react/V2/Routes/Settings/IX/components/PDFSidepanel.tsx +++ b/app/react/V2/Routes/Settings/IX/components/PDFSidepanel.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { useSetAtom, useAtomValue } from 'jotai'; -import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'; +import { ChevronDownIcon, ChevronUpIcon, PlusCircleIcon } from '@heroicons/react/24/outline'; import { TextSelection } from '@huridocs/react-text-selection-handler/dist/TextSelection'; import { Translate } from 'app/I18N'; import { ClientEntitySchema, ClientPropertySchema } from 'app/istore'; @@ -15,7 +15,6 @@ import { PropertyValueSchema, MetadataObjectSchema, } from 'shared/types/commonTypes'; -import { PlusCircleIcon } from '@heroicons/react/24/outline'; import { FileType } from 'shared/types/fileType'; import * as filesAPI from 'V2/api/files'; import * as entitiesAPI from 'V2/api/entities'; @@ -23,7 +22,7 @@ import { secondsToISODate } from 'V2/shared/dateHelpers'; import { Button, Sidepanel } from 'V2/Components/UI'; import { InputField, MultiselectList, MultiselectListOption } from 'V2/Components/Forms'; import { PDF, selectionHandlers } from 'V2/Components/PDFViewer'; -import { notificationAtom, thesauriAtom } from 'V2/atoms'; +import { notificationAtom, pdfScaleAtom, thesauriAtom } from 'V2/atoms'; import { Highlights } from '../types'; const SELECT_TYPES = ['select', 'multiselect', 'relationship']; @@ -153,6 +152,7 @@ const PDFSidepanel = ({ const [selectAndSearch, setSelectAndSearch] = useState(false); const [selectAndSearchValue, setSelectAndSearchValue] = useState(); const [options, setOptions] = useState([]); + const pdfScalingValue = useAtomValue(pdfScaleAtom); useEffect(() => { if (suggestion) { @@ -324,19 +324,29 @@ const PDFSidepanel = ({ } if (selectedText) { + const normalizedSelections = selectionHandlers.adjustSelectionsToScale( + selectedText, + pdfScalingValue, + true + ); + setHighlights( - selectionHandlers.getHighlightsFromSelection(selectedText, HighlightColors.NEW) + selectionHandlers.getHighlightsFromSelection(normalizedSelections, HighlightColors.NEW) ); setSelections( selectionHandlers.updateFileSelection( { name: suggestion?.propertyName || '', id: property._id as string }, pdf?.extractedMetadata, - selectedText + normalizedSelections ) ); if (property.type === 'date' || property.type === 'numeric') { - const coercedValue = await coerceValue(property.type, selectedText.text, pdf?.language); + const coercedValue = await coerceValue( + property.type, + normalizedSelections.text, + pdf?.language + ); if (!coercedValue?.success) { setSelectionError('Value cannot be transformed to the correct type'); @@ -347,7 +357,7 @@ const PDFSidepanel = ({ setSelectionError(undefined); } } else { - setValue('field', selectedText.text, { shouldDirty: true }); + setValue('field', normalizedSelections.text, { shouldDirty: true }); } } }; @@ -457,7 +467,7 @@ const PDFSidepanel = ({ className="flex flex-col h-full gap-4 p-0" onSubmit={handleSubmit(onSubmit)} > -
    +
    {pdf && ( )} diff --git a/app/react/V2/atoms/index.ts b/app/react/V2/atoms/index.ts index c79e481091..369e099758 100644 --- a/app/react/V2/atoms/index.ts +++ b/app/react/V2/atoms/index.ts @@ -8,5 +8,6 @@ export { globalMatomoAtom } from './globalMatomoAtom'; export { ciMatomoActiveAtom } from './ciMatomoActiveAtom'; export { userAtom } from './userAtom'; export { relationshipTypesAtom } from './relationshipTypes'; +export { pdfScaleAtom } from './pdfScaleAtom'; export type { AtomStoreData } from './store'; export type { notificationAtomType } from './notificationAtom'; diff --git a/app/react/V2/atoms/pdfScaleAtom.ts b/app/react/V2/atoms/pdfScaleAtom.ts new file mode 100644 index 0000000000..bcf986cb35 --- /dev/null +++ b/app/react/V2/atoms/pdfScaleAtom.ts @@ -0,0 +1,5 @@ +import { atom } from 'jotai'; + +const pdfScaleAtom = atom(1); + +export { pdfScaleAtom }; diff --git a/app/react/V2/atoms/store.ts b/app/react/V2/atoms/store.ts index 8a155dc658..325f8bb6dc 100644 --- a/app/react/V2/atoms/store.ts +++ b/app/react/V2/atoms/store.ts @@ -11,6 +11,7 @@ import { templatesAtom } from './templatesAtom'; import { translationsAtom } from './translationsAtom'; import { userAtom } from './userAtom'; import { thesauriAtom } from './thesauriAtom'; +import { pdfScaleAtom } from './pdfScaleAtom'; type AtomStoreData = { globalMatomo?: { url: string; id: string }; @@ -60,6 +61,10 @@ if (isClient && window.__atomStoreData__) { const value = atomStore.get(thesauriAtom); store?.dispatch({ type: 'dictionaries/SET', value }); }); + atomStore.sub(pdfScaleAtom, () => { + const value = atomStore.get(pdfScaleAtom); + store?.dispatch({ type: 'viewer/documentScale/SET', value }); + }); } export type { AtomStoreData }; diff --git a/app/react/Viewer/components/Document.js b/app/react/Viewer/components/Document.js index 95222c2b1c..cf9a5a1119 100644 --- a/app/react/Viewer/components/Document.js +++ b/app/react/Viewer/components/Document.js @@ -2,12 +2,14 @@ import 'app/Viewer/scss/conversion_base.scss'; import 'app/Viewer/scss/document.scss'; import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { Component, createRef } from 'react'; import { Loader } from 'app/components/Elements/Loader'; import { PDF } from 'app/PDF'; import Immutable from 'immutable'; import { highlightSnippet } from 'app/Viewer/actions/uiActions'; +import { selectionHandlers } from 'V2/Components/PDFViewer'; +import { atomStore, pdfScaleAtom } from 'V2/atoms'; import determineDirection from '../utils/determineDirection'; @@ -27,6 +29,7 @@ class Document extends Component { this.onTextSelected = this.onTextSelected.bind(this); this.handleClick = this.handleClick.bind(this); this.highlightReference = this.highlightReference.bind(this); + this.containerRef = createRef(); } componentDidUpdate(prevProps) { @@ -43,7 +46,12 @@ class Document extends Component { const selectionRectangles = textSelection.selectionRectangles.map( ({ regionId, ...otherProps }) => ({ ...otherProps, page: regionId }) ); - this.props.setSelection({ ...textSelection, selectionRectangles }, this.props.file._id); + const highlight = selectionHandlers.adjustSelectionsToScale( + { ...textSelection, selectionRectangles }, + atomStore.get(pdfScaleAtom), + true + ); + this.props.setSelection(highlight, this.props.file._id); this.props.deactivateReference(); } @@ -87,7 +95,7 @@ class Document extends Component { handleOver() {} renderPDF(file) { - if (!file._id) { + if (!file._id && this.containerRef) { return ; } @@ -104,14 +112,15 @@ class Document extends Component { highlightReference={this.highlightReference} activeReference={this.props.activeReference} key={file.filename} + parentRef={this.containerRef} /> ); } render() { const { file } = this.props; - const Header = this.props.header; + return (
    (this.pagesContainer = ref)} + ref={this.containerRef} onMouseOver={this.handleOver.bind(this)} onClick={this.handleClick} > diff --git a/app/react/Viewer/components/PageReferences.tsx b/app/react/Viewer/components/PageReferences.tsx index e2af203b0e..613efc630f 100644 --- a/app/react/Viewer/components/PageReferences.tsx +++ b/app/react/Viewer/components/PageReferences.tsx @@ -1,11 +1,14 @@ import React, { FunctionComponent, useCallback, useRef } from 'react'; import { connect } from 'react-redux'; +import { useAtomValue } from 'jotai'; import { IStore } from 'app/istore'; import { ConnectionSchema } from 'shared/types/connectionType'; import { createSelector } from 'reselect'; import { Highlight } from '@huridocs/react-text-selection-handler'; import { unique } from 'shared/filterUnique'; import { SelectionRectangleSchema } from 'shared/types/commonTypes'; +import { pdfScaleAtom } from 'V2/atoms'; +import { selectionHandlers } from 'V2/Components/PDFViewer'; type ReferenceGroup = { _id: string; @@ -27,6 +30,7 @@ const PageReferencesComponent: FunctionComponent = ( props: PageReferencesProps ) => { const referenceGroup = useRef(); + const pdfScaleFactor = useAtomValue(pdfScaleAtom); const handleClick = useCallback( (reference: ConnectionSchema) => @@ -49,7 +53,10 @@ const PageReferencesComponent: FunctionComponent = ( ({ page, ...otherProps }) => ({ regionId: page, ...otherProps }) ); - const highlight = { ...reference.reference, selectionRectangles }; + const highlight = selectionHandlers.adjustSelectionsToScale( + { ...reference.reference, selectionRectangles }, + pdfScaleFactor + ); if (props.groupedReferences && reference._id) { props.groupedReferences.forEach(group => { diff --git a/app/react/Viewer/components/PageSelections.tsx b/app/react/Viewer/components/PageSelections.tsx index ddb24a34d4..9b79ed9924 100644 --- a/app/react/Viewer/components/PageSelections.tsx +++ b/app/react/Viewer/components/PageSelections.tsx @@ -1,9 +1,12 @@ import React from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { uniqBy } from 'lodash'; +import { useAtomValue } from 'jotai'; import { Highlight } from '@huridocs/react-text-selection-handler'; import { IStore } from 'app/istore'; import { ExtractedMetadataSchema, SelectionRectangleSchema } from 'shared/types/commonTypes'; +import { pdfScaleAtom } from 'V2/atoms'; +import { selectionHandlers } from 'app/V2/Components/PDFViewer'; interface Selection extends ExtractedMetadataSchema { isCurrent?: boolean; @@ -25,6 +28,8 @@ const connector = connect(mapStateToProps); type mappedProps = ConnectedProps; const PageSelectionsComponent = ({ userSelections, entityDocument, isEditing }: mappedProps) => { + const pdfScaleFactor = useAtomValue(pdfScaleAtom); + if (!isEditing || !entityDocument?.get('_id')) { return null; } @@ -49,10 +54,13 @@ const PageSelectionsComponent = ({ userSelections, entityDocument, isEditing }: regionId: rectangle.page, ...(rectangle as Required), })); - const highlight = { - text: selected?.text, - selectionRectangles: rectangles, - }; + const highlight = selectionHandlers.adjustSelectionsToScale( + { + text: selected?.text, + selectionRectangles: rectangles, + }, + pdfScaleFactor + ); return (
    ({ + atomStore: { get: () => 2 }, +})); + +// eslint-disable-next-line max-statements describe('Document', () => { let component; let instance; @@ -116,7 +121,7 @@ describe('Document', () => { }); describe('onTextSelected', () => { - it('should set the selection changing regionId to page', () => { + it('should set the selection changing regionId to page and adjusting based on rendering scale', () => { render(); const textSelection = { @@ -131,8 +136,8 @@ describe('Document', () => { expect(props.setSelection).toHaveBeenCalledWith( { selectionRectangles: [ - { height: 12, left: 27, page: '51', top: 186, width: 23 }, - { height: 89, left: 47, page: '52', top: 231, width: 11 }, + { height: 6, left: 13.5, page: '51', top: 93, width: 11.5 }, + { height: 44.5, left: 23.5, page: '52', top: 115.5, width: 5.5 }, ], text: 'Wham Bam Shang-A-Lang', }, diff --git a/app/react/Viewer/components/specs/PageReferences.spec.tsx b/app/react/Viewer/components/specs/PageReferences.spec.tsx index 99dd05b0ec..860c7e99d4 100644 --- a/app/react/Viewer/components/specs/PageReferences.spec.tsx +++ b/app/react/Viewer/components/specs/PageReferences.spec.tsx @@ -1,25 +1,33 @@ +/** + * @jest-environment jsdom + */ import React from 'react'; -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import Immutable from 'immutable'; import { Highlight } from '@huridocs/react-text-selection-handler'; import { Provider } from 'react-redux'; import configureStore, { MockStoreCreator } from 'redux-mock-store'; - -import { PageReferences, groupByRectangle } from '../PageReferences'; import { IStore } from 'app/istore'; +import { TestAtomStoreProvider } from 'V2/testing'; +import { pdfScaleAtom } from 'V2/atoms'; +import { PageReferences, groupByRectangle } from '../PageReferences'; const mockStoreCreator: MockStoreCreator = configureStore([]); describe('FormConfigInput', () => { let props: any; + let pdfScale: number; beforeEach(() => { props = { page: '3', onClick: jest.fn(), }; + pdfScale = 1; }); + afterEach(() => {}); + const store = mockStoreCreator({ documentViewer: { doc: Immutable.fromJS({ sharedId: 'ab42' }), @@ -39,23 +47,23 @@ describe('FormConfigInput', () => { entity: 'ab42', reference: { selectionRectangles: [ - { page: '5', width: 100 }, - { page: '5', width: 20 }, + { page: '5', width: 100, top: 1, left: 2, height: 1 }, + { page: '5', width: 20, top: 3, left: 4, height: 1 }, ], }, }, { - _id: '4', + _id: '5', entity: 'ce87', reference: { selectionRectangles: [{ page: '3', width: 101 }] }, }, { - _id: '5', + _id: '6', entity: 'df57', reference: { selectionRectangles: [{ page: '4', width: 100 }] }, }, { - _id: '6', + _id: '7', entity: 'vhsj', reference: { selectionRectangles: [{ page: '4', width: 100 }] }, }, @@ -63,15 +71,14 @@ describe('FormConfigInput', () => { }, }); - const render = () => { - return shallow( + const render = () => + shallow( ) .dive({ context: { store } }) .dive(); - }; it('should render Hihlight components with references of the page', () => { const component = render(); @@ -92,12 +99,37 @@ describe('FormConfigInput', () => { [{ _id: '1', length: 1, start: { page: '2' }, end: { page: '2' } }], [{ _id: '2', length: 1, start: { page: '3' }, end: { page: '3' } }], [{ _id: '3', length: 2, start: { page: '3' }, end: { page: '4' } }], - [{ _id: '4', length: 2, start: { page: '5', width: 100 }, end: { page: '5', width: 20 } }], - [{ _id: '4', length: 1, start: { page: '3', width: 101 }, end: { page: '3', width: 101 } }], [ - { _id: '5', length: 1, start: { page: '4', width: 100 }, end: { page: '4', width: 100 } }, + { + _id: '4', + length: 2, + start: { page: '5', width: 100, top: 1, left: 2, height: 1 }, + end: { page: '5', width: 20, top: 3, left: 4, height: 1 }, + }, + ], + [{ _id: '5', length: 1, start: { page: '3', width: 101 }, end: { page: '3', width: 101 } }], + [ { _id: '6', length: 1, start: { page: '4', width: 100 }, end: { page: '4', width: 100 } }, + { _id: '7', length: 1, start: { page: '4', width: 100 }, end: { page: '4', width: 100 } }, ], ]); }); + + it('should scale references to match the pdf', () => { + pdfScale = 0.9; + props.page = '5'; + const component = mount( + + + + + + ); + const hihglights = component.find(Highlight); + const firstHighlightProps: any = hihglights.at(0).props(); + expect(firstHighlightProps.textSelection.selectionRectangles).toEqual([ + { height: 0.9, left: 1.8, regionId: '5', top: 0.9, width: 90 }, + { height: 0.9, left: 3.6, regionId: '5', top: 2.7, width: 18 }, + ]); + }); }); diff --git a/app/react/Viewer/components/specs/PageSelections.spec.tsx b/app/react/Viewer/components/specs/PageSelections.spec.tsx index f3a40a7a8f..829ea05653 100644 --- a/app/react/Viewer/components/specs/PageSelections.spec.tsx +++ b/app/react/Viewer/components/specs/PageSelections.spec.tsx @@ -7,6 +7,8 @@ import { RenderResult } from '@testing-library/react'; import { ExtractedMetadataSchema } from 'shared/types/commonTypes'; import { defaultState, renderConnectedContainer } from 'app/utils/test/renderConnected'; import { ClientEntitySchema, ClientFile } from 'app/istore'; +import { TestAtomStoreProvider } from 'V2/testing'; +import { pdfScaleAtom } from 'V2/atoms'; import { PageSelections } from '../PageSelections'; const defaultEntityDocument: ClientFile = { @@ -61,6 +63,7 @@ describe('Page selections highlights', () => { }; let file: any | ClientFile; let selections: ExtractedMetadataSchema[]; + let pdfScalingValue = 1; beforeEach(() => { file = defaultEntityDocument; @@ -82,7 +85,12 @@ describe('Page selections highlights', () => { }), }, }; - ({ renderResult } = renderConnectedContainer(, () => state)); + ({ renderResult } = renderConnectedContainer( + + + , + () => state + )); }; it('should only render when editing the entity and has a document', () => { @@ -101,13 +109,22 @@ describe('Page selections highlights', () => { expect(renderResult.container.children.length).toBe(2); }); + it('should adjust selections by the pdf scaling factor', () => { + pdfScalingValue = 1.5; + render(); + expect(renderResult.baseElement).toMatchSnapshot(); + }); + it('should highligh new selections', () => { selections = [ { propertyID: '4356fdsassda', name: 'property_name', timestamp: 'today', - selection: { text: 'new selected text', selectionRectangles: [{ top: 10, page: '1' }] }, + selection: { + text: 'new selected text', + selectionRectangles: [{ top: 10, left: 1, width: 20, height: 1, page: '1' }], + }, }, ]; render(); @@ -121,7 +138,7 @@ describe('Page selections highlights', () => { timestamp: 'newTitle', selection: { text: 'new selected text to replace current title', - selectionRectangles: [{ top: 10, page: '1' }], + selectionRectangles: [{ top: 10, left: 1, width: 20, height: 1, page: '1' }], }, }, ]; @@ -139,7 +156,7 @@ describe('Page selections highlights', () => { timestamp: 'newProperty', selection: { text: 'new selected text to replace current selected text for the property', - selectionRectangles: [{ top: 10, page: '1' }], + selectionRectangles: [{ top: 10, left: 1, width: 20, height: 1, page: '1' }], }, }, ]; diff --git a/app/react/Viewer/components/specs/__snapshots__/PageSelections.spec.tsx.snap b/app/react/Viewer/components/specs/__snapshots__/PageSelections.spec.tsx.snap new file mode 100644 index 0000000000..9d447fc733 --- /dev/null +++ b/app/react/Viewer/components/specs/__snapshots__/PageSelections.spec.tsx.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page selections highlights should adjust selections by the pdf scaling factor 1`] = ` + +
    +
    +
    +
    +
    +
    +
    +
    + +`; diff --git a/app/react/Viewer/reducers/reducer.js b/app/react/Viewer/reducers/reducer.js index 585cbc6a03..5f70a75442 100644 --- a/app/react/Viewer/reducers/reducer.js +++ b/app/react/Viewer/reducers/reducer.js @@ -31,4 +31,5 @@ export default combineReducers({ }), tab: createReducer('viewer.sidepanel.tab', ''), }), + documentScale: createReducer('viewer/documentScale', 1), }); diff --git a/app/react/Viewer/scss/document.scss b/app/react/Viewer/scss/document.scss index 218b0419da..7a9dc6c069 100644 --- a/app/react/Viewer/scss/document.scss +++ b/app/react/Viewer/scss/document.scss @@ -144,16 +144,6 @@ background: transparent; } -.doc-page { - width: fit-content; - position: relative; -} - -.page-wrapper { - margin: 0 auto 30px !important; - width: fit-content; -} - .page { position: relative; background: $c-white; diff --git a/app/react/istore.d.ts b/app/react/istore.d.ts index 90f34fdb2e..14e1a391d6 100644 --- a/app/react/istore.d.ts +++ b/app/react/istore.d.ts @@ -178,6 +178,7 @@ export interface IStore { sidepanel: { metadata: ClientEntitySchema; }; + documentScale: number; }; oneUpReview: { state?: IImmutable; diff --git a/app/shared/types/commonSchemas.ts b/app/shared/types/commonSchemas.ts index 05203af95c..6155e8ff1e 100644 --- a/app/shared/types/commonSchemas.ts +++ b/app/shared/types/commonSchemas.ts @@ -356,6 +356,7 @@ export const selectionRectangleSchema = { height: { type: 'number' }, page: { type: 'string' }, }, + required: ['top', 'left', 'width', 'height'], }; export const selectionRectanglesSchema = { diff --git a/app/shared/types/commonTypes.d.ts b/app/shared/types/commonTypes.d.ts index 6d220f3b97..0d60143bee 100644 --- a/app/shared/types/commonTypes.d.ts +++ b/app/shared/types/commonTypes.d.ts @@ -215,10 +215,10 @@ export interface ExtractedMetadataSchema { selection?: { text?: string; selectionRectangles?: { - top?: number; - left?: number; - width?: number; - height?: number; + top: number; + left: number; + width: number; + height: number; page?: string; }[]; }; @@ -358,27 +358,27 @@ export interface SelectParentSchema { } export interface SelectionRectangleSchema { - top?: number; - left?: number; - width?: number; - height?: number; + top: number; + left: number; + width: number; + height: number; page?: string; } export type SelectionRectanglesSchema = { - top?: number; - left?: number; - width?: number; - height?: number; + top: number; + left: number; + width: number; + height: number; page?: string; }[]; export interface TocSchema { selectionRectangles?: { - top?: number; - left?: number; - width?: number; - height?: number; + top: number; + left: number; + width: number; + height: number; page?: string; }[]; label?: string; diff --git a/app/shared/types/suggestionType.d.ts b/app/shared/types/suggestionType.d.ts index be4626461a..d928a56b7c 100644 --- a/app/shared/types/suggestionType.d.ts +++ b/app/shared/types/suggestionType.d.ts @@ -27,10 +27,10 @@ export interface EntitySuggestionType { currentValue?: PropertyValueSchema | PropertyValueSchema[]; labeledValue?: PropertyValueSchema; selectionRectangles?: { - top?: number; - left?: number; - width?: number; - height?: number; + top: number; + left: number; + width: number; + height: number; page?: string; }[]; segment: string; @@ -72,10 +72,10 @@ export interface IXSuggestionType { date?: number; error?: string; selectionRectangles?: { - top?: number; - left?: number; - width?: number; - height?: number; + top: number; + left: number; + width: number; + height: number; page?: string; }[]; } diff --git a/cypress/e2e/__image_snapshots__/Entities supporting files and main documents Entity with main documents should create a reference from main document #0.png b/cypress/e2e/__image_snapshots__/Entities supporting files and main documents Entity with main documents should create a reference from main document #0.png index fe07354699..45eb71cb98 100644 Binary files a/cypress/e2e/__image_snapshots__/Entities supporting files and main documents Entity with main documents should create a reference from main document #0.png and b/cypress/e2e/__image_snapshots__/Entities supporting files and main documents Entity with main documents should create a reference from main document #0.png differ diff --git a/cypress/e2e/__image_snapshots__/Entities supporting files and main documents Entity with main documents should create a reference from main document #1.png b/cypress/e2e/__image_snapshots__/Entities supporting files and main documents Entity with main documents should create a reference from main document #1.png deleted file mode 100644 index 056d634229..0000000000 Binary files a/cypress/e2e/__image_snapshots__/Entities supporting files and main documents Entity with main documents should create a reference from main document #1.png and /dev/null differ diff --git a/cypress/e2e/__image_snapshots__/PDF display IX sidepanel pdf on the sidepanel should check that the pdf renders and scrolls to the selection #0.png b/cypress/e2e/__image_snapshots__/PDF display IX sidepanel pdf on the sidepanel should check that the pdf renders and scrolls to the selection #0.png new file mode 100644 index 0000000000..1fdf5f8eb5 Binary files /dev/null and b/cypress/e2e/__image_snapshots__/PDF display IX sidepanel pdf on the sidepanel should check that the pdf renders and scrolls to the selection #0.png differ diff --git a/cypress/e2e/__image_snapshots__/PDF display Library should check the document #0.png b/cypress/e2e/__image_snapshots__/PDF display Library should check the document #0.png new file mode 100644 index 0000000000..66edb7e72d Binary files /dev/null and b/cypress/e2e/__image_snapshots__/PDF display Library should check the document #0.png differ diff --git a/cypress/e2e/__image_snapshots__/PDF display responsiveness IX sidepanel should open the pdf sidepanel and show in the correct page #0.png b/cypress/e2e/__image_snapshots__/PDF display responsiveness IX sidepanel should open the pdf sidepanel and show in the correct page #0.png new file mode 100644 index 0000000000..981fe7e34c Binary files /dev/null and b/cypress/e2e/__image_snapshots__/PDF display responsiveness IX sidepanel should open the pdf sidepanel and show in the correct page #0.png differ diff --git a/cypress/e2e/__image_snapshots__/PDF display responsiveness library should check that the selection looks ok #0.png b/cypress/e2e/__image_snapshots__/PDF display responsiveness library should check that the selection looks ok #0.png new file mode 100644 index 0000000000..b2a2d699d4 Binary files /dev/null and b/cypress/e2e/__image_snapshots__/PDF display responsiveness library should check that the selection looks ok #0.png differ diff --git a/cypress/e2e/__image_snapshots__/PDF display responsiveness library should view the pdf correctly #0.png b/cypress/e2e/__image_snapshots__/PDF display responsiveness library should view the pdf correctly #0.png new file mode 100644 index 0000000000..bd582a3d1f Binary files /dev/null and b/cypress/e2e/__image_snapshots__/PDF display responsiveness library should view the pdf correctly #0.png differ diff --git a/cypress/e2e/__snapshots__/pdf-display.cy.ts.snap b/cypress/e2e/__snapshots__/pdf-display.cy.ts.snap new file mode 100644 index 0000000000..ba943c7219 --- /dev/null +++ b/cypress/e2e/__snapshots__/pdf-display.cy.ts.snap @@ -0,0 +1,2751 @@ +exports[`PDF display > Library > should check the document #0`] = ` +
    +
    +
    +
    + +
    +
    + RESOLUCIÓN DE LA
    CORTE INTERAMERICANA DE DERECHOS HUMANOS

    DE 26 DE JUNIO DE 2012

    MEDIDAS PROVISIONALES

    RESPECTO DE LA REPÚBLICA DE COLOMBIA

    CASO 19 COMERCIANTES + Vs. + COLOMBIA

    VISTO:

    1.
    + La Sentencia de fondo, reparaciones y costas dictada por la Corte + Interamericana de
    Derechos Humanos (en adelante “la Corte Interamericana”, “la Corte” + o “el Tribunal”) el 5

    de julio de 2004.

    2. + Las Resoluciones dictadas por el Presidente de la Corte el 30 de + julio de 2004, 28 de
    abril de 2006 y 6 de febrero de 2007, así como las resoluciones de + la Corte Interamericana
    de 3 de septiembre de 2004, 4 de julio de 2006, 12 de mayo de 2007, + 8 de julio de 2009 y
    26 de agosto de 2010. En esta última resolución el Tribunal + decidió:

    1. + Continuar supervisando el cumplimiento de la obligación de + garantizar la vida, integridad
    y seguridad de Carmen Rosa Barrera Sánchez, Lina Noralba Navarro + Flórez, Luz Marina Pérez
    Quintero, Miryam Mantilla Sánchez, Ana Murillo Delgado de Chaparro, + Suney Dinora Jáuregui
    Jaimes, Ofelia Sauza Suárez de Uribe, Rosalbina Suárez Bravo de + Sauza, Marina Lobo Pacheco,
    Manuel Ayala Mantilla, Jorge Corzo Viviescas, Alejandro Flórez + Pérez, Luz Marina Pinzón Reyes y
    sus familias, según lo señalado en el punto resolutivo undécimo de + la Sentencia, en el marco de
    la + implementación + de + las + medidas + provisionales, + de + conformidad + con + lo + establecido + en + la
    Resolución de la Corte de 8 de julio de 2009.

    2. + Requerir al Estado de Colombia que mantenga las medidas que hubiese + adoptado y que
    adopte, sin dilación, las medidas necesarias para proteger los + derechos a la vida e integridad
    personal de los señores Wilmar Rodríguez Quintero, Yimmy Efraín + Rodríguez Quintero, Nubia
    Saravia, + Karen + Dayana + Rodríguez + Saravia, + Valeria + Rodríguez + Saravia + y + William + Rodríguez
    Quintero, para lo cual deberá brindar participación a los + beneficiarios de estas medidas o sus
    representantes + en + la + planificación + e + implementación + de + las + mismas + y + que, + en + general, + les
    mantenga informados sobre el avance de su ejecución.

    3. + Levantar y dar por concluidas las medidas provisionales otorgadas a + favor de Salomón
    Flórez Contreras, Sandra Belinda Montero Fuentes, y sus respectivas + familias, de conformidad
    con lo establecido en […] esta Resolución.

    4. + Declarar que las medidas provisionales ordenadas por la Corte + Interamericana a favor
    de Luis José Pundor Quintero y su familia quedarán sin efectos + durante el tiempo que éstos
    continúen + residiendo + fuera + de + Colombia, + de + conformidad + con + lo + establecido + en + […] + esta
    Resolución.
    +
    +
    + +
    +
    +
    +`; + +exports[`PDF display > IX sidepanel > pdf on the sidepanel > should check that the pdf renders and scrolls to the selection #0`] = ` +
    +
    +
    +
    + +
    +
    + -2-
    3. + Los escritos de 17 de septiembre y 17 de noviembre de 2010, de 28 + de marzo, 13
    de abril, 27 de mayo, 11 de agosto, 10 de octubre y 7 de diciembre + de 2011 y de 10 de
    febrero y 12 de abril de 2012, mediante los cuales
    + la República de Colombia (en adelante
    “el Estado” o “Colombia”)
    + informó sobre la implementación de las medidas provisionales + y
    presentó solicitudes de levantamiento respecto de algunos + beneficiarios.

    4. + Los escritos de 12 de agosto, 14 y 26 de octubre y 24 de noviembre + de 2010, de 3
    de junio, 29 de noviembre y 2 y 26 de diciembre de 2011, y de 18 de + abril y 21 de junio de
    2012 y sus anexos, mediante los cuales los representantes de los + beneficiarios (en adelante
    “los + representantes”) + presentaron + sus + observaciones + a + lo + informado + por + el + Estado + e
    información adicional respecto a la implementación de las presentes + medidas provisionales.

    5. + Las comunicaciones de 12 de abril, 26 de mayo, 14 de julio y 20 de + septiembre de
    2011 y de 13 de enero, 11 de abril y 8 de junio de 20121, mediante las cuales la Comisión
    Interamericana + de + Derechos + Humanos + (en + adelante + “la + Comisión + Interamericana” + o + “la
    Comisión”) presentó sus observaciones + a los informes estatales y a las correspondientes
    observaciones de los representantes.

    6. + Las notas de 22 de septiembre y 22 de noviembre de 2010 y de 6 de + abril de 2011,
    mediante las cuales la Secretaría de la Corte, siguiendo + instrucciones de la Presidencia del
    Tribunal, + solicitó + expresamente + a + los + representantes + que + en + sus + observaciones + a + los
    informes estatales se refirieran a la solicitud de levantamiento de + las medidas ordenadas a
    favor del beneficiario William Rodríguez Quintero, así como a la + solicitud del Estado de que
    la + Corte + “evalúe + […] + la + vigencia + de las + medidas + provisionales + otorgadas + a + favor + de + las
    personas identificadas en el punto resolutivo No. 1 de la + Resolución de la […] Corte […] de
    26 de agosto de 2010”.

    CONSIDERANDO QUE:

    1. + Colombia es Estado Parte de la Convención Americana sobre Derechos + Humanos (en
    adelante “la Convención Americana” + o “la Convención”) desde el 31 de julio de 1973 y
    reconoció + la + competencia + de + la + Corte + Interamericana, + conforme + al + artículo + 62 + de + la
    Convención, el 21 de junio de 1985.

    2. + La disposición establecida en el artículo 63.2 de la Convención + confiere un carácter
    obligatorio a la adopción, por parte del Estado, de las medidas + provisionales que le ordene
    este + Tribunal, + ya + que + el + principio + básico + del + Derecho + Internacional, + respaldado + por + la
    jurisprudencia internacional, ha señalado que los Estados deben + cumplir sus obligaciones
    convencionales + de + buena + fe + (pacta + sunt + servanda) + 2 + . + Estas + órdenes + implican + un + deber

    1
    + La Comisión Interamericana presentó estas observaciones + el 8 de junio de 2012 + sin haber podido contar
    previamente con las respectivas observaciones de los + representantes
    + al + informe + estatal de 12 de abril de 2012, ya
    que éstos las presentaron el 21 de junio de 2012.

    2
    + Cfr. Asunto James y otros. + Medidas Provisionales respecto de Trinidad y Tobago. + Resolución de la Corte + de
    14 de junio de 1998, Considerando sexto;
    + Asunto Alvarado Reyes y otros. Medidas Provisionales respecto de
    México. Resolución + de + la + Corte + de 26 + de + mayo + de 2010, + Considerando + quinto, + y
    + Asunto + de + la + Fundación + de
    Antropología Forense.
    + Medidas Provisionales respecto de Guatemala. Resolución del + Presidente de la Corte de 21
    de julio de 2010, Considerando cuarto
    . +
    +
    +
    +
    +
    +
    +`; + +exports[`PDF display > responsiveness > library > should check that the selection looks ok #0`] = ` +
    +`; diff --git a/cypress/e2e/entity.cy.ts b/cypress/e2e/entity.cy.ts index 18d4fda4ce..8670c12991 100644 --- a/cypress/e2e/entity.cy.ts +++ b/cypress/e2e/entity.cy.ts @@ -1,7 +1,13 @@ +/* eslint-disable max-statements */ /* eslint-disable max-lines */ import { clearCookiesAndLogin } from './helpers/login'; import { changeLanguage } from './helpers/language'; -import { clickOnCreateEntity, clickOnEditEntity } from './helpers/entities'; +import { + clickOnCreateEntity, + clickOnEditEntity, + saveEntity, + selectRestrictedEntities, +} from './helpers'; const filesAttachments = ['./cypress/test_files/valid.pdf', './cypress/test_files/batman.jpg']; @@ -24,24 +30,6 @@ const webAttachments = { url: 'https://fonts.googleapis.com/icon?family=Material+Icons', }; -const goToRestrictedEntities = () => { - cy.contains('a', 'Library').click(); - cy.get('#publishedStatuspublished').then(element => { - const publishedStatus = element.val(); - cy.get('#publishedStatusrestricted').then(restrictedElement => { - const restrictedStatis = restrictedElement.val(); - - if (publishedStatus) { - cy.get('[title="Published"]').click(); - } - - if (!restrictedStatis) { - cy.get('[title="Restricted"]').click(); - } - }); - }); -}; - describe('Entities', () => { before(() => { const env = { DATABASE_NAME: 'uwazi_e2e', INDEX_NAME: 'uwazi_e2e' }; @@ -49,11 +37,6 @@ describe('Entities', () => { clearCookiesAndLogin(); }); - const saveEntity = (message = 'Entity created') => { - cy.contains('button', 'Save').click(); - cy.contains(message); - }; - it('Should create new entity', () => { clickOnCreateEntity(); cy.get('[name="library.sidepanel.metadata.title"]').click(); @@ -87,7 +70,8 @@ describe('Entities', () => { describe('Entity with files in metadata fields', () => { it('should create and entity with and image in a metadata field', () => { - goToRestrictedEntities(); + cy.contains('a', 'Library').click(); + selectRestrictedEntities(); clickOnCreateEntity(); cy.get('[name="library.sidepanel.metadata.title"]').click(); cy.get('[name="library.sidepanel.metadata.title"]').type('Entity with media files', { @@ -115,28 +99,28 @@ describe('Entities', () => { .first() .selectFile('./cypress/test_files/short-video.webm', { force: true, - timeout: 100, + timeout: 1000, }); saveEntity('Entity updated'); + cy.get('.sidepanel-body.scrollable').scrollTo('top'); + cy.get('.metadata-sidepanel.is-active .closeSidepanel').click(); }); it('should check the entity', () => { - cy.get('.sidepanel-body.scrollable').scrollTo('top'); - cy.get('.metadata-sidepanel.is-active .closeSidepanel').click(); - goToRestrictedEntities(); cy.contains('.item-name span', 'Entity with media files').click(); cy.get('.metadata-name-descripci_n > dd > div > p').should( 'contain.text', 'A description of the report' ); - cy.get('.metadata-name-fotograf_a > dd > img') .should('have.prop', 'src') .and('match', /\w+\/api\/files\/\w+\.jpg$/); cy.get('.metadata-sidepanel .sidepanel-body').scrollTo('bottom'); - cy.get('.metadata-name-video > dd > div > div > div > div:nth-child(1) > div > video') - .should('have.prop', 'src') - .and('match', /^blob:http:\/\/localhost:3000\/[\w-]+$/); + cy.contains('.metadata-name-video', 'Video').within(() => { + cy.get('video') + .should('have.prop', 'src') + .and('match', /^blob:http:\/\/localhost:3000\/[\w-]+$/); + }); const expectedNewEntityFiles = ['batman.jpg', 'short-video.webm']; cy.get('.attachment-name span:not(.attachment-size)').each((element, index) => { const content = element.text(); @@ -149,7 +133,8 @@ describe('Entities', () => { describe('Entity with supporting files', () => { it('Should create a new entity with supporting files', () => { cy.get('.metadata-sidepanel.is-active .closeSidepanel').click(); - goToRestrictedEntities(); + cy.contains('a', 'Library').click(); + selectRestrictedEntities(); clickOnCreateEntity(); cy.contains('button', 'Add file').click(); cy.get('#tab-uploadComputer').click(); @@ -227,7 +212,8 @@ describe('Entities', () => { describe('Entity with main documents', () => { it('Should create a new entity with a main documents', () => { cy.get('.metadata-sidepanel.is-active .closeSidepanel').click(); - goToRestrictedEntities(); + cy.contains('a', 'Library').click(); + selectRestrictedEntities(); clickOnCreateEntity(); cy.get('textarea[name="library.sidepanel.metadata.title"]').click(); cy.get('textarea[name="library.sidepanel.metadata.title"]').type( @@ -257,19 +243,17 @@ describe('Entities', () => { cy.get('.fa-file', { timeout: 5000 }).then(() => { cy.get('.fa-file').realClick(); }); - cy.contains('.create-reference li:nth-child(1) span:nth-child(2)', 'Relacionado a').click({ - timeout: 5000, - }); + cy.contains('.create-reference', 'Relacionado a').should('be.visible'); + cy.contains('li.multiselectItem', 'Relacionado a').realClick(); cy.get('aside.create-reference input').type('Patrick Robinson', { timeout: 5000 }); cy.contains('Tracy Robinson', { timeout: 5000 }); - cy.contains('.item-name', 'Patrick Robinson', { timeout: 5000 }).click(); + cy.contains('.item-name', 'Patrick Robinson', { timeout: 5000 }).realClick(); cy.contains('aside.create-reference .btn-success', 'Save', { timeout: 5000 }).click({ timeout: 5000, }); cy.contains('Saved successfully.'); cy.get('#p3R_mc0').scrollIntoView(); - cy.get('#p3R_mc24 > span:nth-child(2)').toMatchImageSnapshot(); - cy.get('.relationship-active').toMatchImageSnapshot(); + cy.get('.row').toMatchImageSnapshot(); }); it('should edit the entity and the documents', () => { diff --git a/cypress/e2e/helpers/entities.ts b/cypress/e2e/helpers/entities.ts index d149d376f0..31fb7bec42 100644 --- a/cypress/e2e/helpers/entities.ts +++ b/cypress/e2e/helpers/entities.ts @@ -28,4 +28,11 @@ const grantPermission = (row: number, previous: string, action: string = 'write' cy.contains('Update success'); }; -export { clickOnCreateEntity, clickOnEditEntity, shareSearchTerm, grantPermission }; +const saveEntity = (message = 'Entity created') => { + cy.intercept('POST', '/api/entities').as('saveEntity'); + cy.contains('button', 'Save').click(); + cy.wait('@saveEntity'); + cy.contains(message); +}; + +export { clickOnCreateEntity, clickOnEditEntity, shareSearchTerm, grantPermission, saveEntity }; diff --git a/cypress/e2e/helpers/index.ts b/cypress/e2e/helpers/index.ts index 99417c2f9a..31484cb18b 100644 --- a/cypress/e2e/helpers/index.ts +++ b/cypress/e2e/helpers/index.ts @@ -2,3 +2,11 @@ export { clearCookiesAndLogin } from './login'; export { selectPublishedEntities, selectRestrictedEntities } from './entitiesFilters'; export { createUser } from './users'; export { changeLanguage } from './language'; +export { + clickOnCreateEntity, + clickOnEditEntity, + shareSearchTerm, + grantPermission, + saveEntity, +} from './entities'; +export { editPropertyForExtractor } from './information-extraction'; diff --git a/cypress/e2e/helpers/information-extraction.ts b/cypress/e2e/helpers/information-extraction.ts new file mode 100644 index 0000000000..61508e7aaf --- /dev/null +++ b/cypress/e2e/helpers/information-extraction.ts @@ -0,0 +1,12 @@ +const editPropertyForExtractor = ( + alias: string, + templateName: string, + property: string, + shouldUnfold = true +) => { + cy.contains('span', templateName).as(alias); + if (shouldUnfold) cy.get(`@${alias}`).click(); + cy.get(`@${alias}`).parent().parent().contains('span', property).click(); +}; + +export { editPropertyForExtractor }; diff --git a/cypress/e2e/pdf-display.cy.ts b/cypress/e2e/pdf-display.cy.ts new file mode 100644 index 0000000000..999821840b --- /dev/null +++ b/cypress/e2e/pdf-display.cy.ts @@ -0,0 +1,285 @@ +/* eslint-disable max-lines */ +/* eslint-disable max-statements */ +import { clearCookiesAndLogin } from './helpers/login'; +import { clickOnCreateEntity, editPropertyForExtractor, saveEntity } from './helpers'; + +describe('PDF display', () => { + before(() => { + const env = { DATABASE_NAME: 'uwazi_e2e', INDEX_NAME: 'uwazi_e2e' }; + cy.blankState(); + cy.exec('yarn ix-config', { env }); + clearCookiesAndLogin('admin', 'change this password now'); + }); + + describe('setup', () => { + it('should setup the template', () => { + cy.contains('a', 'Settings').click(); + cy.contains('a', 'Templates').click(); + cy.contains('li', 'Document').contains('a', 'Edit').click(); + cy.contains('li', 'Text').within(() => { + cy.get('button').click(); + }); + cy.contains('button', 'Save').click(); + cy.contains('div', 'Saved successfully.'); + }); + + it('should create and entity with a pdf file', () => { + cy.contains('a', 'Library').click(); + clickOnCreateEntity(); + cy.get('[name="library.sidepanel.metadata.title"]').click(); + cy.get('[name="library.sidepanel.metadata.title"]').type('Entity with pdf', { delay: 0 }); + cy.get('.document-list-parent > input').first().selectFile('./cypress/test_files/valid.pdf', { + force: true, + }); + saveEntity(); + cy.get('.metadata-sidepanel.is-active .closeSidepanel').click(); + }); + }); + + describe('Library', () => { + it('should wait until document processing is done and view the entity', () => { + cy.contains('.item-document', 'Entity with pdf').within(() => { + cy.contains('span', 'Processing...').should('not.exist'); + cy.contains('a', 'View').click(); + }); + }); + + it('should check the document', () => { + cy.contains('CORTE INTERAMERICANA DE DERECHOS HUMANOS'); + cy.get('.row').eq(0).toMatchImageSnapshot(); + cy.get('div[data-region-selector-id="1"]').toMatchSnapshot({ name: 'PDF library render' }); + }); + + it('should paginate forward', () => { + cy.get('.paginator').within(() => { + cy.contains('a', 'Next').click(); + }); + cy.contains('Los escritos de 17 de septiembre y 17 de noviembre de 2010,'); + + cy.get('.paginator').within(() => { + cy.contains('a', 'Next').click(); + }); + cy.contains('especial de protección de los beneficiarios de las medidas,'); + + cy.get('.paginator').within(() => { + cy.contains('a', 'Next').click(); + }); + cy.contains('En la presente Resolución el Tribunal examinará:'); + cy.contains('CORTE INTERAMERICANA DE DERECHOS HUMANOS').should('not.exist'); + }); + + it('should check that visited pages are unmounted and that non visited pages are not rendered', () => { + cy.get('#page-1 > div').should('be.empty'); + cy.get('#page-10').should('be.empty'); + }); + + it('should paginate backwards', () => { + cy.get('.paginator').within(() => { + cy.contains('a', 'Previous').click(); + }); + cy.contains('especial de protección de los beneficiarios de las medidas,'); + cy.contains('En la presente Resolución el Tribunal examinará:').should('not.be.visible'); + }); + + it('should show the plaintex for the page', () => { + cy.contains('a', 'Plain text').click(); + cy.get('.raw-text').should('be.visible'); + cy.get('.raw-text').within(() => { + cy.contains('-3especial de protección'); + }); + }); + + it('should paginate in plain text view', () => { + cy.get('.paginator').within(() => { + cy.contains('a', 'Next').click(); + }); + cy.get('.raw-text').within(() => { + cy.contains('-4-'); + }); + }); + }); + + describe('IX sidepanel', () => { + describe('setup', () => { + it('should navigate to the entity with the document', () => { + cy.contains('a', 'Library').click(); + cy.contains('.item-document', 'Entity with pdf').within(() => { + cy.contains('a', 'View').click(); + }); + }); + + it('should make a selection for the text property to test sidepanel automatic scroll', () => { + cy.get('.paginator').within(() => { + cy.contains('a', 'Next').click(); + }); + cy.contains('Los escritos de 17 de septiembre y 17 de noviembre de 2010,'); + + cy.get('button.edit-metadata').click(); + + cy.contains( + 'span[role="presentation"]', + 'Los escritos de 12 de agosto, 14 y 26 de octubre y 24 de noviembre de 2010, de 3' + ).setSelection( + 'Los escritos de 12 de agosto, 14 y 26 de octubre y 24 de noviembre de 2010, de 3' + ); + + cy.get('.form-group.text').within(() => { + cy.get('button.extraction-button').click(); + }); + cy.get('input[name="documentViewer.sidepanel.metadata.metadata.text"]') + .invoke('val') + .should( + 'eq', + 'Los escritos de 12 de agosto, 14 y 26 de octubre y 24 de noviembre de 2010, de 3' + ); + + cy.get('button[type="submit"]').click(); + cy.get('div.alert-success').click(); + }); + + it('should navigate to the IX settings screen and create and extractor for the text property', () => { + cy.contains('a', 'Settings').click(); + cy.contains('a', 'Metadata Extraction').click(); + cy.contains('button', 'Create Extractor').click(); + cy.getByTestId('modal').within(() => { + cy.get('input[id="extractor-name"]').type('Extractor 1', { delay: 0 }); + editPropertyForExtractor('firstTemplate', 'Document', 'Text'); + cy.contains('button', 'Next').click(); + cy.contains('Text'); + cy.contains('button', 'Create').click(); + }); + cy.contains('td', 'Extractor 1'); + cy.contains('button', 'Dismiss').click(); + }); + }); + + describe('pdf on the sidepanel', () => { + it('should view the extractor', () => { + cy.contains('a', 'Settings').click(); + cy.contains('a', 'Metadata Extraction').click(); + cy.contains('td', 'Extractor 1'); + cy.contains('button', 'Review').click(); + cy.contains('td', 'Entity with pdf (es)'); + }); + + it('should check that the pdf renders and scrolls to the selection', () => { + cy.contains('button', 'Open PDF').realClick(); + cy.contains('Loading').should('not.exist'); + cy.get('#pdf-container').within(() => { + cy.contains( + 'Los escritos de 12 de agosto, 14 y 26 de octubre y 24 de noviembre de 2010, de 3' + ); + cy.get('.highlight-rectangle').should('be.visible'); + }); + cy.get('#root').toMatchImageSnapshot(); + cy.get('div[data-region-selector-id="2"]').toMatchSnapshot({ + name: 'IX sidepanel library render', + }); + }); + + it('should only render visible pages', () => { + cy.get('#page-2-container .page').should('not.be.empty'); + cy.get('#page-3-container .page').should('be.empty'); + cy.get('#page-7-container').scrollIntoView(); + cy.get('#page-7-container .page').should('not.be.empty'); + cy.get('#page-2-container .page').should('be.empty'); + cy.get('#page-3-container .page').should('be.empty'); + cy.contains('span[role="presentation"]', '1.3 Consideraciones de la Corte').should( + 'be.visible' + ); + }); + + it('should close the sidepanel', () => { + cy.contains('button', 'Cancel').click(); + }); + }); + }); + + describe('responsiveness', () => { + describe('library', () => { + beforeEach(() => { + cy.viewport('ipad-mini'); + }); + + it('should navigate to the library', () => { + cy.get('header').within(() => { + cy.get('.menu-button').realTouch(); + }); + cy.contains('a', 'Library').realTouch(); + }); + + it('should view the pdf correctly', () => { + cy.contains('.item-document', 'Entity with pdf').within(() => { + cy.contains('a', 'View').realTouch(); + }); + cy.get('.closeSidepanel').realTouch(); + cy.get('aside.metadata-sidepanel').should('not.be.visible'); + cy.contains('CORTE INTERAMERICANA DE DERECHOS HUMANOS').should('be.visible'); + cy.get('#root').toMatchImageSnapshot(); + }); + + it('should check that the selection looks ok', () => { + cy.get('#page-2').scrollIntoView(); + cy.contains('Los escritos de 17 de septiembre y 17 de noviembre de 2010,').should( + 'be.visible' + ); + cy.get('.ContextMenu-bottom .btn').realTouch(); + cy.contains('.btn', 'Edit').realTouch(); + cy.get('.highlight-rectangle').toMatchSnapshot({ name: 'responsive selection' }); + cy.get('#root').toMatchImageSnapshot(); + cy.contains('.btn', 'Cancel').realTouch(); + cy.get('.closeSidepanel').realTouch(); + }); + }); + + describe('IX sidepanel', () => { + beforeEach(() => { + cy.viewport('iphone-x'); + }); + + it('should navigate to the extractor', () => { + cy.get('header').within(() => { + cy.get('.menu-button').realTouch(); + }); + cy.get('.menuActions > .menuNav-list').within(() => { + cy.contains('.only-mobile', 'Settings').realTouch(); + }); + cy.contains('a', 'Metadata Extraction').realTouch(); + cy.contains('td', 'Extractor 1'); + cy.contains('button', 'Review').realTouch(); + cy.contains('td', 'Entity with pdf (es)'); + }); + + it('should open the pdf sidepanel and show in the correct page', () => { + cy.contains('button', 'Open PDF').realTouch(); + cy.contains('Loading').should('not.exist'); + cy.get('#pdf-container').within(() => { + cy.contains( + 'Los escritos de 12 de agosto, 14 y 26 de octubre y 24 de noviembre de 2010, de 3' + ); + cy.get('.highlight-rectangle').should('be.visible'); + }); + cy.get('#root').toMatchImageSnapshot(); + }); + + it('should only show visible pages', () => { + cy.get('#pdf-container').within(() => { + cy.get('#page-1-container .page').should('be.empty'); + cy.get('#page-2-container .page').should('not.be.empty'); + cy.get('#page-3-container .page').should('not.be.empty'); + cy.get('#page-10-container .page').should('be.empty'); + cy.get('#page-10-container').scrollIntoView(); + cy.get('#page-1-container .page').should('be.empty'); + cy.get('#page-2-container .page').should('be.empty'); + cy.get('#page-3-container .page').should('be.empty'); + cy.get('#page-10-container .page').should('not.be.empty'); + cy.get('#page-11-container .page').should('not.be.empty'); + cy.contains( + 'span[role="presentation"]', + 'El artículo 63.2 de la Convención exige que para que la Corte pueda disponer de' + ).should('be.visible'); + }); + }); + }); + }); +}); diff --git a/cypress/e2e/settings/information-extraction.cy.ts b/cypress/e2e/settings/information-extraction.cy.ts index c5b10395b8..4736715c3e 100644 --- a/cypress/e2e/settings/information-extraction.cy.ts +++ b/cypress/e2e/settings/information-extraction.cy.ts @@ -1,6 +1,6 @@ /* eslint-disable max-statements */ /* eslint-disable max-lines */ -import { clearCookiesAndLogin } from '../helpers'; +import { clearCookiesAndLogin, editPropertyForExtractor } from '../helpers'; import 'cypress-axe'; const labelEntityTitle = ( @@ -23,17 +23,6 @@ const checkTemplatesList = (templates: string[]) => { templates.map(template => cy.getByTestId('pill-comp').contains(template)); }; -const editPropertyForExtractor = ( - alias: string, - templateName: string, - property: string, - shouldUnfold = true -) => { - cy.contains('span', templateName).as(alias); - if (shouldUnfold) cy.get(`@${alias}`).click(); - cy.get(`@${alias}`).parent().parent().contains('span', property).click(); -}; - describe('Information Extraction', () => { before(() => { const env = { DATABASE_NAME: 'uwazi_e2e', INDEX_NAME: 'uwazi_e2e' }; @@ -336,11 +325,10 @@ describe('Information Extraction', () => { }); it('should click to fill with a new text', () => { - cy.contains('The Spectacular Spider-Man').parent().parent().siblings().last().click(); + cy.contains('tr', 'The Spectacular Spider-Man').contains('button', 'Open PDF').click(); cy.get('aside').within(() => { cy.get('input').clear(); }); - cy.get('#pdf-container').scrollTo(0, 0); cy.contains('button', 'Clear').click(); cy.contains('span[role="presentation"]', 'The Spectacular Spider-Man') .eq(0) diff --git a/package.json b/package.json index 04b20a46ec..b043069507 100644 --- a/package.json +++ b/package.json @@ -332,7 +332,6 @@ "babel-loader": "9.2.1", "babel-plugin-module-resolver": "5.0.2", "babel-plugin-transform-react-remove-prop-types": "0.4.24", - "canvas": "^2.11.2", "clean-webpack-plugin": "4.0.0", "copy-webpack-plugin": "12.0.2", "css-loader": "^7.1.2", @@ -365,6 +364,7 @@ "jest-image-snapshot": "^6.4.0", "jest-jasmine2": "^29.7.0", "jest-puppeteer": "6.1.0", + "jsdom-testing-mocks": "^1.13.1", "mini-css-extract-plugin": "^2.9.2", "mutationobserver-shim": "^0.3.7", "node-polyfill-webpack-plugin": "^4.1.0", diff --git a/yarn.lock b/yarn.lock index 14e55d924e..b78c8d3be6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2792,21 +2792,6 @@ hoist-non-react-statics "^3.3.1" react-is "^16.12.0" -"@mapbox/node-pre-gyp@^1.0.0": - version "1.0.10" - resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c" - integrity sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA== - dependencies: - detect-libc "^2.0.0" - https-proxy-agent "^5.0.0" - make-dir "^3.1.0" - node-fetch "^2.6.7" - nopt "^5.0.0" - npmlog "^5.0.1" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.11" - "@mdx-js/react@^3.0.0": version "3.0.1" resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.0.1.tgz#997a19b3a5b783d936c75ae7c47cfe62f967f746" @@ -5475,24 +5460,11 @@ append-field@^1.0.0: resolved "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz" integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY= -"aproba@^1.0.3 || ^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" - integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== - arch@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== -are-we-there-yet@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" - integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== - dependencies: - delegates "^1.0.0" - readable-stream "^3.6.0" - arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -5978,6 +5950,11 @@ better-opn@^3.0.2: dependencies: open "^8.0.4" +bezier-easing@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/bezier-easing/-/bezier-easing-2.1.0.tgz#c04dfe8b926d6ecaca1813d69ff179b7c2025d86" + integrity sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig== + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -6343,15 +6320,6 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001663: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz#99fc5ea0d9c6e96897a104a8352604378377f949" integrity sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw== -canvas@^2.11.2: - version "2.11.2" - resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.11.2.tgz#553d87b1e0228c7ac0fc72887c3adbac4abbd860" - integrity sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw== - dependencies: - "@mapbox/node-pre-gyp" "^1.0.0" - nan "^2.17.0" - simple-get "^3.0.3" - capital-case@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" @@ -6488,11 +6456,6 @@ chownr@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz" -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - chromatic@^11.15.0: version "11.16.1" resolved "https://registry.yarnpkg.com/chromatic/-/chromatic-11.16.1.tgz#d3088cdbccb00f7b12e489d0d1439665aa0db0a9" @@ -6682,11 +6645,6 @@ color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-support@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - colord@^2.9.3: version "2.9.3" resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" @@ -6838,10 +6796,6 @@ console-browserify@^1.1.0: resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== -console-control-strings@^1.0.0, console-control-strings@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" - constant-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1" @@ -7105,6 +7059,11 @@ css-loader@^7.1.2: postcss-value-parser "^4.2.0" semver "^7.5.4" +css-mediaquery@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/css-mediaquery/-/css-mediaquery-0.1.2.tgz#6a2c37344928618631c54bd33cedd301da18bea0" + integrity sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q== + css-minimizer-webpack-plugin@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-7.0.0.tgz#b77a3d2f7c0fd02d3ac250dcc2f79065363f3cd3" @@ -7578,13 +7537,6 @@ decimal.js@^10.3.1: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.0.tgz#97a7448873b01e92e5ff9117d89a7bca8e63e0fe" integrity sha512-Nv6ENEzyPQ6AItkGwLE2PGKinZZ9g59vSh2BeH6NqPu0OTKZ5ruJsVqh/orbAnqXc9pBbgXAIrc2EyaCj8NpGg== -decompress-response@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" - integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== - dependencies: - mimic-response "^2.0.0" - decompress-response@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" @@ -7683,10 +7635,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" - denque@^1.5.0: version "1.5.1" resolved "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz" @@ -7723,11 +7671,6 @@ detect-libc@^1.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== -detect-libc@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" - integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== - detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -9533,13 +9476,6 @@ fs-extra@^9.1.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-minipass@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - dependencies: - minipass "^3.0.0" - fs-monkey@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.5.tgz#fe450175f0db0d7ea758102e1d84096acb925788" @@ -9592,21 +9528,6 @@ functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== -gauge@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" - integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== - dependencies: - aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.2" - console-control-strings "^1.0.0" - has-unicode "^2.0.1" - object-assign "^4.1.1" - signal-exit "^3.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - wide-align "^1.1.2" - gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -10018,10 +9939,6 @@ has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: dependencies: has-symbols "^1.0.3" -has-unicode@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" - has@^1.0.3, has@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" @@ -10273,7 +10190,7 @@ https-proxy-agent@5.0.0: agent-base "6" debug "4" -https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: +https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -11666,6 +11583,14 @@ jsdoc-type-pratt-parser@^4.0.0: resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz#ff6b4a3f339c34a6c188cbf50a16087858d22113" integrity sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg== +jsdom-testing-mocks@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/jsdom-testing-mocks/-/jsdom-testing-mocks-1.13.1.tgz#14682f2eec4c76777cf9ae6d42de7bf665a4b83e" + integrity sha512-8BAsnuoO4DLGTf7LDbSm8fcx5CUHSv4h+bdUbwyt6rMYAXWjeHLRx9f8sYiSxoOTXy3S1e06pe87KER39o1ckA== + dependencies: + bezier-easing "^2.1.0" + css-mediaquery "^0.1.2" + jsdom@^20.0.0: version "20.0.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.0.tgz#882825ac9cc5e5bbee704ba16143e1fa78361ebf" @@ -12220,7 +12145,7 @@ make-dir@^2.0.0, make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" -make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: +make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -12424,11 +12349,6 @@ mimic-response@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== -mimic-response@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" - integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== - mimic-response@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" @@ -12495,36 +12415,16 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -minipass@^3.0.0: - version "3.1.3" - resolved "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz" - integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== - dependencies: - yallist "^4.0.0" - minipass@^4.2.4: version "4.2.8" resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== -minipass@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" - integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== - "minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== -minizlib@^2.1.1: - version "2.1.2" - resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - mixin-object@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz" @@ -12550,11 +12450,6 @@ mkdirp@^0.5.1, mkdirp@^0.5.4: dependencies: minimist "^1.2.6" -mkdirp@^1.0.3: - version "1.0.4" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - mkdirp@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" @@ -12682,11 +12577,6 @@ n-gram@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/n-gram/-/n-gram-1.0.1.tgz" -nan@^2.17.0: - version "2.17.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" - integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== - nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -12743,7 +12633,7 @@ node-addon-api@^7.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== -node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7: +node-fetch@2.6.7, node-fetch@^2.6.1: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -12861,13 +12751,6 @@ nopt@1.0.10, nopt@~1.0.10: dependencies: abbrev "1" -nopt@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" - integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== - dependencies: - abbrev "1" - normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" @@ -12894,16 +12777,6 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -npmlog@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" - integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== - dependencies: - are-we-there-yet "^2.0.0" - console-control-strings "^1.1.0" - gauge "^3.0.0" - set-blocking "^2.0.0" - nprogress@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz" @@ -15317,10 +15190,6 @@ serve-static@1.16.2: parseurl "~1.3.3" send "0.19.0" -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" - set-function-length@^1.2.1, set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -15462,7 +15331,7 @@ sift@^17.1.3: resolved "https://registry.yarnpkg.com/sift/-/sift-17.1.3.tgz#9d2000d4d41586880b0079b5183d839c7a142bf7" integrity sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ== -signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.6, signal-exit@^3.0.7: +signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.6, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -15472,20 +15341,6 @@ signal-exit@^4.0.1: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== -simple-concat@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz" - integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== - -simple-get@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55" - integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA== - dependencies: - decompress-response "^4.2.0" - once "^1.3.1" - simple-concat "^1.0.0" - simple-html-tokenizer@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/simple-html-tokenizer/-/simple-html-tokenizer-0.1.1.tgz#05c2eec579ffffe145a030ac26cfea61b980fabe" @@ -15796,7 +15651,7 @@ string-length@^4.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -16191,18 +16046,6 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" -tar@^6.1.11: - version "6.2.1" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" - integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^5.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - tdigest@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021" @@ -17356,13 +17199,6 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -wide-align@^1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" - integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== - dependencies: - string-width "^1.0.2 || 2 || 3 || 4" - wildcard@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" @@ -17510,11 +17346,6 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - yaml@^1.10.0: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"