Repository: shlinkio/shlink-web-client (ts)

Issue:
Drop support for Shlink older than v2.4.0
<!--
Before opening an issue, just take into account that this is a completely free of charge and open source project.
I'm always happy to help and provide support, but some understanding will be expected.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
You may also be asked to provide tests or ways to reproduce reported bugs.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
-->

Remove version checks for features older than Shlink 2.4.0

Pull request description:
Feature/remove old shlink
Closes #491 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 958c0192..0a733642 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@ 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).
 
-## [Unreleased]
+## [3.3.0] - 2021-09-25
 ### Added
 * [#465](https://github.com/shlinkio/shlink-web-client/issues/465) Added new page to manage domains and their redirects, when consuming Shlink 2.8 or higher.
 * [#460](https://github.com/shlinkio/shlink-web-client/issues/460) Added dynamic title on hover for tags with a very long title.
@@ -31,7 +31,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 * *Nothing*
 
 ### Removed
-* *Nothing*
+* [#491](https://github.com/shlinkio/shlink-web-client/issues/491) Dropped support for Shlink older than v2.4.0.
 
 ### Fixed
 * *Nothing*
diff --git a/src/common/MenuLayout.tsx b/src/common/MenuLayout.tsx
index 871a0f97..6672583e 100644
--- a/src/common/MenuLayout.tsx
+++ b/src/common/MenuLayout.tsx
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import classNames from 'classnames';
 import { withSelectedServer } from '../servers/helpers/withSelectedServer';
 import { useSwipeable, useToggle } from '../utils/helpers/hooks';
-import { supportsDomainRedirects, supportsOrphanVisits, supportsTagVisits } from '../utils/helpers/features';
+import { supportsDomainRedirects, supportsOrphanVisits } from '../utils/helpers/features';
 import { isReachableServer } from '../servers/data';
 import NotFound from './NotFound';
 import { AsideMenuProps } from './AsideMenu';
@@ -32,7 +32,6 @@ const MenuLayout = (
     return <ServerError />;
   }
 
-  const addTagsVisitsRoute = supportsTagVisits(selectedServer);
   const addOrphanVisitsRoute = supportsOrphanVisits(selectedServer);
   const addManageDomainsRoute = supportsDomainRedirects(selectedServer);
   const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible });
@@ -54,7 +53,7 @@ const MenuLayout = (
                 <Route exact path="/server/:serverId/create-short-url" component={CreateShortUrl} />
                 <Route path="/server/:serverId/short-code/:shortCode/visits" component={ShortUrlVisits} />
                 <Route path="/server/:serverId/short-code/:shortCode/edit" component={EditShortUrl} />
-                {addTagsVisitsRoute && <Route path="/server/:serverId/tag/:tag/visits" component={TagVisits} />}
+                <Route path="/server/:serverId/tag/:tag/visits" component={TagVisits} />
                 {addOrphanVisitsRoute && <Route path="/server/:serverId/orphan-visits" component={OrphanVisits} />}
                 <Route exact path="/server/:serverId/manage-tags" component={TagsList} />
                 {addManageDomainsRoute && <Route exact path="/server/:serverId/manage-domains" component={ManageDomains} />}
diff --git a/src/servers/Overview.tsx b/src/servers/Overview.tsx
index 84614b2e..99311133 100644
--- a/src/servers/Overview.tsx
+++ b/src/servers/Overview.tsx
@@ -55,14 +55,7 @@ export const Overview = (
         <div className="col-md-6 col-xl-3">
           <Card className="overview__card mb-3" body>
             <CardTitle tag="h5" className="overview__card-title">Visits</CardTitle>
-            <CardText tag="h2">
-              <ForServerVersion minVersion="2.2.0">
-                {loadingVisits ? 'Loading...' : prettify(visitsCount)}
-              </ForServerVersion>
-              <ForServerVersion maxVersion="2.1.*">
-                <small className="text-muted"><i>Shlink 2.2 is needed</i></small>
-              </ForServerVersion>
-            </CardText>
+            <CardText tag="h2">{loadingVisits ? 'Loading...' : prettify(visitsCount)}</CardText>
           </Card>
         </div>
         <div className="col-md-6 col-xl-3">
diff --git a/src/short-urls/ShortUrlForm.tsx b/src/short-urls/ShortUrlForm.tsx
index 96fc6eea..b09a70cb 100644
--- a/src/short-urls/ShortUrlForm.tsx
+++ b/src/short-urls/ShortUrlForm.tsx
@@ -5,13 +5,7 @@ import { isEmpty, pipe, replace, trim } from 'ramda';
 import classNames from 'classnames';
 import { parseISO } from 'date-fns';
 import DateInput, { DateInputProps } from '../utils/DateInput';
-import {
-  supportsCrawlableVisits,
-  supportsListingDomains,
-  supportsSettingShortCodeLength,
-  supportsShortUrlTitle,
-  supportsValidateUrl,
-} from '../utils/helpers/features';
+import { supportsCrawlableVisits, supportsShortUrlTitle } from '../utils/helpers/features';
 import { SimpleCard } from '../utils/SimpleCard';
 import { handleEventPreventingDefault, hasValue } from '../utils/utils';
 import Checkbox from '../utils/Checkbox';
@@ -102,17 +96,13 @@ export const ShortUrlForm = (
     </>
   );
 
-  const showDomainSelector = supportsListingDomains(selectedServer);
-  const disableShortCodeLength = !supportsSettingShortCodeLength(selectedServer);
   const supportsTitle = supportsShortUrlTitle(selectedServer);
   const showCustomizeCard = supportsTitle || !isEdit;
   const limitAccessCardClasses = classNames('mb-3', {
     'col-sm-6': showCustomizeCard,
     'col-sm-12': !showCustomizeCard,
   });
-  const showValidateUrl = supportsValidateUrl(selectedServer);
   const showCrawlableControl = supportsCrawlableVisits(selectedServer);
-  const showExtraValidationsCard = showValidateUrl || showCrawlableControl || !isEdit;
 
   return (
     <form className="short-url-form" onSubmit={submit}>
@@ -139,22 +129,16 @@ export const ShortUrlForm = (
                         <div className="col-lg-6">
                           {renderOptionalInput('shortCodeLength', 'Short code length', 'number', {
                             min: 4,
-                            disabled: disableShortCodeLength || hasValue(shortUrlData.customSlug),
-                            ...disableShortCodeLength && {
-                              title: 'Shlink 2.1.0 or higher is required to be able to provide the short code length',
-                            },
+                            disabled: hasValue(shortUrlData.customSlug),
                           })}
                         </div>
                       </Row>
-                      {!showDomainSelector && renderOptionalInput('domain', 'Domain', 'text')}
-                      {showDomainSelector && (
-                        <FormGroup>
-                          <DomainSelector
-                            value={shortUrlData.domain}
-                            onChange={(domain?: string) => setShortUrlData({ ...shortUrlData, domain })}
-                          />
-                        </FormGroup>
-                      )}
+                      <FormGroup>
+                        <DomainSelector
+                          value={shortUrlData.domain}
+                          onChange={(domain?: string) => setShortUrlData({ ...shortUrlData, domain })}
+                        />
+                      </FormGroup>
                     </>
                   )}
                 </SimpleCard>
@@ -170,41 +154,37 @@ export const ShortUrlForm = (
             </div>
           </Row>
 
-          {showExtraValidationsCard && (
-            <SimpleCard title="Extra checks" className="mb-3">
-              {showValidateUrl && (
-                <ShortUrlFormCheckboxGroup
-                  infoTooltip="If checked, Shlink will try to reach the long URL, failing in case it's not publicly accessible."
-                  checked={shortUrlData.validateUrl}
-                  onChange={(validateUrl) => setShortUrlData({ ...shortUrlData, validateUrl })}
-                >
-                  Validate URL
-                </ShortUrlFormCheckboxGroup>
-              )}
-              {showCrawlableControl && (
-                <ShortUrlFormCheckboxGroup
-                  infoTooltip="This short URL will be included in the robots.txt for your Shlink instance, allowing web crawlers (like Google) to index it."
-                  checked={shortUrlData.crawlable}
-                  onChange={(crawlable) => setShortUrlData({ ...shortUrlData, crawlable })}
+          <SimpleCard title="Extra checks" className="mb-3">
+            <ShortUrlFormCheckboxGroup
+              infoTooltip="If checked, Shlink will try to reach the long URL, failing in case it's not publicly accessible."
+              checked={shortUrlData.validateUrl}
+              onChange={(validateUrl) => setShortUrlData({ ...shortUrlData, validateUrl })}
+            >
+              Validate URL
+            </ShortUrlFormCheckboxGroup>
+            {showCrawlableControl && (
+              <ShortUrlFormCheckboxGroup
+                infoTooltip="This short URL will be included in the robots.txt for your Shlink instance, allowing web crawlers (like Google) to index it."
+                checked={shortUrlData.crawlable}
+                onChange={(crawlable) => setShortUrlData({ ...shortUrlData, crawlable })}
+              >
+                Make it crawlable
+              </ShortUrlFormCheckboxGroup>
+            )}
+            {!isEdit && (
+              <p>
+                <Checkbox
+                  inline
+                  className="mr-2"
+                  checked={shortUrlData.findIfExists}
+                  onChange={(findIfExists) => setShortUrlData({ ...shortUrlData, findIfExists })}
                 >
-                  Make it crawlable
-                </ShortUrlFormCheckboxGroup>
-              )}
-              {!isEdit && (
-                <p>
-                  <Checkbox
-                    inline
-                    className="mr-2"
-                    checked={shortUrlData.findIfExists}
-                    onChange={(findIfExists) => setShortUrlData({ ...shortUrlData, findIfExists })}
-                  >
-                    Use existing URL if found
-                  </Checkbox>
-                  <UseExistingIfFoundInfoIcon />
-                </p>
-              )}
-            </SimpleCard>
-          )}
+                  Use existing URL if found
+                </Checkbox>
+                <UseExistingIfFoundInfoIcon />
+              </p>
+            )}
+          </SimpleCard>
         </>
       )}
 
diff --git a/src/short-urls/helpers/QrCodeModal.tsx b/src/short-urls/helpers/QrCodeModal.tsx
index 6521e5e1..053c5e11 100644
--- a/src/short-urls/helpers/QrCodeModal.tsx
+++ b/src/short-urls/helpers/QrCodeModal.tsx
@@ -10,7 +10,6 @@ import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
 import { buildQrCodeUrl, QrCodeCapabilities, QrCodeFormat, QrErrorCorrection } from '../../utils/helpers/qrCodes';
 import {
   supportsQrCodeSizeInQuery,
-  supportsQrCodeSvgFormat,
   supportsQrCodeMargin,
   supportsQrErrorCorrection,
 } from '../../utils/helpers/features';
@@ -33,10 +32,10 @@ const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Vers
   const [ errorCorrection, setErrorCorrection ] = useState<QrErrorCorrection>('L');
   const capabilities: QrCodeCapabilities = useMemo(() => ({
     useSizeInPath: !supportsQrCodeSizeInQuery(selectedServer),
-    svgIsSupported: supportsQrCodeSvgFormat(selectedServer),
     marginIsSupported: supportsQrCodeMargin(selectedServer),
     errorCorrectionIsSupported: supportsQrErrorCorrection(selectedServer),
   }), [ selectedServer ]);
+  const willRenderThreeControls = capabilities.marginIsSupported !== capabilities.errorCorrectionIsSupported;
   const qrCodeUrl = useMemo(
     () => buildQrCodeUrl(shortUrl, { size, format, margin, errorCorrection }, capabilities),
     [ shortUrl, size, format, margin, errorCorrection, capabilities ],
@@ -58,11 +57,7 @@ const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Vers
       <ModalBody>
         <Row>
           <FormGroup
-            className={classNames({
-              'col-md-4': capabilities.marginIsSupported && capabilities.svgIsSupported && !capabilities.errorCorrectionIsSupported,
-              'col-md-6': capabilities.errorCorrectionIsSupported || (!capabilities.marginIsSupported && capabilities.svgIsSupported) || (capabilities.marginIsSupported && !capabilities.svgIsSupported),
-              'col-12': !capabilities.marginIsSupported && !capabilities.svgIsSupported && !capabilities.errorCorrectionIsSupported,
-            })}
+            className={classNames({ 'col-md-4': willRenderThreeControls, 'col-md-6': !willRenderThreeControls })}
           >
             <label className="mb-0">Size: {size}px</label>
             <input
@@ -76,7 +71,7 @@ const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Vers
             />
           </FormGroup>
           {capabilities.marginIsSupported && (
-            <FormGroup className={capabilities.svgIsSupported && !capabilities.errorCorrectionIsSupported ? 'col-md-4' : 'col-md-6'}>
+            <FormGroup className={willRenderThreeControls ? 'col-md-4' : 'col-md-6'}>
               <label className="mb-0">Margin: {margin}px</label>
               <input
                 type="range"
@@ -89,11 +84,9 @@ const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Vers
               />
             </FormGroup>
           )}
-          {capabilities.svgIsSupported && (
-            <FormGroup className={capabilities.marginIsSupported && !capabilities.errorCorrectionIsSupported ? 'col-md-4' : 'col-md-6'}>
-              <QrFormatDropdown format={format} setFormat={setFormat} />
-            </FormGroup>
-          )}
+          <FormGroup className={willRenderThreeControls ? 'col-md-4' : 'col-md-6'}>
+            <QrFormatDropdown format={format} setFormat={setFormat} />
+          </FormGroup>
           {capabilities.errorCorrectionIsSupported && (
             <FormGroup className="col-md-6">
               <QrErrorCorrectionDropdown errorCorrection={errorCorrection} setErrorCorrection={setErrorCorrection} />
diff --git a/src/tags/TagCard.tsx b/src/tags/TagCard.tsx
index 6bad3281..e3cd220e 100644
--- a/src/tags/TagCard.tsx
+++ b/src/tags/TagCard.tsx
@@ -5,7 +5,6 @@ import { FC, useEffect, useRef } from 'react';
 import { Link } from 'react-router-dom';
 import { prettify } from '../utils/helpers/numbers';
 import { useToggle } from '../utils/helpers/hooks';
-import { Versions } from '../utils/helpers/version';
 import ColorGenerator from '../utils/services/ColorGenerator';
 import { isServerWithId, SelectedServer } from '../servers/data';
 import TagBullet from './helpers/TagBullet';
@@ -25,16 +24,13 @@ const isTruncated = (el: HTMLElement | undefined): boolean => !!el && el.scrollW
 const TagCard = (
   DeleteTagConfirmModal: FC<TagModalProps>,
   EditTagModal: FC<TagModalProps>,
-  ForServerVersion: FC<Versions>,
   colorGenerator: ColorGenerator,
 ) => ({ tag, tagStats, selectedServer, displayed, toggle }: TagCardProps) => {
   const [ isDeleteModalOpen, toggleDelete ] = useToggle();
   const [ isEditModalOpen, toggleEdit ] = useToggle();
   const [ hasTitle,, displayTitle ] = useToggle();
   const titleRef = useRef<HTMLElement>();
-
   const serverId = isServerWithId(selectedServer) ? selectedServer.id : '';
-  const shortUrlsLink = `/server/${serverId}/list-short-urls/1?tag=${encodeURIComponent(tag)}`;
 
   useEffect(() => {
     if (isTruncated(titleRef.current)) {
@@ -59,12 +55,7 @@ const TagCard = (
           }}
         >
           <TagBullet tag={tag} colorGenerator={colorGenerator} />
-          <ForServerVersion minVersion="2.2.0">
-            <span className="tag-card__tag-name" onClick={toggle}>{tag}</span>
-          </ForServerVersion>
-          <ForServerVersion maxVersion="2.1.*">
-            <Link to={shortUrlsLink}>{tag}</Link>
-          </ForServerVersion>
+          <span className="tag-card__tag-name" onClick={toggle}>{tag}</span>
         </h5>
       </CardHeader>
 
@@ -72,7 +63,7 @@ const TagCard = (
         <Collapse isOpen={displayed}>
           <CardBody className="tag-card__body">
             <Link
-              to={shortUrlsLink}
+              to={`/server/${serverId}/list-short-urls/1?tag=${encodeURIComponent(tag)}`}
               className="btn btn-outline-secondary btn-block d-flex justify-content-between align-items-center mb-1"
             >
               <span className="text-ellipsis"><FontAwesomeIcon icon={faLink} className="mr-2" />Short URLs</span>
diff --git a/src/tags/services/provideServices.ts b/src/tags/services/provideServices.ts
index ac1753dc..1e3e8d39 100644
--- a/src/tags/services/provideServices.ts
+++ b/src/tags/services/provideServices.ts
@@ -18,14 +18,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
   bottle.serviceFactory('TagsSelector', TagsSelector, 'ColorGenerator');
   bottle.decorator('TagsSelector', connect([ 'tagsList', 'settings' ], [ 'listTags' ]));
 
-  bottle.serviceFactory(
-    'TagCard',
-    TagCard,
-    'DeleteTagConfirmModal',
-    'EditTagModal',
-    'ForServerVersion',
-    'ColorGenerator',
-  );
+  bottle.serviceFactory('TagCard', TagCard, 'DeleteTagConfirmModal', 'EditTagModal', 'ColorGenerator');
 
   bottle.serviceFactory('DeleteTagConfirmModal', () => DeleteTagConfirmModal);
   bottle.decorator('DeleteTagConfirmModal', connect([ 'tagDelete' ], [ 'deleteTag', 'tagDeleted' ]));
diff --git a/src/utils/helpers/features.ts b/src/utils/helpers/features.ts
index 68651fcc..120926f8 100644
--- a/src/utils/helpers/features.ts
+++ b/src/utils/helpers/features.ts
@@ -4,16 +4,6 @@ import { versionMatch, Versions } from './version';
 const serverMatchesVersions = (versions: Versions) => (selectedServer: SelectedServer): boolean =>
   isReachableServer(selectedServer) && versionMatch(selectedServer.version, versions);
 
-export const supportsSettingShortCodeLength = serverMatchesVersions({ minVersion: '2.1.0' });
-
-export const supportsTagVisits = serverMatchesVersions({ minVersion: '2.2.0' });
-
-export const supportsListingDomains = serverMatchesVersions({ minVersion: '2.4.0' });
-
-export const supportsQrCodeSvgFormat = supportsListingDomains;
-
-export const supportsValidateUrl = supportsListingDomains;
-
 export const supportsQrCodeSizeInQuery = serverMatchesVersions({ minVersion: '2.5.0' });
 
 export const supportsShortUrlTitle = serverMatchesVersions({ minVersion: '2.6.0' });
diff --git a/src/utils/helpers/qrCodes.ts b/src/utils/helpers/qrCodes.ts
index fb94350a..c096f148 100644
--- a/src/utils/helpers/qrCodes.ts
+++ b/src/utils/helpers/qrCodes.ts
@@ -3,7 +3,6 @@ import { stringifyQuery } from './query';
 
 export interface QrCodeCapabilities {
   useSizeInPath: boolean;
-  svgIsSupported: boolean;
   marginIsSupported: boolean;
   errorCorrectionIsSupported: boolean;
 }
@@ -22,12 +21,12 @@ export interface QrCodeOptions {
 export const buildQrCodeUrl = (
   shortUrl: string,
   { size, format, margin, errorCorrection }: QrCodeOptions,
-  { useSizeInPath, svgIsSupported, marginIsSupported, errorCorrectionIsSupported }: QrCodeCapabilities,
+  { useSizeInPath, marginIsSupported, errorCorrectionIsSupported }: QrCodeCapabilities,
 ): string => {
   const baseUrl = `${shortUrl}/qr-code${useSizeInPath ? `/${size}` : ''}`;
   const query = stringifyQuery({
     size: useSizeInPath ? undefined : size,
-    format: svgIsSupported ? format : undefined,
+    format,
     margin: marginIsSupported && margin > 0 ? margin : undefined,
     errorCorrection: errorCorrectionIsSupported ? errorCorrection : undefined,
   });


Test patch:
diff --git a/test/common/MenuLayout.test.tsx b/test/common/MenuLayout.test.tsx
index 439bb81f..e45dfb3d 100644
--- a/test/common/MenuLayout.test.tsx
+++ b/test/common/MenuLayout.test.tsx
@@ -49,8 +49,6 @@ describe('<MenuLayout />', () => {
   });
 
   it.each([
-    [ '2.1.0' as SemVer, 7 ],
-    [ '2.2.0' as SemVer, 8 ],
     [ '2.5.0' as SemVer, 8 ],
     [ '2.6.0' as SemVer, 9 ],
     [ '2.7.0' as SemVer, 9 ],
diff --git a/test/servers/Overview.test.tsx b/test/servers/Overview.test.tsx
index 3b29b3e4..14cc85b1 100644
--- a/test/servers/Overview.test.tsx
+++ b/test/servers/Overview.test.tsx
@@ -64,13 +64,6 @@ describe('<Overview />', () => {
     expect(cards.at(3).html()).toContain(prettify(3));
   });
 
-  it('displays warning in first card for old shlink versions', () => {
-    const wrapper = createWrapper();
-    const firstCard = wrapper.find(CardText).first();
-
-    expect(firstCard.html()).toContain('Shlink 2.2 is needed');
-  });
-
   it('nests complex components', () => {
     const wrapper = createWrapper();
 
diff --git a/test/short-urls/ShortUrlForm.test.tsx b/test/short-urls/ShortUrlForm.test.tsx
index 380d1309..66ae1fc1 100644
--- a/test/short-urls/ShortUrlForm.test.tsx
+++ b/test/short-urls/ShortUrlForm.test.tsx
@@ -13,9 +13,10 @@ import { parseDate } from '../../src/utils/helpers/date';
 describe('<ShortUrlForm />', () => {
   let wrapper: ShallowWrapper;
   const TagsSelector = () => null;
+  const DomainSelector = () => null;
   const createShortUrl = jest.fn(async () => Promise.resolve());
   const createWrapper = (selectedServer: SelectedServer = null, mode: Mode = 'create') => {
-    const ShortUrlForm = createShortUrlForm(TagsSelector, () => null);
+    const ShortUrlForm = createShortUrlForm(TagsSelector, DomainSelector);
 
     wrapper = shallow(
       <ShortUrlForm
@@ -41,7 +42,7 @@ describe('<ShortUrlForm />', () => {
     wrapper.find(Input).first().simulate('change', { target: { value: 'https://long-domain.com/foo/bar' } });
     wrapper.find('TagsSelector').simulate('change', [ 'tag_foo', 'tag_bar' ]);
     wrapper.find('#customSlug').simulate('change', { target: { value: 'my-slug' } });
-    wrapper.find('#domain').simulate('change', { target: { value: 'example.com' } });
+    wrapper.find(DomainSelector).simulate('change', 'example.com');
     wrapper.find('#maxVisits').simulate('change', { target: { value: '20' } });
     wrapper.find('#shortCodeLength').simulate('change', { target: { value: 15 } });
     wrapper.find(DateInput).at(0).simulate('change', validSince);
@@ -68,12 +69,8 @@ describe('<ShortUrlForm />', () => {
     [ null, 'create-basic' as Mode, 0 ],
     [ Mock.of<ReachableServer>({ version: '2.6.0' }), 'create' as Mode, 4 ],
     [ Mock.of<ReachableServer>({ version: '2.5.0' }), 'create' as Mode, 4 ],
-    [ Mock.of<ReachableServer>({ version: '2.4.0' }), 'create' as Mode, 4 ],
-    [ Mock.of<ReachableServer>({ version: '2.3.0' }), 'create' as Mode, 4 ],
     [ Mock.of<ReachableServer>({ version: '2.6.0' }), 'edit' as Mode, 4 ],
     [ Mock.of<ReachableServer>({ version: '2.5.0' }), 'edit' as Mode, 3 ],
-    [ Mock.of<ReachableServer>({ version: '2.4.0' }), 'edit' as Mode, 3 ],
-    [ Mock.of<ReachableServer>({ version: '2.3.0' }), 'edit' as Mode, 2 ],
   ])(
     'renders expected amount of cards based on server capabilities and mode',
     (selectedServer, mode, expectedAmountOfCards) => {
diff --git a/test/short-urls/helpers/QrCodeModal.test.tsx b/test/short-urls/helpers/QrCodeModal.test.tsx
index aa8b5160..900e90e2 100644
--- a/test/short-urls/helpers/QrCodeModal.test.tsx
+++ b/test/short-urls/helpers/QrCodeModal.test.tsx
@@ -43,9 +43,6 @@ describe('<QrCodeModal />', () => {
   });
 
   it.each([
-    [ '2.3.0' as SemVer, 0, '/qr-code/300' ],
-    [ '2.4.0' as SemVer, 0, '/qr-code/300?format=png' ],
-    [ '2.4.0' as SemVer, 10, '/qr-code/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, 10, '/qr-code?size=300&format=png&margin=10' ],
@@ -90,8 +87,6 @@ describe('<QrCodeModal />', () => {
   });
 
   it.each([
-    [ '2.3.0' as SemVer, 0, 'col-12' ],
-    [ '2.4.0' as SemVer, 1, 'col-md-6' ],
     [ '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) => {
diff --git a/test/tags/TagCard.test.tsx b/test/tags/TagCard.test.tsx
index db1dce18..8bdd6c6c 100644
--- a/test/tags/TagCard.test.tsx
+++ b/test/tags/TagCard.test.tsx
@@ -14,7 +14,7 @@ describe('<TagCard />', () => {
   };
   const DeleteTagConfirmModal = jest.fn();
   const EditTagModal = jest.fn();
-  const TagCard = createTagCard(DeleteTagConfirmModal, EditTagModal, () => null, Mock.all<ColorGenerator>());
+  const TagCard = createTagCard(DeleteTagConfirmModal, EditTagModal, Mock.all<ColorGenerator>());
   const createWrapper = (tag = 'ssr') => {
     wrapper = shallow(
       <TagCard
@@ -65,9 +65,10 @@ describe('<TagCard />', () => {
   it('shows expected tag stats', () => {
     const links = wrapper.find(Link);
 
-    expect(links.at(1).prop('to')).toEqual('/server/1/list-short-urls/1?tag=ssr');
-    expect(links.at(1).text()).toContain('48');
-    expect(links.at(2).prop('to')).toEqual('/server/1/tag/ssr/visits');
-    expect(links.at(2).text()).toContain('23,257');
+    expect(links).toHaveLength(2);
+    expect(links.at(0).prop('to')).toEqual('/server/1/list-short-urls/1?tag=ssr');
+    expect(links.at(0).text()).toContain('48');
+    expect(links.at(1).prop('to')).toEqual('/server/1/tag/ssr/visits');
+    expect(links.at(1).text()).toContain('23,257');
   });
 });
diff --git a/test/utils/helpers/qrCodes.test.ts b/test/utils/helpers/qrCodes.test.ts
index c9d7bcbe..5a2edf22 100644
--- a/test/utils/helpers/qrCodes.test.ts
+++ b/test/utils/helpers/qrCodes.test.ts
@@ -6,67 +6,67 @@ describe('qrCodes', () => {
       [
         'foo.com',
         { size: 530, format: 'svg' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
-        { useSizeInPath: true, svgIsSupported: true, marginIsSupported: false, errorCorrectionIsSupported: false },
+        { useSizeInPath: true, marginIsSupported: false, errorCorrectionIsSupported: false },
         'foo.com/qr-code/530?format=svg',
       ],
       [
         'foo.com',
         { size: 530, format: 'png' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
-        { useSizeInPath: true, svgIsSupported: true, marginIsSupported: false, errorCorrectionIsSupported: false },
+        { useSizeInPath: true, marginIsSupported: false, errorCorrectionIsSupported: false },
         'foo.com/qr-code/530?format=png',
       ],
       [
         'bar.io',
         { size: 870, format: 'svg' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
-        { useSizeInPath: false, svgIsSupported: false, marginIsSupported: false, errorCorrectionIsSupported: false },
-        'bar.io/qr-code?size=870',
+        { useSizeInPath: false, marginIsSupported: false, errorCorrectionIsSupported: false },
+        'bar.io/qr-code?size=870&format=svg',
       ],
       [
         'bar.io',
         { size: 200, format: 'png' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
-        { useSizeInPath: false, svgIsSupported: true, marginIsSupported: false, errorCorrectionIsSupported: false },
+        { useSizeInPath: false, marginIsSupported: false, errorCorrectionIsSupported: false },
         'bar.io/qr-code?size=200&format=png',
       ],
       [
         'bar.io',
         { size: 200, format: 'svg' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
-        { useSizeInPath: false, svgIsSupported: true, marginIsSupported: false, errorCorrectionIsSupported: false },
+        { useSizeInPath: false, marginIsSupported: false, errorCorrectionIsSupported: false },
         'bar.io/qr-code?size=200&format=svg',
       ],
       [
         'foo.net',
         { size: 480, format: 'png' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
-        { useSizeInPath: true, svgIsSupported: false, marginIsSupported: false, errorCorrectionIsSupported: false },
-        'foo.net/qr-code/480',
+        { useSizeInPath: true, marginIsSupported: false, errorCorrectionIsSupported: false },
+        'foo.net/qr-code/480?format=png',
       ],
       [
         'foo.net',
         { size: 480, format: 'svg' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
-        { useSizeInPath: true, svgIsSupported: false, marginIsSupported: false, errorCorrectionIsSupported: false },
-        'foo.net/qr-code/480',
+        { useSizeInPath: true, marginIsSupported: false, errorCorrectionIsSupported: false },
+        'foo.net/qr-code/480?format=svg',
       ],
       [
         'shlink.io',
         { size: 123, format: 'svg' as QrCodeFormat, margin: 10, errorCorrection: 'L' as QrErrorCorrection },
-        { useSizeInPath: true, svgIsSupported: false, marginIsSupported: false, errorCorrectionIsSupported: false },
-        'shlink.io/qr-code/123',
+        { useSizeInPath: true, marginIsSupported: false, errorCorrectionIsSupported: false },
+        'shlink.io/qr-code/123?format=svg',
       ],
       [
         'shlink.io',
         { size: 456, format: 'png' as QrCodeFormat, margin: 10, errorCorrection: 'L' as QrErrorCorrection },
-        { useSizeInPath: true, svgIsSupported: true, marginIsSupported: true, errorCorrectionIsSupported: false },
+        { useSizeInPath: true, marginIsSupported: true, errorCorrectionIsSupported: false },
         'shlink.io/qr-code/456?format=png&margin=10',
       ],
       [
         'shlink.io',
         { size: 456, format: 'png' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
-        { useSizeInPath: true, svgIsSupported: true, marginIsSupported: true, errorCorrectionIsSupported: false },
+        { useSizeInPath: true, marginIsSupported: true, errorCorrectionIsSupported: false },
         '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 },
+        { useSizeInPath: true, marginIsSupported: true, errorCorrectionIsSupported: true },
         'shlink.io/qr-code/456?format=png&errorCorrection=H',
       ],
     ])('builds expected URL based in params', (shortUrl, options, capabilities, expectedUrl) => {