mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Merge pull request #471 from acelaya-forks/feature/qr-code-error-correction
Feature/qr code error correction
This commit is contained in:
commit
5eee86003d
11 changed files with 223 additions and 71 deletions
|
@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
* `includes`: Suggests tags that contain the input.
|
* `includes`: Suggests tags that contain the input.
|
||||||
|
|
||||||
* [#464](https://github.com/shlinkio/shlink-web-client/pull/464) Added support to download QR codes. This feature requires an unreleased version of Shlink, so it comes disabled, and will get enabled as soon as Shlink v2.9 is released.
|
* [#464](https://github.com/shlinkio/shlink-web-client/pull/464) Added support to download QR codes. This feature requires an unreleased version of Shlink, so it comes disabled, and will get enabled as soon as Shlink v2.9 is released.
|
||||||
|
* [#469](https://github.com/shlinkio/shlink-web-client/pull/469) Added support `errorCorrection` in QR codes, when consuming Shlink 2.8 or higher.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|
|
@ -7,10 +7,11 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "npm run lint:css && npm run lint:js",
|
"lint": "npm run lint:css && npm run lint:js",
|
||||||
"lint:js": "eslint --ext .js,.ts,.tsx src test",
|
|
||||||
"lint:js:fix": "npm run lint:js -- --fix",
|
|
||||||
"lint:css": "stylelint src/*.scss src/**/*.scss",
|
"lint:css": "stylelint src/*.scss src/**/*.scss",
|
||||||
|
"lint:js": "eslint --ext .js,.ts,.tsx src test",
|
||||||
|
"lint:fix": "npm run lint:css:fix && npm run lint:js:fix",
|
||||||
"lint:css:fix": "npm run lint:css -- --fix",
|
"lint:css:fix": "npm run lint:css -- --fix",
|
||||||
|
"lint:js:fix": "npm run lint:js -- --fix",
|
||||||
"start": "node scripts/start.js",
|
"start": "node scripts/start.js",
|
||||||
"serve:build": "serve ./build",
|
"serve:build": "serve ./build",
|
||||||
"build": "node scripts/build.js",
|
"build": "node scripts/build.js",
|
||||||
|
|
|
@ -1,37 +1,45 @@
|
||||||
import { FC, useMemo, useState } from 'react';
|
import { FC, useMemo, useState } from 'react';
|
||||||
import { Modal, DropdownItem, FormGroup, ModalBody, ModalHeader, Row, Button } from 'reactstrap';
|
import { Modal, FormGroup, ModalBody, ModalHeader, Row, Button } from 'reactstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faFileDownload as downloadIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faFileDownload as downloadIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { ShortUrlModalProps } from '../data';
|
import { ShortUrlModalProps } from '../data';
|
||||||
import { SelectedServer } from '../../servers/data';
|
import { SelectedServer } from '../../servers/data';
|
||||||
import { DropdownBtn } from '../../utils/DropdownBtn';
|
|
||||||
import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
|
import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
|
||||||
import { buildQrCodeUrl, QrCodeCapabilities, QrCodeFormat } from '../../utils/helpers/qrCodes';
|
import { buildQrCodeUrl, QrCodeCapabilities, QrCodeFormat, QrErrorCorrection } from '../../utils/helpers/qrCodes';
|
||||||
import { supportsQrCodeSizeInQuery, supportsQrCodeSvgFormat, supportsQrCodeMargin } from '../../utils/helpers/features';
|
import {
|
||||||
|
supportsQrCodeSizeInQuery,
|
||||||
|
supportsQrCodeSvgFormat,
|
||||||
|
supportsQrCodeMargin,
|
||||||
|
supportsQrErrorCorrection,
|
||||||
|
} from '../../utils/helpers/features';
|
||||||
import { ImageDownloader } from '../../common/services/ImageDownloader';
|
import { ImageDownloader } from '../../common/services/ImageDownloader';
|
||||||
import { Versions } from '../../utils/helpers/version';
|
import { Versions } from '../../utils/helpers/version';
|
||||||
|
import { QrFormatDropdown } from './qr-codes/QrFormatDropdown';
|
||||||
import './QrCodeModal.scss';
|
import './QrCodeModal.scss';
|
||||||
|
import { QrErrorCorrectionDropdown } from './qr-codes/QrErrorCorrectionDropdown';
|
||||||
|
|
||||||
interface QrCodeModalConnectProps extends ShortUrlModalProps {
|
interface QrCodeModalConnectProps extends ShortUrlModalProps {
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Versions>) => (
|
const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Versions>) => ( // eslint-disable-line
|
||||||
{ shortUrl: { shortUrl, shortCode }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps,
|
{ shortUrl: { shortUrl, shortCode }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps,
|
||||||
) => {
|
) => {
|
||||||
const [ size, setSize ] = useState(300);
|
const [ size, setSize ] = useState(300);
|
||||||
const [ margin, setMargin ] = useState(0);
|
const [ margin, setMargin ] = useState(0);
|
||||||
const [ format, setFormat ] = useState<QrCodeFormat>('png');
|
const [ format, setFormat ] = useState<QrCodeFormat>('png');
|
||||||
|
const [ errorCorrection, setErrorCorrection ] = useState<QrErrorCorrection>('L');
|
||||||
const capabilities: QrCodeCapabilities = useMemo(() => ({
|
const capabilities: QrCodeCapabilities = useMemo(() => ({
|
||||||
useSizeInPath: !supportsQrCodeSizeInQuery(selectedServer),
|
useSizeInPath: !supportsQrCodeSizeInQuery(selectedServer),
|
||||||
svgIsSupported: supportsQrCodeSvgFormat(selectedServer),
|
svgIsSupported: supportsQrCodeSvgFormat(selectedServer),
|
||||||
marginIsSupported: supportsQrCodeMargin(selectedServer),
|
marginIsSupported: supportsQrCodeMargin(selectedServer),
|
||||||
|
errorCorrectionIsSupported: supportsQrErrorCorrection(selectedServer),
|
||||||
}), [ selectedServer ]);
|
}), [ selectedServer ]);
|
||||||
const qrCodeUrl = useMemo(
|
const qrCodeUrl = useMemo(
|
||||||
() => buildQrCodeUrl(shortUrl, { size, format, margin }, capabilities),
|
() => buildQrCodeUrl(shortUrl, { size, format, margin, errorCorrection }, capabilities),
|
||||||
[ shortUrl, size, format, margin, capabilities ],
|
[ shortUrl, size, format, margin, errorCorrection, capabilities ],
|
||||||
);
|
);
|
||||||
const totalSize = useMemo(() => size + margin, [ size, margin ]);
|
const totalSize = useMemo(() => size + margin, [ size, margin ]);
|
||||||
const modalSize = useMemo(() => {
|
const modalSize = useMemo(() => {
|
||||||
|
@ -48,50 +56,48 @@ const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Vers
|
||||||
QR code for <ExternalLink href={shortUrl}>{shortUrl}</ExternalLink>
|
QR code for <ExternalLink href={shortUrl}>{shortUrl}</ExternalLink>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Row className="mb-2">
|
<Row>
|
||||||
<div
|
<FormGroup
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'col-md-4': capabilities.marginIsSupported && capabilities.svgIsSupported,
|
'col-md-4': capabilities.marginIsSupported && capabilities.svgIsSupported && !capabilities.errorCorrectionIsSupported,
|
||||||
'col-md-6': (!capabilities.marginIsSupported && capabilities.svgIsSupported) || (capabilities.marginIsSupported && !capabilities.svgIsSupported),
|
'col-md-6': capabilities.errorCorrectionIsSupported || (!capabilities.marginIsSupported && capabilities.svgIsSupported) || (capabilities.marginIsSupported && !capabilities.svgIsSupported),
|
||||||
'col-12': !capabilities.marginIsSupported && !capabilities.svgIsSupported,
|
'col-12': !capabilities.marginIsSupported && !capabilities.svgIsSupported && !capabilities.errorCorrectionIsSupported,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<FormGroup>
|
<label className="mb-0">Size: {size}px</label>
|
||||||
<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>
|
||||||
|
{capabilities.marginIsSupported && (
|
||||||
|
<FormGroup className={capabilities.svgIsSupported && !capabilities.errorCorrectionIsSupported ? 'col-md-4' : 'col-md-6'}>
|
||||||
|
<label className="mb-0">Margin: {margin}px</label>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
className="form-control-range"
|
className="form-control-range"
|
||||||
value={size}
|
value={margin}
|
||||||
step={10}
|
step={1}
|
||||||
min={50}
|
min={0}
|
||||||
max={1000}
|
max={100}
|
||||||
onChange={(e) => setSize(Number(e.target.value))}
|
onChange={(e) => setMargin(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</div>
|
|
||||||
{capabilities.marginIsSupported && (
|
|
||||||
<div className={capabilities.svgIsSupported ? 'col-md-4' : 'col-md-6'}>
|
|
||||||
<FormGroup>
|
|
||||||
<label className="mb-0">Margin: {margin}px</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
className="form-control-range"
|
|
||||||
value={margin}
|
|
||||||
step={1}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
onChange={(e) => setMargin(Number(e.target.value))}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{capabilities.svgIsSupported && (
|
{capabilities.svgIsSupported && (
|
||||||
<div className={capabilities.marginIsSupported ? 'col-md-4' : 'col-md-6'}>
|
<FormGroup className={capabilities.marginIsSupported && !capabilities.errorCorrectionIsSupported ? 'col-md-4' : 'col-md-6'}>
|
||||||
<DropdownBtn text={`Format (${format})`}>
|
<QrFormatDropdown format={format} setFormat={setFormat} />
|
||||||
<DropdownItem active={format === 'png'} onClick={() => setFormat('png')}>PNG</DropdownItem>
|
</FormGroup>
|
||||||
<DropdownItem active={format === 'svg'} onClick={() => setFormat('svg')}>SVG</DropdownItem>
|
)}
|
||||||
</DropdownBtn>
|
{capabilities.errorCorrectionIsSupported && (
|
||||||
</div>
|
<FormGroup className="col-md-6">
|
||||||
|
<QrErrorCorrectionDropdown errorCorrection={errorCorrection} setErrorCorrection={setErrorCorrection} />
|
||||||
|
</FormGroup>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { DropdownItem } from 'reactstrap';
|
||||||
|
import { DropdownBtn } from '../../../utils/DropdownBtn';
|
||||||
|
import { QrErrorCorrection } from '../../../utils/helpers/qrCodes';
|
||||||
|
|
||||||
|
interface QrErrorCorrectionDropdownProps {
|
||||||
|
errorCorrection: QrErrorCorrection;
|
||||||
|
setErrorCorrection: (errorCorrection: QrErrorCorrection) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QrErrorCorrectionDropdown: FC<QrErrorCorrectionDropdownProps> = (
|
||||||
|
{ errorCorrection, setErrorCorrection },
|
||||||
|
) => (
|
||||||
|
<DropdownBtn text={`Error correction (${errorCorrection})`}>
|
||||||
|
<DropdownItem active={errorCorrection === 'L'} onClick={() => setErrorCorrection('L')}>
|
||||||
|
<b>L</b>ow
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem active={errorCorrection === 'M'} onClick={() => setErrorCorrection('M')}>
|
||||||
|
<b>M</b>edium
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem active={errorCorrection === 'Q'} onClick={() => setErrorCorrection('Q')}>
|
||||||
|
<b>Q</b>uartile
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem active={errorCorrection === 'H'} onClick={() => setErrorCorrection('H')}>
|
||||||
|
<b>H</b>igh
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownBtn>
|
||||||
|
);
|
16
src/short-urls/helpers/qr-codes/QrFormatDropdown.tsx
Normal file
16
src/short-urls/helpers/qr-codes/QrFormatDropdown.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { DropdownItem } from 'reactstrap';
|
||||||
|
import { DropdownBtn } from '../../../utils/DropdownBtn';
|
||||||
|
import { QrCodeFormat } from '../../../utils/helpers/qrCodes';
|
||||||
|
|
||||||
|
interface QrFormatDropdownProps {
|
||||||
|
format: QrCodeFormat;
|
||||||
|
setFormat: (format: QrCodeFormat) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QrFormatDropdown: FC<QrFormatDropdownProps> = ({ format, setFormat }) => (
|
||||||
|
<DropdownBtn text={`Format (${format})`}>
|
||||||
|
<DropdownItem active={format === 'png'} onClick={() => setFormat('png')}>PNG</DropdownItem>
|
||||||
|
<DropdownItem active={format === 'svg'} onClick={() => setFormat('svg')}>SVG</DropdownItem>
|
||||||
|
</DropdownBtn>
|
||||||
|
);
|
|
@ -27,3 +27,5 @@ export const supportsTagsInPatch = supportsShortUrlTitle;
|
||||||
export const supportsBotVisits = serverMatchesVersions({ minVersion: '2.7.0' });
|
export const supportsBotVisits = serverMatchesVersions({ minVersion: '2.7.0' });
|
||||||
|
|
||||||
export const supportsCrawlableVisits = supportsBotVisits;
|
export const supportsCrawlableVisits = supportsBotVisits;
|
||||||
|
|
||||||
|
export const supportsQrErrorCorrection = serverMatchesVersions({ minVersion: '2.8.0' });
|
||||||
|
|
|
@ -5,26 +5,31 @@ export interface QrCodeCapabilities {
|
||||||
useSizeInPath: boolean;
|
useSizeInPath: boolean;
|
||||||
svgIsSupported: boolean;
|
svgIsSupported: boolean;
|
||||||
marginIsSupported: boolean;
|
marginIsSupported: boolean;
|
||||||
|
errorCorrectionIsSupported: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QrCodeFormat = 'svg' | 'png';
|
export type QrCodeFormat = 'svg' | 'png';
|
||||||
|
|
||||||
|
export type QrErrorCorrection = 'L' | 'M' | 'Q' | 'H';
|
||||||
|
|
||||||
export interface QrCodeOptions {
|
export interface QrCodeOptions {
|
||||||
size: number;
|
size: number;
|
||||||
format: QrCodeFormat;
|
format: QrCodeFormat;
|
||||||
margin: number;
|
margin: number;
|
||||||
|
errorCorrection: QrErrorCorrection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildQrCodeUrl = (
|
export const buildQrCodeUrl = (
|
||||||
shortUrl: string,
|
shortUrl: string,
|
||||||
{ size, format, margin }: QrCodeOptions,
|
{ size, format, margin, errorCorrection }: QrCodeOptions,
|
||||||
{ useSizeInPath, svgIsSupported, marginIsSupported }: QrCodeCapabilities,
|
{ useSizeInPath, svgIsSupported, marginIsSupported, errorCorrectionIsSupported }: QrCodeCapabilities,
|
||||||
): string => {
|
): string => {
|
||||||
const baseUrl = `${shortUrl}/qr-code${useSizeInPath ? `/${size}` : ''}`;
|
const baseUrl = `${shortUrl}/qr-code${useSizeInPath ? `/${size}` : ''}`;
|
||||||
const query = stringifyQuery({
|
const query = stringifyQuery({
|
||||||
size: useSizeInPath ? undefined : size,
|
size: useSizeInPath ? undefined : size,
|
||||||
format: svgIsSupported ? format : undefined,
|
format: svgIsSupported ? format : undefined,
|
||||||
margin: marginIsSupported && margin > 0 ? margin : undefined,
|
margin: marginIsSupported && margin > 0 ? margin : undefined,
|
||||||
|
errorCorrection: errorCorrectionIsSupported ? errorCorrection : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return `${baseUrl}${isEmpty(query) ? '' : `?${query}`}`;
|
return `${baseUrl}${isEmpty(query) ? '' : `?${query}`}`;
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import { Button, Modal, ModalBody, ModalHeader, Row } from 'reactstrap';
|
import { Button, FormGroup, Modal, ModalBody, ModalHeader, Row } from 'reactstrap';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import createQrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
|
import createQrCodeModal 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 { ReachableServer } from '../../../src/servers/data';
|
||||||
import { CopyToClipboardIcon } from '../../../src/utils/CopyToClipboardIcon';
|
import { CopyToClipboardIcon } from '../../../src/utils/CopyToClipboardIcon';
|
||||||
import { DropdownBtn } from '../../../src/utils/DropdownBtn';
|
|
||||||
import { SemVer } from '../../../src/utils/helpers/version';
|
import { SemVer } from '../../../src/utils/helpers/version';
|
||||||
import { ImageDownloader } from '../../../src/common/services/ImageDownloader';
|
import { ImageDownloader } from '../../../src/common/services/ImageDownloader';
|
||||||
|
import { QrFormatDropdown } from '../../../src/short-urls/helpers/qr-codes/QrFormatDropdown';
|
||||||
|
import { QrErrorCorrectionDropdown } from '../../../src/short-urls/helpers/qr-codes/QrErrorCorrectionDropdown';
|
||||||
|
|
||||||
describe('<QrCodeModal />', () => {
|
describe('<QrCodeModal />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
@ -48,6 +49,7 @@ describe('<QrCodeModal />', () => {
|
||||||
[ '2.5.0' as SemVer, 0, '/qr-code?size=300&format=png' ],
|
[ '2.5.0' as SemVer, 0, '/qr-code?size=300&format=png' ],
|
||||||
[ '2.6.0' as SemVer, 0, '/qr-code?size=300&format=png' ],
|
[ '2.6.0' as SemVer, 0, '/qr-code?size=300&format=png' ],
|
||||||
[ '2.6.0' as SemVer, 10, '/qr-code?size=300&format=png&margin=10' ],
|
[ '2.6.0' as SemVer, 10, '/qr-code?size=300&format=png&margin=10' ],
|
||||||
|
[ '2.8.0' as SemVer, 0, '/qr-code?size=300&format=png&errorCorrection=L' ],
|
||||||
])('displays an image with the QR code of the URL', (version, margin, expectedUrl) => {
|
])('displays an image with the QR code of the URL', (version, margin, expectedUrl) => {
|
||||||
const wrapper = createWrapper(version);
|
const wrapper = createWrapper(version);
|
||||||
const formControls = wrapper.find('.form-control-range');
|
const formControls = wrapper.find('.form-control-range');
|
||||||
|
@ -91,12 +93,13 @@ describe('<QrCodeModal />', () => {
|
||||||
[ '2.3.0' as SemVer, 0, 'col-12' ],
|
[ '2.3.0' as SemVer, 0, 'col-12' ],
|
||||||
[ '2.4.0' as SemVer, 1, 'col-md-6' ],
|
[ '2.4.0' as SemVer, 1, 'col-md-6' ],
|
||||||
[ '2.6.0' as SemVer, 1, 'col-md-4' ],
|
[ '2.6.0' as SemVer, 1, 'col-md-4' ],
|
||||||
|
[ '2.8.0' as SemVer, 2, 'col-md-6' ],
|
||||||
])('shows expected components based on server version', (version, expectedAmountOfDropdowns, expectedRangeClass) => {
|
])('shows expected components based on server version', (version, expectedAmountOfDropdowns, expectedRangeClass) => {
|
||||||
const wrapper = createWrapper(version);
|
const wrapper = createWrapper(version);
|
||||||
const dropdown = wrapper.find(DropdownBtn);
|
const dropdownsLength = wrapper.find(QrFormatDropdown).length + wrapper.find(QrErrorCorrectionDropdown).length;
|
||||||
const firstCol = wrapper.find(Row).find('div').first();
|
const firstCol = wrapper.find(Row).find(FormGroup).first();
|
||||||
|
|
||||||
expect(dropdown).toHaveLength(expectedAmountOfDropdowns);
|
expect(dropdownsLength).toEqual(expectedAmountOfDropdowns);
|
||||||
expect(firstCol.prop('className')).toEqual(expectedRangeClass);
|
expect(firstCol.prop('className')).toEqual(expectedRangeClass);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
|
import { DropdownItem } from 'reactstrap';
|
||||||
|
import { QrErrorCorrection } from '../../../../src/utils/helpers/qrCodes';
|
||||||
|
import { QrErrorCorrectionDropdown } from '../../../../src/short-urls/helpers/qr-codes/QrErrorCorrectionDropdown';
|
||||||
|
|
||||||
|
describe('<QrErrorCorrectionDropdown />', () => {
|
||||||
|
const initialErrorCorrection: QrErrorCorrection = 'Q';
|
||||||
|
const setErrorCorrection = jest.fn();
|
||||||
|
let wrapper: ShallowWrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = shallow(
|
||||||
|
<QrErrorCorrectionDropdown errorCorrection={initialErrorCorrection} setErrorCorrection={setErrorCorrection} />,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => wrapper?.unmount());
|
||||||
|
afterEach(jest.clearAllMocks);
|
||||||
|
|
||||||
|
it('renders initial state', () => {
|
||||||
|
const items = wrapper.find(DropdownItem);
|
||||||
|
|
||||||
|
expect(wrapper.prop('text')).toEqual('Error correction (Q)');
|
||||||
|
expect(items.at(0).prop('active')).toEqual(false);
|
||||||
|
expect(items.at(1).prop('active')).toEqual(false);
|
||||||
|
expect(items.at(2).prop('active')).toEqual(true);
|
||||||
|
expect(items.at(3).prop('active')).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes callback when items are clicked', () => {
|
||||||
|
const items = wrapper.find(DropdownItem);
|
||||||
|
|
||||||
|
expect(setErrorCorrection).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
items.at(0).simulate('click');
|
||||||
|
expect(setErrorCorrection).toHaveBeenCalledWith('L');
|
||||||
|
|
||||||
|
items.at(1).simulate('click');
|
||||||
|
expect(setErrorCorrection).toHaveBeenCalledWith('M');
|
||||||
|
|
||||||
|
items.at(2).simulate('click');
|
||||||
|
expect(setErrorCorrection).toHaveBeenCalledWith('Q');
|
||||||
|
|
||||||
|
items.at(3).simulate('click');
|
||||||
|
expect(setErrorCorrection).toHaveBeenCalledWith('H');
|
||||||
|
});
|
||||||
|
});
|
37
test/short-urls/helpers/qr-codes/QrFormatDropdown.test.tsx
Normal file
37
test/short-urls/helpers/qr-codes/QrFormatDropdown.test.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
|
import { DropdownItem } from 'reactstrap';
|
||||||
|
import { QrCodeFormat } from '../../../../src/utils/helpers/qrCodes';
|
||||||
|
import { QrFormatDropdown } from '../../../../src/short-urls/helpers/qr-codes/QrFormatDropdown';
|
||||||
|
|
||||||
|
describe('<QrFormatDropdown />', () => {
|
||||||
|
const initialFormat: QrCodeFormat = 'svg';
|
||||||
|
const setFormat = jest.fn();
|
||||||
|
let wrapper: ShallowWrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = shallow(<QrFormatDropdown format={initialFormat} setFormat={setFormat} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => wrapper?.unmount());
|
||||||
|
afterEach(jest.clearAllMocks);
|
||||||
|
|
||||||
|
it('renders initial state', () => {
|
||||||
|
const items = wrapper.find(DropdownItem);
|
||||||
|
|
||||||
|
expect(wrapper.prop('text')).toEqual('Format (svg)');
|
||||||
|
expect(items.at(0).prop('active')).toEqual(false);
|
||||||
|
expect(items.at(1).prop('active')).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes callback when items are clicked', () => {
|
||||||
|
const items = wrapper.find(DropdownItem);
|
||||||
|
|
||||||
|
expect(setFormat).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
items.at(0).simulate('click');
|
||||||
|
expect(setFormat).toHaveBeenCalledWith('png');
|
||||||
|
|
||||||
|
items.at(1).simulate('click');
|
||||||
|
expect(setFormat).toHaveBeenCalledWith('svg');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,68 +1,74 @@
|
||||||
import { buildQrCodeUrl, QrCodeFormat } from '../../../src/utils/helpers/qrCodes';
|
import { buildQrCodeUrl, QrCodeFormat, QrErrorCorrection } from '../../../src/utils/helpers/qrCodes';
|
||||||
|
|
||||||
describe('qrCodes', () => {
|
describe('qrCodes', () => {
|
||||||
describe('buildQrCodeUrl', () => {
|
describe('buildQrCodeUrl', () => {
|
||||||
test.each([
|
test.each([
|
||||||
[
|
[
|
||||||
'foo.com',
|
'foo.com',
|
||||||
{ size: 530, format: 'svg' as QrCodeFormat, margin: 0 },
|
{ size: 530, format: 'svg' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
|
||||||
{ useSizeInPath: true, svgIsSupported: true, marginIsSupported: false },
|
{ useSizeInPath: true, svgIsSupported: true, marginIsSupported: false, errorCorrectionIsSupported: false },
|
||||||
'foo.com/qr-code/530?format=svg',
|
'foo.com/qr-code/530?format=svg',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'foo.com',
|
'foo.com',
|
||||||
{ size: 530, format: 'png' as QrCodeFormat, margin: 0 },
|
{ size: 530, format: 'png' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
|
||||||
{ useSizeInPath: true, svgIsSupported: true, marginIsSupported: false },
|
{ useSizeInPath: true, svgIsSupported: true, marginIsSupported: false, errorCorrectionIsSupported: false },
|
||||||
'foo.com/qr-code/530?format=png',
|
'foo.com/qr-code/530?format=png',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'bar.io',
|
'bar.io',
|
||||||
{ size: 870, format: 'svg' as QrCodeFormat, margin: 0 },
|
{ size: 870, format: 'svg' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
|
||||||
{ useSizeInPath: false, svgIsSupported: false, marginIsSupported: false },
|
{ useSizeInPath: false, svgIsSupported: false, marginIsSupported: false, errorCorrectionIsSupported: false },
|
||||||
'bar.io/qr-code?size=870',
|
'bar.io/qr-code?size=870',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'bar.io',
|
'bar.io',
|
||||||
{ size: 200, format: 'png' as QrCodeFormat, margin: 0 },
|
{ size: 200, format: 'png' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
|
||||||
{ useSizeInPath: false, svgIsSupported: true, marginIsSupported: false },
|
{ useSizeInPath: false, svgIsSupported: true, marginIsSupported: false, errorCorrectionIsSupported: false },
|
||||||
'bar.io/qr-code?size=200&format=png',
|
'bar.io/qr-code?size=200&format=png',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'bar.io',
|
'bar.io',
|
||||||
{ size: 200, format: 'svg' as QrCodeFormat, margin: 0 },
|
{ size: 200, format: 'svg' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
|
||||||
{ useSizeInPath: false, svgIsSupported: true, marginIsSupported: false },
|
{ useSizeInPath: false, svgIsSupported: true, marginIsSupported: false, errorCorrectionIsSupported: false },
|
||||||
'bar.io/qr-code?size=200&format=svg',
|
'bar.io/qr-code?size=200&format=svg',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'foo.net',
|
'foo.net',
|
||||||
{ size: 480, format: 'png' as QrCodeFormat, margin: 0 },
|
{ size: 480, format: 'png' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
|
||||||
{ useSizeInPath: true, svgIsSupported: false, marginIsSupported: false },
|
{ useSizeInPath: true, svgIsSupported: false, marginIsSupported: false, errorCorrectionIsSupported: false },
|
||||||
'foo.net/qr-code/480',
|
'foo.net/qr-code/480',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'foo.net',
|
'foo.net',
|
||||||
{ size: 480, format: 'svg' as QrCodeFormat, margin: 0 },
|
{ size: 480, format: 'svg' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
|
||||||
{ useSizeInPath: true, svgIsSupported: false, marginIsSupported: false },
|
{ useSizeInPath: true, svgIsSupported: false, marginIsSupported: false, errorCorrectionIsSupported: false },
|
||||||
'foo.net/qr-code/480',
|
'foo.net/qr-code/480',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'shlink.io',
|
'shlink.io',
|
||||||
{ size: 123, format: 'svg' as QrCodeFormat, margin: 10 },
|
{ size: 123, format: 'svg' as QrCodeFormat, margin: 10, errorCorrection: 'L' as QrErrorCorrection },
|
||||||
{ useSizeInPath: true, svgIsSupported: false, marginIsSupported: false },
|
{ useSizeInPath: true, svgIsSupported: false, marginIsSupported: false, errorCorrectionIsSupported: false },
|
||||||
'shlink.io/qr-code/123',
|
'shlink.io/qr-code/123',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'shlink.io',
|
'shlink.io',
|
||||||
{ size: 456, format: 'png' as QrCodeFormat, margin: 10 },
|
{ size: 456, format: 'png' as QrCodeFormat, margin: 10, errorCorrection: 'L' as QrErrorCorrection },
|
||||||
{ useSizeInPath: true, svgIsSupported: true, marginIsSupported: true },
|
{ useSizeInPath: true, svgIsSupported: true, marginIsSupported: true, errorCorrectionIsSupported: false },
|
||||||
'shlink.io/qr-code/456?format=png&margin=10',
|
'shlink.io/qr-code/456?format=png&margin=10',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'shlink.io',
|
'shlink.io',
|
||||||
{ size: 456, format: 'png' as QrCodeFormat, margin: 0 },
|
{ size: 456, format: 'png' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
|
||||||
{ useSizeInPath: true, svgIsSupported: true, marginIsSupported: true },
|
{ useSizeInPath: true, svgIsSupported: true, marginIsSupported: true, errorCorrectionIsSupported: false },
|
||||||
'shlink.io/qr-code/456?format=png',
|
'shlink.io/qr-code/456?format=png',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'shlink.io',
|
||||||
|
{ size: 456, format: 'png' as QrCodeFormat, margin: 0, errorCorrection: 'H' as QrErrorCorrection },
|
||||||
|
{ useSizeInPath: true, svgIsSupported: true, marginIsSupported: true, errorCorrectionIsSupported: true },
|
||||||
|
'shlink.io/qr-code/456?format=png&errorCorrection=H',
|
||||||
|
],
|
||||||
])('builds expected URL based in params', (shortUrl, options, capabilities, expectedUrl) => {
|
])('builds expected URL based in params', (shortUrl, options, capabilities, expectedUrl) => {
|
||||||
expect(buildQrCodeUrl(shortUrl, options, capabilities)).toEqual(expectedUrl);
|
expect(buildQrCodeUrl(shortUrl, options, capabilities)).toEqual(expectedUrl);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue