mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-14 03:23:49 +00:00
Merge pull request #183 from acelaya-forks/feature/support-shlink-2
Feature/support shlink 2
This commit is contained in:
@@ -28,6 +28,7 @@
|
|||||||
"no-warning-comments": "off",
|
"no-warning-comments": "off",
|
||||||
"no-magic-numbers": "off",
|
"no-magic-numbers": "off",
|
||||||
"no-undefined": "off",
|
"no-undefined": "off",
|
||||||
|
"no-inline-comments": "off",
|
||||||
"indent": ["error", 2, {
|
"indent": ["error", 2, {
|
||||||
"SwitchCase": 1
|
"SwitchCase": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
|
|
||||||
* *Nothing*
|
* [#174](https://github.com/shlinkio/shlink-web-client/issues/174) Added complete support for Shlink v2.x together with currently supported Shlink versions.
|
||||||
|
|
||||||
#### Changed
|
#### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as PropTypes from 'prop-types';
|
|||||||
import './ErrorHandler.scss';
|
import './ErrorHandler.scss';
|
||||||
import { Button } from 'reactstrap';
|
import { Button } from 'reactstrap';
|
||||||
|
|
||||||
|
// FIXME Replace with typescript: (window, console)
|
||||||
const ErrorHandler = ({ location }, { error }) => class ErrorHandler extends React.Component {
|
const ErrorHandler = ({ location }, { error }) => class ErrorHandler extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { identity } from 'ramda';
|
|||||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||||
import { shortUrlDeletionType } from '../reducers/shortUrlDeletion';
|
import { shortUrlDeletionType } from '../reducers/shortUrlDeletion';
|
||||||
|
|
||||||
|
const THRESHOLD_REACHED = 'INVALID_SHORTCODE_DELETION';
|
||||||
|
|
||||||
export default class DeleteShortUrlModal extends React.Component {
|
export default class DeleteShortUrlModal extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
shortUrl: shortUrlType,
|
shortUrl: shortUrlType,
|
||||||
@@ -39,9 +41,10 @@ export default class DeleteShortUrlModal extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { shortUrl, toggle, isOpen, shortUrlDeletion } = this.props;
|
const { shortUrl, toggle, isOpen, shortUrlDeletion } = this.props;
|
||||||
const THRESHOLD_REACHED = 'INVALID_SHORTCODE_DELETION';
|
const { error, errorData } = shortUrlDeletion;
|
||||||
const hasThresholdError = shortUrlDeletion.error && shortUrlDeletion.errorData.error === THRESHOLD_REACHED;
|
const errorCode = error && (errorData.type || errorData.error);
|
||||||
const hasErrorOtherThanThreshold = shortUrlDeletion.error && shortUrlDeletion.errorData.error !== THRESHOLD_REACHED;
|
const hasThresholdError = errorCode === THRESHOLD_REACHED;
|
||||||
|
const hasErrorOtherThanThreshold = error && errorCode !== THRESHOLD_REACHED;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} toggle={toggle} centered>
|
<Modal isOpen={isOpen} toggle={toggle} centered>
|
||||||
@@ -63,7 +66,8 @@ export default class DeleteShortUrlModal extends React.Component {
|
|||||||
|
|
||||||
{hasThresholdError && (
|
{hasThresholdError && (
|
||||||
<div className="p-2 mt-2 bg-warning text-center">
|
<div className="p-2 mt-2 bg-warning text-center">
|
||||||
This short URL has received too many visits and therefore, it cannot be deleted
|
{errorData.threshold && `This short URL has received more than ${errorData.threshold} visits, and therefore, it cannot be deleted.`}
|
||||||
|
{!errorData.threshold && 'This short URL has received too many visits, and therefore, it cannot be deleted.'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hasErrorOtherThanThreshold && (
|
{hasErrorOtherThanThreshold && (
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import { CopyToClipboard } from 'react-copy-to-clipboard';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { ButtonDropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
import { ButtonDropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { isEmpty } from 'ramda';
|
||||||
import { serverType } from '../../servers/prop-types';
|
import { serverType } from '../../servers/prop-types';
|
||||||
|
import { compareVersions } from '../../utils/utils';
|
||||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||||
import PreviewModal from './PreviewModal';
|
import PreviewModal from './PreviewModal';
|
||||||
import QrCodeModal from './QrCodeModal';
|
import QrCodeModal from './QrCodeModal';
|
||||||
@@ -37,6 +39,8 @@ const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrls
|
|||||||
render() {
|
render() {
|
||||||
const { onCopyToClipboard, shortUrl, selectedServer } = this.props;
|
const { onCopyToClipboard, shortUrl, selectedServer } = this.props;
|
||||||
const completeShortUrl = shortUrl && shortUrl.shortUrl ? shortUrl.shortUrl : '';
|
const completeShortUrl = shortUrl && shortUrl.shortUrl ? shortUrl.shortUrl : '';
|
||||||
|
const currentServerVersion = selectedServer ? selectedServer.version : '';
|
||||||
|
const showPreviewBtn = !isEmpty(currentServerVersion) && compareVersions(currentServerVersion, '<', '2.0.0');
|
||||||
const toggleModal = (prop) => () => this.setState((prevState) => ({ [prop]: !prevState[prop] }));
|
const toggleModal = (prop) => () => this.setState((prevState) => ({ [prop]: !prevState[prop] }));
|
||||||
const toggleQrCode = toggleModal('isQrModalOpen');
|
const toggleQrCode = toggleModal('isQrModalOpen');
|
||||||
const togglePreview = toggleModal('isPreviewModalOpen');
|
const togglePreview = toggleModal('isPreviewModalOpen');
|
||||||
@@ -70,17 +74,21 @@ const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrls
|
|||||||
|
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
|
|
||||||
<DropdownItem onClick={togglePreview}>
|
{showPreviewBtn && (
|
||||||
<FontAwesomeIcon icon={pictureIcon} /> Preview
|
<React.Fragment>
|
||||||
</DropdownItem>
|
<DropdownItem onClick={togglePreview}>
|
||||||
<PreviewModal url={completeShortUrl} isOpen={this.state.isPreviewModalOpen} toggle={togglePreview} />
|
<FontAwesomeIcon icon={pictureIcon} /> Preview
|
||||||
|
</DropdownItem>
|
||||||
|
<PreviewModal url={completeShortUrl} isOpen={this.state.isPreviewModalOpen} toggle={togglePreview} />
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
<DropdownItem onClick={toggleQrCode}>
|
<DropdownItem onClick={toggleQrCode}>
|
||||||
<FontAwesomeIcon icon={qrIcon} /> QR code
|
<FontAwesomeIcon icon={qrIcon} /> QR code
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<QrCodeModal url={completeShortUrl} isOpen={this.state.isQrModalOpen} toggle={toggleQrCode} />
|
<QrCodeModal url={completeShortUrl} isOpen={this.state.isQrModalOpen} toggle={toggleQrCode} />
|
||||||
|
|
||||||
<DropdownItem divider />
|
{showPreviewBtn && <DropdownItem divider />}
|
||||||
|
|
||||||
<CopyToClipboard text={completeShortUrl} onCopy={onCopyToClipboard}>
|
<CopyToClipboard text={completeShortUrl} onCopy={onCopyToClipboard}>
|
||||||
<DropdownItem>
|
<DropdownItem>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createAction, handleActions } from 'redux-actions';
|
import { createAction, handleActions } from 'redux-actions';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { apiErrorType } from '../../utils/services/ShlinkApiClient';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START';
|
export const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START';
|
||||||
@@ -13,10 +14,7 @@ export const shortUrlDeletionType = PropTypes.shape({
|
|||||||
shortCode: PropTypes.string.isRequired,
|
shortCode: PropTypes.string.isRequired,
|
||||||
loading: PropTypes.bool.isRequired,
|
loading: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.bool.isRequired,
|
error: PropTypes.bool.isRequired,
|
||||||
errorData: PropTypes.shape({
|
errorData: apiErrorType.isRequired,
|
||||||
error: PropTypes.string,
|
|
||||||
message: PropTypes.string,
|
|
||||||
}).isRequired,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import { isEmpty, isNil, reject } from 'ramda';
|
import { isEmpty, isNil, reject } from 'ramda';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const API_VERSION = '1';
|
export const apiErrorType = PropTypes.shape({
|
||||||
|
type: PropTypes.string,
|
||||||
|
detail: PropTypes.string,
|
||||||
|
title: PropTypes.string,
|
||||||
|
status: PropTypes.number,
|
||||||
|
error: PropTypes.string, // Deprecated
|
||||||
|
message: PropTypes.string, // Deprecated
|
||||||
|
});
|
||||||
|
|
||||||
export const buildShlinkBaseUrl = (url) => url ? `${url}/rest/v${API_VERSION}` : '';
|
const buildShlinkBaseUrl = (url, apiVersion) => url ? `${url}/rest/v${apiVersion}` : '';
|
||||||
|
|
||||||
export default class ShlinkApiClient {
|
export default class ShlinkApiClient {
|
||||||
constructor(axios, baseUrl, apiKey) {
|
constructor(axios, baseUrl, apiKey) {
|
||||||
this.axios = axios;
|
this.axios = axios;
|
||||||
this._baseUrl = buildShlinkBaseUrl(baseUrl);
|
this._apiVersion = 2;
|
||||||
|
this._baseUrl = baseUrl;
|
||||||
this._apiKey = apiKey || '';
|
this._apiKey = apiKey || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,13 +62,35 @@ export default class ShlinkApiClient {
|
|||||||
|
|
||||||
health = () => this._performRequest('/health', 'GET').then((resp) => resp.data);
|
health = () => this._performRequest('/health', 'GET').then((resp) => resp.data);
|
||||||
|
|
||||||
_performRequest = async (url, method = 'GET', query = {}, body = {}) =>
|
_performRequest = async (url, method = 'GET', query = {}, body = {}) => {
|
||||||
await this.axios({
|
try {
|
||||||
method,
|
return await this.axios({
|
||||||
url: `${this._baseUrl}${url}`,
|
method,
|
||||||
headers: { 'X-Api-Key': this._apiKey },
|
url: `${buildShlinkBaseUrl(this._baseUrl, this._apiVersion)}${url}`,
|
||||||
params: query,
|
headers: { 'X-Api-Key': this._apiKey },
|
||||||
data: body,
|
params: query,
|
||||||
paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'brackets' }),
|
data: body,
|
||||||
});
|
paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'brackets' }),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
const { response } = e;
|
||||||
|
|
||||||
|
// Due to a bug on all previous Shlink versions, requests to non-matching URLs will always result on a CORS error
|
||||||
|
// when performed from the browser (due to the preflight request not returning a 2xx status.
|
||||||
|
// See https://github.com/shlinkio/shlink/issues/614), which will make the "response" prop not to be set here.
|
||||||
|
// The bug will be fixed on upcoming Shlink patches, but for other versions, we can consider this situation as
|
||||||
|
// if a request has been performed to a not supported API version.
|
||||||
|
const apiVersionIsNotSupported = !response;
|
||||||
|
|
||||||
|
// When the request is not invalid or we have already tried both API versions, throw the error and let the
|
||||||
|
// caller handle it
|
||||||
|
if (!apiVersionIsNotSupported || this._apiVersion === 1) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._apiVersion = 1;
|
||||||
|
|
||||||
|
return await this._performRequest(url, method, query, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { identity } from 'ramda';
|
import { identity } from 'ramda';
|
||||||
|
import each from 'jest-each';
|
||||||
import DeleteShortUrlModal from '../../../src/short-urls/helpers/DeleteShortUrlModal';
|
import DeleteShortUrlModal from '../../../src/short-urls/helpers/DeleteShortUrlModal';
|
||||||
|
|
||||||
describe('<DeleteShortUrlModal />', () => {
|
describe('<DeleteShortUrlModal />', () => {
|
||||||
@@ -32,17 +33,34 @@ describe('<DeleteShortUrlModal />', () => {
|
|||||||
deleteShortUrl.mockClear();
|
deleteShortUrl.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows threshold error message when threshold error occurs', () => {
|
each([
|
||||||
|
[
|
||||||
|
{ error: 'INVALID_SHORTCODE_DELETION' },
|
||||||
|
'This short URL has received too many visits, and therefore, it cannot be deleted.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ type: 'INVALID_SHORTCODE_DELETION' },
|
||||||
|
'This short URL has received too many visits, and therefore, it cannot be deleted.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ error: 'INVALID_SHORTCODE_DELETION', threshold: 35 },
|
||||||
|
'This short URL has received more than 35 visits, and therefore, it cannot be deleted.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ type: 'INVALID_SHORTCODE_DELETION', threshold: 8 },
|
||||||
|
'This short URL has received more than 8 visits, and therefore, it cannot be deleted.',
|
||||||
|
],
|
||||||
|
]).it('shows threshold error message when threshold error occurs', (errorData, expectedMessage) => {
|
||||||
const wrapper = createWrapper({
|
const wrapper = createWrapper({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: true,
|
error: true,
|
||||||
shortCode: 'abc123',
|
shortCode: 'abc123',
|
||||||
errorData: { error: 'INVALID_SHORTCODE_DELETION' },
|
errorData,
|
||||||
});
|
});
|
||||||
const warning = wrapper.find('.bg-warning');
|
const warning = wrapper.find('.bg-warning');
|
||||||
|
|
||||||
expect(warning).toHaveLength(1);
|
expect(warning).toHaveLength(1);
|
||||||
expect(warning.html()).toContain('This short URL has received too many visits and therefore, it cannot be deleted');
|
expect(warning.html()).toContain(expectedMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows generic error when non-threshold error occurs', () => {
|
it('shows generic error when non-threshold error occurs', () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { ButtonDropdown, DropdownItem } from 'reactstrap';
|
import { ButtonDropdown, DropdownItem } from 'reactstrap';
|
||||||
|
import each from 'jest-each';
|
||||||
import createShortUrlsRowMenu from '../../../src/short-urls/helpers/ShortUrlsRowMenu';
|
import createShortUrlsRowMenu from '../../../src/short-urls/helpers/ShortUrlsRowMenu';
|
||||||
import PreviewModal from '../../../src/short-urls/helpers/PreviewModal';
|
import PreviewModal from '../../../src/short-urls/helpers/PreviewModal';
|
||||||
import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
|
import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
|
||||||
@@ -15,18 +16,24 @@ describe('<ShortUrlsRowMenu />', () => {
|
|||||||
shortCode: 'abc123',
|
shortCode: 'abc123',
|
||||||
shortUrl: 'https://doma.in/abc123',
|
shortUrl: 'https://doma.in/abc123',
|
||||||
};
|
};
|
||||||
|
const createWrapper = (serverVersion = '1.21.1') => {
|
||||||
beforeEach(() => {
|
|
||||||
const ShortUrlsRowMenu = createShortUrlsRowMenu(DeleteShortUrlModal, EditTagsModal);
|
const ShortUrlsRowMenu = createShortUrlsRowMenu(DeleteShortUrlModal, EditTagsModal);
|
||||||
|
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<ShortUrlsRowMenu selectedServer={selectedServer} shortUrl={shortUrl} onCopyToClipboard={onCopyToClipboard} />
|
<ShortUrlsRowMenu
|
||||||
|
selectedServer={{ ...selectedServer, version: serverVersion }}
|
||||||
|
shortUrl={shortUrl}
|
||||||
|
onCopyToClipboard={onCopyToClipboard}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => wrapper.unmount());
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => wrapper && wrapper.unmount());
|
||||||
|
|
||||||
it('renders modal windows', () => {
|
it('renders modal windows', () => {
|
||||||
|
const wrapper = createWrapper();
|
||||||
const deleteShortUrlModal = wrapper.find(DeleteShortUrlModal);
|
const deleteShortUrlModal = wrapper.find(DeleteShortUrlModal);
|
||||||
const editTagsModal = wrapper.find(EditTagsModal);
|
const editTagsModal = wrapper.find(EditTagsModal);
|
||||||
const previewModal = wrapper.find(PreviewModal);
|
const previewModal = wrapper.find(PreviewModal);
|
||||||
@@ -38,10 +45,16 @@ describe('<ShortUrlsRowMenu />', () => {
|
|||||||
expect(qrCodeModal).toHaveLength(1);
|
expect(qrCodeModal).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correct amount of menu items', () => {
|
each([
|
||||||
|
[ '1.20.3', 6, 2 ],
|
||||||
|
[ '1.21.0', 6, 2 ],
|
||||||
|
[ '1.21.1', 6, 2 ],
|
||||||
|
[ '2.0.0', 5, 1 ],
|
||||||
|
[ '2.0.1', 5, 1 ],
|
||||||
|
[ '2.1.0', 5, 1 ],
|
||||||
|
]).it('renders correct amount of menu items depending on the version', (version, expectedNonDividerItems, expectedDividerItems) => {
|
||||||
|
const wrapper = createWrapper(version);
|
||||||
const items = wrapper.find(DropdownItem);
|
const items = wrapper.find(DropdownItem);
|
||||||
const expectedNonDividerItems = 6;
|
|
||||||
const expectedDividerItems = 2;
|
|
||||||
|
|
||||||
expect(items).toHaveLength(expectedNonDividerItems + expectedDividerItems);
|
expect(items).toHaveLength(expectedNonDividerItems + expectedDividerItems);
|
||||||
expect(items.find('[divider]')).toHaveLength(expectedDividerItems);
|
expect(items.find('[divider]')).toHaveLength(expectedDividerItems);
|
||||||
@@ -49,6 +62,7 @@ describe('<ShortUrlsRowMenu />', () => {
|
|||||||
|
|
||||||
describe('toggles state when toggling modal windows', () => {
|
describe('toggles state when toggling modal windows', () => {
|
||||||
const assert = (modalComponent, stateProp, done) => {
|
const assert = (modalComponent, stateProp, done) => {
|
||||||
|
const wrapper = createWrapper();
|
||||||
const modal = wrapper.find(modalComponent);
|
const modal = wrapper.find(modalComponent);
|
||||||
|
|
||||||
expect(wrapper.state(stateProp)).toEqual(false);
|
expect(wrapper.state(stateProp)).toEqual(false);
|
||||||
@@ -66,6 +80,7 @@ describe('<ShortUrlsRowMenu />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('toggles dropdown state when toggling dropdown', (done) => {
|
it('toggles dropdown state when toggling dropdown', (done) => {
|
||||||
|
const wrapper = createWrapper();
|
||||||
const dropdown = wrapper.find(ButtonDropdown);
|
const dropdown = wrapper.find(ButtonDropdown);
|
||||||
|
|
||||||
expect(wrapper.state('isOpen')).toEqual(false);
|
expect(wrapper.state('isOpen')).toEqual(false);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import buildShlinkApiClient from '../../../src/utils/services/ShlinkApiClientBuilder';
|
import buildShlinkApiClient from '../../../src/utils/services/ShlinkApiClientBuilder';
|
||||||
import { buildShlinkBaseUrl } from '../../../src/utils/services/ShlinkApiClient';
|
|
||||||
|
|
||||||
describe('ShlinkApiClientBuilder', () => {
|
describe('ShlinkApiClientBuilder', () => {
|
||||||
const createBuilder = () => {
|
const createBuilder = () => {
|
||||||
@@ -40,7 +39,7 @@ describe('ShlinkApiClientBuilder', () => {
|
|||||||
const apiKey = 'apiKey';
|
const apiKey = 'apiKey';
|
||||||
const apiClient = await buildShlinkApiClient({})({ url, apiKey });
|
const apiClient = await buildShlinkApiClient({})({ url, apiKey });
|
||||||
|
|
||||||
expect(apiClient._baseUrl).toEqual(buildShlinkBaseUrl(url));
|
expect(apiClient._baseUrl).toEqual(url);
|
||||||
expect(apiClient._apiKey).toEqual(apiKey);
|
expect(apiClient._apiKey).toEqual(apiKey);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user