From b00b020c1fa1dba2c854425921c4790d00b8e524 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Wed, 20 Nov 2024 14:40:18 -0800 Subject: [PATCH 1/2] Revert "feat(link): update link to encode essentials urls" This reverts commit 65484e85994a6825b372345326d7c53f512c5af8. --- packages/link/src/Link.js | 6 ++---- packages/link/tests/Link.test.js | 21 --------------------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/packages/link/src/Link.js b/packages/link/src/Link.js index 1055c8e985..2d2d192eb8 100644 --- a/packages/link/src/Link.js +++ b/packages/link/src/Link.js @@ -4,10 +4,8 @@ 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 = false, absolute = false) => { - const isEssentials = /(test|qa(p?)-)?essentials\.availity\.com/.test(url); - - if ((absolute || !loadApp) && !isEssentials) return url; +export const getUrl = (url = '', loadApp, absolute) => { + if (absolute || !loadApp) return url; return `/public/apps/home/#!/loadApp?appUrl=${encodeURIComponent(url)}`; }; diff --git a/packages/link/tests/Link.test.js b/packages/link/tests/Link.test.js index 560fdfcaa6..cee295ed5c 100644 --- a/packages/link/tests/Link.test.js +++ b/packages/link/tests/Link.test.js @@ -114,25 +114,4 @@ describe('AvLink', () => { expect(link).toHaveAttribute('class', 'link card-link'); expect(link).toHaveAttribute('style', 'font-weight: bold;'); }); - - test('should render urls from the test-essentials.availity.com domain correctly', () => { - const exampleHref = 'https://test-essentials.availity.com/static/avonly/post/cs/enhanced-claim-status-ui/'; - - const { getByTestId } = render(My App); - - const tag = getByTestId('av-link-tag'); - const expected = `/public/apps/home/#!/loadApp?appUrl=${encodeURIComponent(exampleHref)}`; - - expect(tag.getAttribute('href')).toBe(expected); - }); - test('should render urls from the qa-essentials.availity.com domain correctly', () => { - const exampleHref = 'https://qa-essentials.availity.com/static/avonly/post/cs/enhanced-claim-status-ui/'; - - const { getByTestId } = render(My App); - - const tag = getByTestId('av-link-tag'); - const expected = `/public/apps/home/#!/loadApp?appUrl=${encodeURIComponent(exampleHref)}`; - - expect(tag.getAttribute('href')).toBe(expected); - }); }); From 3528b98bb0105215a88948850a4434730ced858b Mon Sep 17 00:00:00 2001 From: Jordan Young Date: Thu, 21 Nov 2024 13:33:26 -0500 Subject: [PATCH 2/2] chore(link): refactor and add unit tests --- packages/link/index.d.ts | 3 +- packages/link/index.js | 3 +- packages/link/src/Link.js | 86 +++++++------------------ packages/link/src/util.js | 46 ++++++++++++++ packages/link/tests/Link.test.js | 5 +- packages/link/tests/util.test.js | 105 +++++++++++++++++++++++++++++++ packages/link/types/Link.d.ts | 12 +--- packages/link/types/util.d.ts | 11 ++++ 8 files changed, 194 insertions(+), 77 deletions(-) create mode 100644 packages/link/src/util.js create mode 100644 packages/link/tests/util.test.js create mode 100644 packages/link/types/util.d.ts diff --git a/packages/link/index.d.ts b/packages/link/index.d.ts index 936f277f14..f2dfc731e4 100644 --- a/packages/link/index.d.ts +++ b/packages/link/index.d.ts @@ -1 +1,2 @@ -export { default, getUrl, getTarget, AvLinkProps } from './types/Link'; +export { default, AvLinkProps } from './types/Link'; +export { getLocation, getTarget, getLocation } from './types/util'; diff --git a/packages/link/index.js b/packages/link/index.js index 602deea77f..753e6a0165 100644 --- a/packages/link/index.js +++ b/packages/link/index.js @@ -1 +1,2 @@ -export { default, getTarget, getUrl } from './src/Link'; +export { default } from './src/Link'; +export { getLocation, getTarget, getUrl } from './src/util'; diff --git a/packages/link/src/Link.js b/packages/link/src/Link.js index 2d2d192eb8..7a50072b2f 100644 --- a/packages/link/src/Link.js +++ b/packages/link/src/Link.js @@ -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 ( 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} ); }; -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: . */ - 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: . */ + 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, }; diff --git a/packages/link/src/util.js b/packages/link/src/util.js new file mode 100644 index 0000000000..9cd93f8fa9 --- /dev/null +++ b/packages/link/src/util.js @@ -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; +}; diff --git a/packages/link/tests/Link.test.js b/packages/link/tests/Link.test.js index cee295ed5c..938b1b9dfe 100644 --- a/packages/link/tests/Link.test.js +++ b/packages/link/tests/Link.test.js @@ -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', () => { diff --git a/packages/link/tests/util.test.js b/packages/link/tests/util.test.js new file mode 100644 index 0000000000..461280d678 --- /dev/null +++ b/packages/link/tests/util.test.js @@ -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(); + }); + }); +}); diff --git a/packages/link/types/Link.d.ts b/packages/link/types/Link.d.ts index fc746e8d40..0c5b22178f 100644 --- a/packages/link/types/Link.d.ts +++ b/packages/link/types/Link.d.ts @@ -1,18 +1,12 @@ export interface AvLinkProps extends React.AnchorHTMLAttributes { - target?: string; - tag?: React.ReactType | string; - onClick?: (event: React.SyntheticEvent, url: string) => void; href: string; loadApp?: boolean; + onClick?: (event: React.SyntheticEvent, url: string) => void; rel?: string; + tag?: React.ReactType | string; + target?: string; } declare const AvLink: React.FC; -declare function getUrl(url: string, loadApp: boolean, absolute: boolean): string; - -declare function getTarget(target: string): string; - -export { getUrl, getTarget }; - export default AvLink; diff --git a/packages/link/types/util.d.ts b/packages/link/types/util.d.ts new file mode 100644 index 0000000000..d09a59d885 --- /dev/null +++ b/packages/link/types/util.d.ts @@ -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 };