Skip to content

Commit

Permalink
chore(link): refactor and add unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jordan-a-young committed Nov 21, 2024
1 parent b00b020 commit 3528b98
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 77 deletions.
3 changes: 2 additions & 1 deletion packages/link/index.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default, getUrl, getTarget, AvLinkProps } from './types/Link';
export { default, AvLinkProps } from './types/Link';
export { getLocation, getTarget, getLocation } from './types/util';
3 changes: 2 additions & 1 deletion packages/link/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default, getTarget, getUrl } from './src/Link';
export { default } from './src/Link';
export { getLocation, getTarget, getUrl } from './src/util';
86 changes: 23 additions & 63 deletions packages/link/src/Link.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,94 +3,54 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { isAbsoluteUrl } from '@availity/resolve-url';

// if absolute or loadApp is disabled, return url. otherwise loadappify the url
export const getUrl = (url = '', loadApp, absolute) => {
if (absolute || !loadApp) return url;

return `/public/apps/home/#!/loadApp?appUrl=${encodeURIComponent(url)}`;
};

export const getTarget = (target) => {
// should start with _, otherwise it is specifying a specific frame name
// _blank = new tab/window, _self = same frame, _parent = parent frame (use for home page from modals), _top = document body, framename = specific frame
if (target && !target.startsWith('_')) {
// Thanos uses BODY
// 'newBody' hard-coded in spaces -> should we keep this logic?
if (target === 'BODY' || target === 'newBody') {
return '_self';
}
if (target === 'TAB') {
return '_blank';
}
}

return target || '_self';
};

// takes href and transforms it so that we can compare hostnames and other properties
const getLocation = (href) => {
const location = document.createElement('a');
location.href = href;
return location;
};

const setRel = (url, target, absolute) => {
if (target === '_blank' && absolute) {
const dest = getLocation(url);
if (dest.hostname !== window.location.hostname) {
// default rel when linking to external destinations for performance and security
return 'noopener noreferrer';
}
}
// eslint-disable-next-line unicorn/no-useless-undefined
return undefined;
};
import { getRel, getTarget, getUrl } from './util';

const linkStyles = { fontWeight: 'bold' };

