mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-11 01:53:51 +00:00
Merge pull request #381 from acelaya-forks/feature/qr-code-improvements
Feature/qr code improvements
This commit is contained in:
17
CHANGELOG.md
17
CHANGELOG.md
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
### Added
|
||||||
|
* [#379](https://github.com/shlinkio/shlink-web-client/issues/379) Improved QR code modal, including controls to customize size and format, as well as a button to copy the link to the clipboard.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
|
||||||
## [3.0.1] - 2020-12-30
|
## [3.0.1] - 2020-12-30
|
||||||
### Added
|
### Added
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { CsvJson } from 'csvjson';
|
import { CsvJson } from 'csvjson';
|
||||||
import { ServerData } from '../data';
|
import { ServerData } from '../data';
|
||||||
|
|
||||||
|
|
||||||
interface CsvFile extends File {
|
interface CsvFile extends File {
|
||||||
type: 'text/csv' | 'text/comma-separated-values' | 'application/csv';
|
type: 'text/csv' | 'text/comma-separated-values' | 'application/csv';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,79 @@
|
|||||||
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
import { useMemo, useState } from 'react';
|
||||||
|
import { DropdownItem, FormGroup, Modal, ModalBody, ModalHeader, Row } from 'reactstrap';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import { ShortUrlModalProps } from '../data';
|
import { ShortUrlModalProps } from '../data';
|
||||||
|
import { ReachableServer } from '../../servers/data';
|
||||||
|
import { versionMatch } from '../../utils/helpers/version';
|
||||||
|
import { DropdownBtn } from '../../utils/DropdownBtn';
|
||||||
|
import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
|
||||||
|
import { buildQrCodeUrl, QrCodeCapabilities, QrCodeFormat } from '../../utils/helpers/qrCodes';
|
||||||
import './QrCodeModal.scss';
|
import './QrCodeModal.scss';
|
||||||
|
|
||||||
const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen }: ShortUrlModalProps) => (
|
interface QrCodeModalConnectProps extends ShortUrlModalProps {
|
||||||
<Modal isOpen={isOpen} toggle={toggle} centered>
|
selectedServer: ReachableServer;
|
||||||
<ModalHeader toggle={toggle}>
|
}
|
||||||
QR code for <ExternalLink href={shortUrl}>{shortUrl}</ExternalLink>
|
|
||||||
</ModalHeader>
|
const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps) => {
|
||||||
<ModalBody>
|
const [ size, setSize ] = useState(300);
|
||||||
<div className="text-center">
|
const [ format, setFormat ] = useState<QrCodeFormat>('png');
|
||||||
<img src={`${shortUrl}/qr-code`} className="qr-code-modal__img" alt="QR code" />
|
const capabilities: QrCodeCapabilities = useMemo(() => ({
|
||||||
</div>
|
useSizeInPath: !versionMatch(selectedServer.version, { minVersion: '2.5.0' }),
|
||||||
</ModalBody>
|
svgIsSupported: versionMatch(selectedServer.version, { minVersion: '2.4.0' }),
|
||||||
</Modal>
|
}), [ selectedServer ]);
|
||||||
);
|
const qrCodeUrl = useMemo(
|
||||||
|
() => buildQrCodeUrl(shortUrl, size, format, capabilities),
|
||||||
|
[ shortUrl, size, format, capabilities ],
|
||||||
|
);
|
||||||
|
const modalSize = useMemo(() => {
|
||||||
|
if (size < 500) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return size < 800 ? 'lg' : 'xl';
|
||||||
|
}, [ size ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} toggle={toggle} centered size={modalSize}>
|
||||||
|
<ModalHeader toggle={toggle}>
|
||||||
|
QR code for <ExternalLink href={shortUrl}>{shortUrl}</ExternalLink>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<Row className="mb-2">
|
||||||
|
<div className={capabilities.svgIsSupported ? 'col-md-6' : 'col-12'}>
|
||||||
|
<FormGroup>
|
||||||
|
<label className="mb-0">Size: {size}px</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
className="form-control-range"
|
||||||
|
value={size}
|
||||||
|
step={10}
|
||||||
|
min={50}
|
||||||
|
max={1000}
|
||||||
|
onChange={(e) => setSize(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
{capabilities.svgIsSupported && (
|
||||||
|
<div className="col-md-6">
|
||||||
|
<DropdownBtn text={`Format (${format})`}>
|
||||||
|
<DropdownItem active={format === 'png'} onClick={() => setFormat('png')}>PNG</DropdownItem>
|
||||||
|
<DropdownItem active={format === 'svg'} onClick={() => setFormat('svg')}>SVG</DropdownItem>
|
||||||
|
</DropdownBtn>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="mb-3">
|
||||||
|
<div>QR code URL:</div>
|
||||||
|
<ExternalLink className="indivisible" href={qrCodeUrl} />
|
||||||
|
<CopyToClipboardIcon text={qrCodeUrl} />
|
||||||
|
</div>
|
||||||
|
<img src={qrCodeUrl} className="qr-code-modal__img" alt="QR code" />
|
||||||
|
<div className="mt-2">{size}x{size}</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default QrCodeModal;
|
export default QrCodeModal;
|
||||||
|
|||||||
@@ -48,11 +48,6 @@
|
|||||||
transform: scale(1.5);
|
transform: scale(1.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.short-urls-row__copy-btn {
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.short-urls-row__copy-hint {
|
.short-urls-row__copy-hint {
|
||||||
@include vertical-align(translateX(10px));
|
@include vertical-align(translateX(10px));
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ import { isEmpty } from 'ramda';
|
|||||||
import { FC, useEffect, useRef } from 'react';
|
import { FC, useEffect, useRef } from 'react';
|
||||||
import Moment from 'react-moment';
|
import Moment from 'react-moment';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons';
|
|
||||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
|
||||||
import ColorGenerator from '../../utils/services/ColorGenerator';
|
import ColorGenerator from '../../utils/services/ColorGenerator';
|
||||||
import { StateFlagTimeout } from '../../utils/helpers/hooks';
|
import { StateFlagTimeout } from '../../utils/helpers/hooks';
|
||||||
import Tag from '../../tags/helpers/Tag';
|
import Tag from '../../tags/helpers/Tag';
|
||||||
import { SelectedServer } from '../../servers/data';
|
import { SelectedServer } from '../../servers/data';
|
||||||
|
import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
|
||||||
import { ShortUrl } from '../data';
|
import { ShortUrl } from '../data';
|
||||||
import ShortUrlVisitsCount from './ShortUrlVisitsCount';
|
import ShortUrlVisitsCount from './ShortUrlVisitsCount';
|
||||||
import { ShortUrlsRowMenuProps } from './ShortUrlsRowMenu';
|
import { ShortUrlsRowMenuProps } from './ShortUrlsRowMenu';
|
||||||
@@ -60,9 +58,7 @@ const ShortUrlsRow = (
|
|||||||
<td className="short-urls-row__cell" data-th="Short URL: ">
|
<td className="short-urls-row__cell" data-th="Short URL: ">
|
||||||
<span className="indivisible short-urls-row__cell--relative">
|
<span className="indivisible short-urls-row__cell--relative">
|
||||||
<ExternalLink href={shortUrl.shortUrl} />
|
<ExternalLink href={shortUrl.shortUrl} />
|
||||||
<CopyToClipboard text={shortUrl.shortUrl} onCopy={setCopiedToClipboard}>
|
<CopyToClipboardIcon text={shortUrl.shortUrl} onCopy={setCopiedToClipboard} />
|
||||||
<FontAwesomeIcon icon={copyIcon} className="ml-2 short-urls-row__copy-btn" />
|
|
||||||
</CopyToClipboard>
|
|
||||||
<span className="badge badge-warning short-urls-row__copy-hint" hidden={!copiedToClipboard}>
|
<span className="badge badge-warning short-urls-row__copy-hint" hidden={!copiedToClipboard}>
|
||||||
Copied short URL!
|
Copied short URL!
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { useToggle } from '../../utils/helpers/hooks';
|
|||||||
import { ShortUrl, ShortUrlModalProps } from '../data';
|
import { ShortUrl, ShortUrlModalProps } from '../data';
|
||||||
import { Versions } from '../../utils/helpers/version';
|
import { Versions } from '../../utils/helpers/version';
|
||||||
import { SelectedServer } from '../../servers/data';
|
import { SelectedServer } from '../../servers/data';
|
||||||
import QrCodeModal from './QrCodeModal';
|
|
||||||
import VisitStatsLink from './VisitStatsLink';
|
import VisitStatsLink from './VisitStatsLink';
|
||||||
import './ShortUrlsRowMenu.scss';
|
import './ShortUrlsRowMenu.scss';
|
||||||
|
|
||||||
@@ -29,6 +28,7 @@ const ShortUrlsRowMenu = (
|
|||||||
EditTagsModal: ShortUrlModal,
|
EditTagsModal: ShortUrlModal,
|
||||||
EditMetaModal: ShortUrlModal,
|
EditMetaModal: ShortUrlModal,
|
||||||
EditShortUrlModal: ShortUrlModal,
|
EditShortUrlModal: ShortUrlModal,
|
||||||
|
QrCodeModal: ShortUrlModal,
|
||||||
ForServerVersion: FC<Versions>,
|
ForServerVersion: FC<Versions>,
|
||||||
) => ({ shortUrl, selectedServer }: ShortUrlsRowMenuProps) => {
|
) => ({ shortUrl, selectedServer }: ShortUrlsRowMenuProps) => {
|
||||||
const [ isOpen, toggle ] = useToggle();
|
const [ isOpen, toggle ] = useToggle();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { resetShortUrlParams } from '../reducers/shortUrlsListParams';
|
|||||||
import { editShortUrl } from '../reducers/shortUrlEdition';
|
import { editShortUrl } from '../reducers/shortUrlEdition';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
import { ConnectDecorator } from '../../container/types';
|
||||||
import { ShortUrlsTable } from '../ShortUrlsTable';
|
import { ShortUrlsTable } from '../ShortUrlsTable';
|
||||||
|
import QrCodeModal from '../helpers/QrCodeModal';
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
@@ -40,6 +41,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
'EditTagsModal',
|
'EditTagsModal',
|
||||||
'EditMetaModal',
|
'EditMetaModal',
|
||||||
'EditShortUrlModal',
|
'EditShortUrlModal',
|
||||||
|
'QrCodeModal',
|
||||||
'ForServerVersion',
|
'ForServerVersion',
|
||||||
);
|
);
|
||||||
bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'useStateFlagTimeout');
|
bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'useStateFlagTimeout');
|
||||||
@@ -69,6 +71,9 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
bottle.serviceFactory('EditShortUrlModal', () => EditShortUrlModal);
|
bottle.serviceFactory('EditShortUrlModal', () => EditShortUrlModal);
|
||||||
bottle.decorator('EditShortUrlModal', connect([ 'shortUrlEdition' ], [ 'editShortUrl' ]));
|
bottle.decorator('EditShortUrlModal', connect([ 'shortUrlEdition' ], [ 'editShortUrl' ]));
|
||||||
|
|
||||||
|
bottle.serviceFactory('QrCodeModal', () => QrCodeModal);
|
||||||
|
bottle.decorator('QrCodeModal', connect([ 'selectedServer' ]));
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator');
|
bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator');
|
||||||
bottle.decorator('SearchBar', connect([ 'shortUrlsListParams' ], [ 'listShortUrls' ]));
|
bottle.decorator('SearchBar', connect([ 'shortUrlsListParams' ], [ 'listShortUrls' ]));
|
||||||
|
|||||||
4
src/utils/CopyToClipboardIcon.scss
Normal file
4
src/utils/CopyToClipboardIcon.scss
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.copy-to-clipboard-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
16
src/utils/CopyToClipboardIcon.tsx
Normal file
16
src/utils/CopyToClipboardIcon.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
|
import './CopyToClipboardIcon.scss';
|
||||||
|
|
||||||
|
interface CopyToClipboardIconProps {
|
||||||
|
text: string;
|
||||||
|
onCopy?: (text: string, result: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CopyToClipboardIcon: FC<CopyToClipboardIconProps> = ({ text, onCopy }) => (
|
||||||
|
<CopyToClipboard text={text} onCopy={onCopy}>
|
||||||
|
<FontAwesomeIcon icon={copyIcon} className="ml-2 copy-to-clipboard-icon" />
|
||||||
|
</CopyToClipboard>
|
||||||
|
);
|
||||||
25
src/utils/helpers/qrCodes.ts
Normal file
25
src/utils/helpers/qrCodes.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { always, cond } from 'ramda';
|
||||||
|
|
||||||
|
export interface QrCodeCapabilities {
|
||||||
|
useSizeInPath: boolean;
|
||||||
|
svgIsSupported: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QrCodeFormat = 'svg' | 'png';
|
||||||
|
|
||||||
|
export const buildQrCodeUrl = (
|
||||||
|
shortUrl: string,
|
||||||
|
size: number,
|
||||||
|
format: QrCodeFormat,
|
||||||
|
{ useSizeInPath, svgIsSupported }: QrCodeCapabilities,
|
||||||
|
): string => {
|
||||||
|
const sizeFragment = useSizeInPath ? `/${size}` : `?size=${size}`;
|
||||||
|
const formatFragment = !svgIsSupported ? '' : `format=${format}`;
|
||||||
|
const joinSymbolResolver = cond([
|
||||||
|
[ () => useSizeInPath && svgIsSupported, always('?') ],
|
||||||
|
[ () => !useSizeInPath && svgIsSupported, always('&') ],
|
||||||
|
]);
|
||||||
|
const joinSymbol = joinSymbolResolver() ?? '';
|
||||||
|
|
||||||
|
return `${shortUrl}/qr-code${sizeFragment}${joinSymbol}${formatFragment}`;
|
||||||
|
};
|
||||||
@@ -1,29 +1,81 @@
|
|||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
|
import { Modal, ModalBody, ModalHeader, Row } from 'reactstrap';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
|
import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
|
||||||
import { ShortUrl } from '../../../src/short-urls/data';
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
|
import { ReachableServer } from '../../../src/servers/data';
|
||||||
|
import { CopyToClipboardIcon } from '../../../src/utils/CopyToClipboardIcon';
|
||||||
|
import { DropdownBtn } from '../../../src/utils/DropdownBtn';
|
||||||
|
|
||||||
describe('<QrCodeModal />', () => {
|
describe('<QrCodeModal />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const shortUrl = 'https://doma.in/abc123';
|
const shortUrl = 'https://doma.in/abc123';
|
||||||
|
const createWrapper = (version = '2.5.0') => {
|
||||||
|
const selectedServer = Mock.of<ReachableServer>({ version });
|
||||||
|
|
||||||
beforeEach(() => {
|
wrapper = shallow(
|
||||||
wrapper = shallow(<QrCodeModal shortUrl={Mock.of<ShortUrl>({ shortUrl })} isOpen={true} toggle={() => {}} />);
|
<QrCodeModal
|
||||||
});
|
shortUrl={Mock.of<ShortUrl>({ shortUrl })}
|
||||||
afterEach(() => wrapper.unmount());
|
isOpen={true}
|
||||||
|
toggle={() => {}}
|
||||||
|
selectedServer={selectedServer}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
it('shows an external link to the URL', () => {
|
return wrapper;
|
||||||
const externalLink = wrapper.find(ExternalLink);
|
};
|
||||||
|
|
||||||
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
|
it('shows an external link to the URL in the header', () => {
|
||||||
|
const wrapper = createWrapper();
|
||||||
|
const externalLink = wrapper.find(ModalHeader).find(ExternalLink);
|
||||||
|
|
||||||
expect(externalLink).toHaveLength(1);
|
expect(externalLink).toHaveLength(1);
|
||||||
expect(externalLink.prop('href')).toEqual(shortUrl);
|
expect(externalLink.prop('href')).toEqual(shortUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays an image with the QR code of the URL', () => {
|
it.each([
|
||||||
const img = wrapper.find('img');
|
[ '2.3.0', '/qr-code/300' ],
|
||||||
|
[ '2.4.0', '/qr-code/300?format=png' ],
|
||||||
|
[ '2.5.0', '/qr-code?size=300&format=png' ],
|
||||||
|
])('displays an image with the QR code of the URL', (version, expectedUrl) => {
|
||||||
|
const wrapper = createWrapper(version);
|
||||||
|
const modalBody = wrapper.find(ModalBody);
|
||||||
|
const img = modalBody.find('img');
|
||||||
|
const linkInBody = modalBody.find(ExternalLink);
|
||||||
|
const copyToClipboard = modalBody.find(CopyToClipboardIcon);
|
||||||
|
|
||||||
expect(img).toHaveLength(1);
|
expect(img.prop('src')).toEqual(`${shortUrl}${expectedUrl}`);
|
||||||
expect(img.prop('src')).toEqual(`${shortUrl}/qr-code`);
|
expect(linkInBody.prop('href')).toEqual(`${shortUrl}${expectedUrl}`);
|
||||||
|
expect(copyToClipboard.prop('text')).toEqual(`${shortUrl}${expectedUrl}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[ 530, 'lg' ],
|
||||||
|
[ 200, undefined ],
|
||||||
|
[ 830, 'xl' ],
|
||||||
|
])('renders expected size', (size, modalSize) => {
|
||||||
|
const wrapper = createWrapper();
|
||||||
|
const sizeInput = wrapper.find('.form-control-range');
|
||||||
|
|
||||||
|
sizeInput.simulate('change', { target: { value: `${size}` } });
|
||||||
|
|
||||||
|
expect(wrapper.find('.mt-2').text()).toEqual(`${size}x${size}`);
|
||||||
|
expect(wrapper.find('label').text()).toEqual(`Size: ${size}px`);
|
||||||
|
expect(wrapper.find(Modal).prop('size')).toEqual(modalSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[ '2.3.0', 0, 'col-12' ],
|
||||||
|
[ '2.4.0', 1, 'col-md-6' ],
|
||||||
|
])('shows expected components based on server version', (version, expectedAmountOfDropdowns, expectedRangeClass) => {
|
||||||
|
const wrapper = createWrapper(version);
|
||||||
|
const dropdown = wrapper.find(DropdownBtn);
|
||||||
|
const firstCol = wrapper.find(Row).find('div').first();
|
||||||
|
|
||||||
|
expect(dropdown).toHaveLength(expectedAmountOfDropdowns);
|
||||||
|
expect(firstCol.prop('className')).toEqual(expectedRangeClass);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import Moment from 'react-moment';
|
|||||||
import { assoc, toString } from 'ramda';
|
import { assoc, toString } from 'ramda';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
|
||||||
import createShortUrlsRow from '../../../src/short-urls/helpers/ShortUrlsRow';
|
import createShortUrlsRow from '../../../src/short-urls/helpers/ShortUrlsRow';
|
||||||
import Tag from '../../../src/tags/helpers/Tag';
|
import Tag from '../../../src/tags/helpers/Tag';
|
||||||
import ColorGenerator from '../../../src/utils/services/ColorGenerator';
|
import ColorGenerator from '../../../src/utils/services/ColorGenerator';
|
||||||
import { StateFlagTimeout } from '../../../src/utils/helpers/hooks';
|
import { StateFlagTimeout } from '../../../src/utils/helpers/hooks';
|
||||||
import { ShortUrl } from '../../../src/short-urls/data';
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
import { ReachableServer } from '../../../src/servers/data';
|
import { ReachableServer } from '../../../src/servers/data';
|
||||||
|
import { CopyToClipboardIcon } from '../../../src/utils/CopyToClipboardIcon';
|
||||||
|
|
||||||
describe('<ShortUrlsRow />', () => {
|
describe('<ShortUrlsRow />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
@@ -98,7 +98,7 @@ describe('<ShortUrlsRow />', () => {
|
|||||||
|
|
||||||
it('updates state when copied to clipboard', () => {
|
it('updates state when copied to clipboard', () => {
|
||||||
const col = wrapper.find('td').at(1);
|
const col = wrapper.find('td').at(1);
|
||||||
const menu = col.find(CopyToClipboard);
|
const menu = col.find(CopyToClipboardIcon);
|
||||||
|
|
||||||
expect(menu).toHaveLength(1);
|
expect(menu).toHaveLength(1);
|
||||||
expect(stateFlagTimeout).not.toHaveBeenCalled();
|
expect(stateFlagTimeout).not.toHaveBeenCalled();
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { shallow, ShallowWrapper } from 'enzyme';
|
|||||||
import { ButtonDropdown, DropdownItem } from 'reactstrap';
|
import { ButtonDropdown, DropdownItem } from 'reactstrap';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import createShortUrlsRowMenu from '../../../src/short-urls/helpers/ShortUrlsRowMenu';
|
import createShortUrlsRowMenu from '../../../src/short-urls/helpers/ShortUrlsRowMenu';
|
||||||
import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
|
|
||||||
import { ReachableServer } from '../../../src/servers/data';
|
import { ReachableServer } from '../../../src/servers/data';
|
||||||
import { ShortUrl } from '../../../src/short-urls/data';
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
|
|
||||||
@@ -12,6 +11,7 @@ describe('<ShortUrlsRowMenu />', () => {
|
|||||||
const EditTagsModal = () => null;
|
const EditTagsModal = () => null;
|
||||||
const EditMetaModal = () => null;
|
const EditMetaModal = () => null;
|
||||||
const EditShortUrlModal = () => null;
|
const EditShortUrlModal = () => null;
|
||||||
|
const QrCodeModal = () => null;
|
||||||
const selectedServer = Mock.of<ReachableServer>({ id: 'abc123' });
|
const selectedServer = Mock.of<ReachableServer>({ id: 'abc123' });
|
||||||
const shortUrl = Mock.of<ShortUrl>({
|
const shortUrl = Mock.of<ShortUrl>({
|
||||||
shortCode: 'abc123',
|
shortCode: 'abc123',
|
||||||
@@ -23,6 +23,7 @@ describe('<ShortUrlsRowMenu />', () => {
|
|||||||
EditTagsModal,
|
EditTagsModal,
|
||||||
EditMetaModal,
|
EditMetaModal,
|
||||||
EditShortUrlModal,
|
EditShortUrlModal,
|
||||||
|
QrCodeModal,
|
||||||
() => null,
|
() => null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
27
test/utils/CopyToClipboardIcon.test.tsx
Normal file
27
test/utils/CopyToClipboardIcon.test.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
|
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
import { CopyToClipboardIcon } from '../../src/utils/CopyToClipboardIcon';
|
||||||
|
|
||||||
|
describe('<CopyToClipboardIcon />', () => {
|
||||||
|
let wrapper: ShallowWrapper;
|
||||||
|
const onCopy = () => {};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = shallow(<CopyToClipboardIcon text="foo" onCopy={onCopy} />);
|
||||||
|
});
|
||||||
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
|
test('expected components are wrapped', () => {
|
||||||
|
const copyToClipboard = wrapper.find(CopyToClipboard);
|
||||||
|
const icon = wrapper.find(FontAwesomeIcon);
|
||||||
|
|
||||||
|
expect(copyToClipboard).toHaveLength(1);
|
||||||
|
expect(copyToClipboard.prop('text')).toEqual('foo');
|
||||||
|
expect(copyToClipboard.prop('onCopy')).toEqual(onCopy);
|
||||||
|
expect(icon).toHaveLength(1);
|
||||||
|
expect(icon.prop('icon')).toEqual(copyIcon);
|
||||||
|
expect(icon.prop('className')).toEqual('ml-2 copy-to-clipboard-icon');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,7 +6,7 @@ import { DropdownBtn, DropdownBtnProps } from '../../src/utils/DropdownBtn';
|
|||||||
describe('<DropdownBtn />', () => {
|
describe('<DropdownBtn />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const createWrapper = (props: PropsWithChildren<DropdownBtnProps>) => {
|
const createWrapper = (props: PropsWithChildren<DropdownBtnProps>) => {
|
||||||
wrapper = shallow(<DropdownBtn {...props} />);
|
wrapper = shallow(<DropdownBtn children={'foo'} {...props} />);
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
};
|
};
|
||||||
@@ -17,7 +17,7 @@ describe('<DropdownBtn />', () => {
|
|||||||
const wrapper = createWrapper({ text });
|
const wrapper = createWrapper({ text });
|
||||||
const toggle = wrapper.find(DropdownToggle);
|
const toggle = wrapper.find(DropdownToggle);
|
||||||
|
|
||||||
expect(toggle.html()).toContain(text);
|
expect(toggle.prop('children')).toContain(text);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([[ 'foo' ], [ 'bar' ], [ 'baz' ]])('displays provided children', (children) => {
|
it.each([[ 'foo' ], [ 'bar' ], [ 'baz' ]])('displays provided children', (children) => {
|
||||||
|
|||||||
@@ -76,21 +76,22 @@ describe('<SortingDropdown />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[{ isButton: false }, '>Order by<' ],
|
[{ isButton: false }, <>Order by</> ],
|
||||||
[{ isButton: true }, '>Order by...<' ],
|
[{ isButton: true }, <>Order by...</> ],
|
||||||
[
|
[
|
||||||
{ isButton: true, orderField: 'foo', orderDir: 'ASC' as OrderDir },
|
{ isButton: true, orderField: 'foo', orderDir: 'ASC' as OrderDir },
|
||||||
'Order by: "Foo" - "ASC"',
|
'Order by: "Foo" - "ASC"',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ isButton: true, orderField: 'baz', orderDir: 'DESC' as OrderDir },
|
{ isButton: true, orderField: 'baz', orderDir: 'DESC' as OrderDir },
|
||||||
'Order by: "Hello World" - "DESC"',
|
'Order by: "Hello World" - "DESC"',
|
||||||
],
|
],
|
||||||
[{ isButton: true, orderField: 'baz' }, 'Order by: "Hello World" - "DESC"' ],
|
[{ isButton: true, orderField: 'baz' }, 'Order by: "Hello World" - "DESC"' ],
|
||||||
])('displays expected text in toggle', (props, expectedText) => {
|
])('displays expected text in toggle', (props, expectedText) => {
|
||||||
const wrapper = createWrapper(props);
|
const wrapper = createWrapper(props);
|
||||||
const toggle = wrapper.find(DropdownToggle);
|
const toggle = wrapper.find(DropdownToggle);
|
||||||
|
const [ children ] = (toggle.prop('children') as any[]).filter(Boolean);
|
||||||
|
|
||||||
expect(toggle.html()).toContain(expectedText);
|
expect(children).toEqual(expectedText);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
17
test/utils/helpers/qrCodes.test.ts
Normal file
17
test/utils/helpers/qrCodes.test.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { buildQrCodeUrl, QrCodeFormat } from '../../../src/utils/helpers/qrCodes';
|
||||||
|
|
||||||
|
describe('qrCodes', () => {
|
||||||
|
describe('buildQrCodeUrl', () => {
|
||||||
|
test.each([
|
||||||
|
[ 'foo.com', 530, 'svg', { useSizeInPath: true, svgIsSupported: true }, 'foo.com/qr-code/530?format=svg' ],
|
||||||
|
[ 'foo.com', 530, 'png', { useSizeInPath: true, svgIsSupported: true }, 'foo.com/qr-code/530?format=png' ],
|
||||||
|
[ 'bar.io', 870, 'svg', { useSizeInPath: false, svgIsSupported: false }, 'bar.io/qr-code?size=870' ],
|
||||||
|
[ 'bar.io', 200, 'png', { useSizeInPath: false, svgIsSupported: true }, 'bar.io/qr-code?size=200&format=png' ],
|
||||||
|
[ 'bar.io', 200, 'svg', { useSizeInPath: false, svgIsSupported: true }, 'bar.io/qr-code?size=200&format=svg' ],
|
||||||
|
[ 'foo.net', 480, 'png', { useSizeInPath: true, svgIsSupported: false }, 'foo.net/qr-code/480' ],
|
||||||
|
[ 'foo.net', 480, 'svg', { useSizeInPath: true, svgIsSupported: false }, 'foo.net/qr-code/480' ],
|
||||||
|
])('builds expected URL based in params', (shortUrl, size, format, capabilities, expectedUrl) => {
|
||||||
|
expect(buildQrCodeUrl(shortUrl, size, format as QrCodeFormat, capabilities)).toEqual(expectedUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user