const AvLink = ({ tag: Tag, href, target, children, onClick, loadApp, className, ...props }) => {
const AvLink = ({ href, children, className, loadApp = true, onClick, tag: Tag = 'a', target, ...rest }) => {
const absolute = isAbsoluteUrl(href);
const url = getUrl(href, loadApp, absolute);
const classnames = classNames('link', className);
target = getTarget(target);
const rel = getRel(url, target, absolute);

const handleOnClick = (event) => {
if (onClick) onClick(event, url);
};

return (
<Tag
href={url}
target={target}
style={linkStyles}
className={classnames}
onClick={(event) => onClick && onClick(event, url)}
data-testid="av-link-tag"
rel={setRel(url, target, absolute)}
{...props}
onClick={handleOnClick}
rel={rel}
style={linkStyles}
target={target}
{...rest}
>
{children}
</Tag>
);
};

AvLink.defaultProps = {
tag: 'a',
loadApp: true,
};

AvLink.propTypes = {
/** Where to open the linked document. */
target: PropTypes.string,
/** The tag to use in the link that gets rendered. Default: <a>. */
tag: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
/** Children can be a react child or render pop. */
children: PropTypes.node,
/** The url of the page the link goes to. */
href: PropTypes.string.isRequired,
/** Function to run onClick of the link. The first argument passed to onClick is the event. The second argument is the processed url. */
onClick: PropTypes.func,
/** When false, the url prop to AvLink is not formatted to leverage loadApp. */
loadApp: PropTypes.bool,
/** Children can be a react child or render pop. */
children: PropTypes.node,
/** Additional classes that should be applied to Link.
or Pass a string containing the class names as a prop. */
className: PropTypes.string,
/** When false, the url prop to AvLink is not formatted to leverage loadApp. */
loadApp: PropTypes.bool,
/** The tag to use in the link that gets rendered. Default: <a>. */
tag: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
/** Where to open the linked document. */
target: PropTypes.string,
/** Function that is called when the element is clicked. The first argument passed to onClick is the event. The second argument is the processed url. */
onClick: PropTypes.func,
/** The relationship of the linked URL as space-separated link types. */
rel: PropTypes.string,
};

Expand Down
46 changes: 46 additions & 0 deletions packages/link/src/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export const isEssentialsUrl = (url) => /(test|qa(p?)-)?essentials\.availity\.com/.test(url);

/** If absolute or loadApp is disabled, return url. Otherwise loadappify the url */
export const getUrl = (url = '', loadApp = false, absolute = false) => {
// if ((absolute || !loadApp) && !isEssentialsUrl(url)) return url;
if (absolute || !loadApp) return url;

return `/public/apps/home/#!/loadApp?appUrl=${encodeURIComponent(url)}`;
};

/** Return a valid target based on what is passed in */
export const getTarget = (target) => {
// should start with _, otherwise it is specifying a specific frame name
// _blank = new tab/window, _self = same frame, _parent = parent frame (use for home page from modals), _top = document body, framename = specific frame
if (target && !target.startsWith('_')) {
// Thanos uses BODY
// 'newBody' hard-coded in spaces -> should we keep this logic?
if (target === 'BODY' || target === 'newBody') {
return '_self';
}
if (target === 'TAB') {
return '_blank';
}
}

return target || '_self';
};

/** Takes href and transforms it so that we can compare hostnames and other properties */
export const getLocation = (href) => {
const location = document.createElement('a');
location.href = href;
return location;
};

export const getRel = (url, target, absolute) => {
if (target === '_blank' && absolute) {
const dest = getLocation(url);
if (dest.hostname !== window.location.hostname) {
// default rel when linking to external destinations for performance and security
return 'noopener noreferrer';
}
}
// eslint-disable-next-line unicorn/no-useless-undefined
return undefined;
};
5 changes: 2 additions & 3 deletions packages/link/tests/Link.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React from 'react';
import { render, cleanup, fireEvent } from '@testing-library/react';
import AvLink from '..';
import { render, fireEvent } from '@testing-library/react';

afterEach(cleanup);
import AvLink from '..';

describe('AvLink', () => {
test('should render absolute url', () => {
Expand Down
105 changes: 105 additions & 0 deletions packages/link/tests/util.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { getLocation, getRel, getTarget, getUrl, isEssentialsUrl } from '../src/util';

describe('AvLink utils', () => {
const APPS = 'https://apps.availity.com';
const ESSENTIALS = 'https://essentials.availity.com';

beforeEach(() => {
global.jsdom.reconfigure({
url: 'https://apps.availity.com/public/apps/home/#!/',
});
});

describe('getLocation', () => {
test('should return current href in apps', () => {
expect(getLocation(APPS).href).toBe(`${APPS}/`);
});
test('should return current href in essentials', () => {
expect(getLocation(ESSENTIALS).href).toBe(`${ESSENTIALS}/`);
});
});

describe('getTarget', () => {
const SELF = '_self';
test('handles newBody', () => {
expect(getTarget('newBody')).toBe(SELF);
});
test('handles BODY', () => {
expect(getTarget('BODY')).toBe(SELF);
});
test('handles TAB', () => {
expect(getTarget('TAB')).toBe('_blank');
});
test('handles underline prefix', () => {
expect(getTarget('_test')).toBe('_test');
});
test('handles other', () => {
expect(getTarget('foobar')).toBe('foobar');
});
test('handles no target', () => {
expect(getTarget()).toBe(SELF);
});
});

describe('getUrl', () => {
test('apps domain loadApp false and absolute false', () => {
expect(getUrl(APPS, false, false)).toBe(APPS);
});
test('apps domain loadApp false and absolute true', () => {
expect(getUrl(APPS, false, true)).toBe(APPS);
});
test('apps domain loadApp true and absolute false', () => {
expect(getUrl(APPS, true, false)).toBe('/public/apps/home/#!/loadApp?appUrl=https%3A%2F%2Fapps.availity.com');
});
test('apps domain loadApp true and absolute true', () => {
expect(getUrl(APPS, true, true)).toBe(APPS);
});
// test('essentials domain loadApp false and absolute false', () => {
// expect(getUrl(ESSENTIALS, false, false)).toBe(
// '/public/apps/home/#!/loadApp?appUrl=https%3A%2F%2Fessentials.availity.com'
// );
// });
// test('essentials domain loadApp false and absolute true', () => {
// expect(getUrl(ESSENTIALS, false, true)).toBe(
// '/public/apps/home/#!/loadApp?appUrl=https%3A%2F%2Fessentials.availity.com'
// );
// });
// test('essentials domain loadApp true and absolute false', () => {
// expect(getUrl(ESSENTIALS, true, false)).toBe(
// '/public/apps/home/#!/loadApp?appUrl=https%3A%2F%2Fessentials.availity.com'
// );
// });
// test('essentials domain loadApp true and absolute true', () => {
// expect(getUrl(ESSENTIALS, true, true)).toBe(
// '/public/apps/home/#!/loadApp?appUrl=https%3A%2F%2Fessentials.availity.com'
// );
// });
});

describe('getRel', () => {
test('handles _blank target and relative url', () => {
expect(getRel(APPS, '_blank', false)).toBeUndefined();
});
test('handles _blank target and absolute url', () => {
expect(getRel(APPS, '_blank', true)).toBeUndefined();
});
test('handles non _blank target and relative url', () => {
expect(getRel(APPS, '_blank', false)).toBeUndefined();
});
test('handles non _blank target and absolute url', () => {
expect(getRel(APPS, '_blank', true)).toBeUndefined();
});
});

describe('isEssentialsUrl', () => {
test('handles apps domain', () => {
expect(isEssentialsUrl(APPS)).toBeFalsy();
});

test('handles essentials domain', () => {
expect(isEssentialsUrl(ESSENTIALS)).toBeTruthy();
expect(isEssentialsUrl(ESSENTIALS.replace('essentials', 'test-essentials'))).toBeTruthy();
expect(isEssentialsUrl(ESSENTIALS.replace('essentials', 'qa-essentials'))).toBeTruthy();
});
});
});
12 changes: 3 additions & 9 deletions packages/link/types/Link.d.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
export interface AvLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
target?: string;
tag?: React.ReactType | string;
onClick?: (event: React.SyntheticEvent<HTMLAnchorElement>, url: string) => void;
href: string;
loadApp?: boolean;
onClick?: (event: React.SyntheticEvent<HTMLAnchorElement>, url: string) => void;
rel?: string;
tag?: React.ReactType | string;
target?: string;
}

declare const AvLink: React.FC<AvLinkProps>;

declare function getUrl(url: string, loadApp: boolean, absolute: boolean): string;

declare function getTarget(target: string): string;

export { getUrl, getTarget };

export default AvLink;
11 changes: 11 additions & 0 deletions packages/link/types/util.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
declare function getLocation(href: string): HTMLAnchorElement;

declare function getTarget(target?: string): string;

declare function getUrl(url?: string, loadApp?: boolean, absolute?: boolean): string;

declare function getRel(): string | undefined;

declare function isEssentialsUrl(url: string): boolean;

export { getLocation, getTarget, getUrl, getRel };

0 comments on commit 3528b98

Please sign in to comment.