-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #176 from adobecom/linkTransformerPoc
feat(MWPW-155317): Add dynamic link transformation component
- Loading branch information
Showing
8 changed files
with
794 additions
and
333 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
200 changes: 200 additions & 0 deletions
200
react/src/js/components/Consonant/Helpers/__tests__/withLinkTransformer.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
import React from 'react'; | ||
import { render, screen } from '@testing-library/react'; | ||
import '@testing-library/jest-dom/extend-expect'; | ||
import withLinkTransformer from '../withLinkTransformer'; | ||
import { ConfigContext } from '../contexts'; | ||
import { logLana } from '../lana'; | ||
|
||
jest.mock('../lana', () => ({ | ||
logLana: jest.fn(), | ||
})); | ||
|
||
// Mock component to test link transformation | ||
// eslint-disable-next-line react/prop-types | ||
const TestComponent = ({ href, text }) => <a href={href}>{text}</a>; | ||
const TransformedComponent = withLinkTransformer(TestComponent); | ||
|
||
describe('withLinkTransformer', () => { | ||
const mockConfig = { | ||
linkTransformer: { | ||
enabled: true, | ||
hostnameTransforms: [ | ||
{ | ||
from: '(https?:\\/\\/)business\\.adobe\\.com((?!\\/content\\/dam).*)', | ||
to: '$1business.stage.adobe.com$2', | ||
}, | ||
{ | ||
from: '(https?:\\/\\/)(www\\.adobe\\.com)(\\/max.*)', | ||
to: '$1www.stage.adobe.com$3', | ||
}, | ||
], | ||
}, | ||
}; | ||
|
||
// eslint-disable-next-line max-len | ||
const renderWithConfig = (ui, config) => render(<ConfigContext.Provider value={config}>{ui}</ConfigContext.Provider>); | ||
|
||
beforeEach(() => { | ||
// Clear localStorage before each test | ||
localStorage.clear(); | ||
}); | ||
|
||
test('transforms business.adobe.com links when enabled', () => { | ||
renderWithConfig( | ||
<TransformedComponent | ||
href="https://business.adobe.com/products/magento/magento-commerce.html" | ||
text="Magento Commerce" />, | ||
mockConfig, | ||
); | ||
const link = screen.getByRole('link', { name: /Magento Commerce/i }); | ||
expect(link).toHaveAttribute('href', 'https://business.stage.adobe.com/products/magento/magento-commerce.html'); | ||
}); | ||
|
||
test('transforms www.adobe.com/max links when enabled', () => { | ||
renderWithConfig( | ||
<TransformedComponent | ||
href="https://www.adobe.com/max/2023.html" | ||
text="Adobe MAX 2023" />, | ||
mockConfig, | ||
); | ||
const link = screen.getByRole('link', { name: /Adobe MAX 2023/i }); | ||
expect(link).toHaveAttribute('href', 'https://www.stage.adobe.com/max/2023.html'); | ||
}); | ||
|
||
test('does not transform links when disabled in config', () => { | ||
const disabledConfig = { | ||
...mockConfig, | ||
linkTransformer: { ...mockConfig.linkTransformer, enabled: false }, | ||
}; | ||
|
||
renderWithConfig( | ||
<TransformedComponent | ||
href="https://business.adobe.com/products/magento/magento-commerce.html" | ||
text="Magento Commerce" />, | ||
disabledConfig, | ||
); | ||
const link = screen.getByRole('link', { name: /Magento Commerce/i }); | ||
expect(link).toHaveAttribute('href', 'https://business.adobe.com/products/magento/magento-commerce.html'); | ||
}); | ||
|
||
test('uses localStorage settings when available', () => { | ||
localStorage.setItem('linkTransformerSettings', JSON.stringify({ | ||
enabled: true, | ||
hostnameTransforms: [ | ||
{ | ||
from: '(https?:\\/\\/)www\\.adobe\\.com', | ||
to: '$1www.test.adobe.com', | ||
}, | ||
], | ||
})); | ||
|
||
renderWithConfig( | ||
<TransformedComponent | ||
href="https://www.adobe.com/products/photoshop.html" | ||
text="Photoshop" />, | ||
{ linkTransformer: { enabled: false } }, | ||
); | ||
const link = screen.getByRole('link', { name: /Photoshop/i }); | ||
expect(link).toHaveAttribute('href', 'https://www.test.adobe.com/products/photoshop.html'); | ||
}); | ||
|
||
test('does not transform non-matching links', () => { | ||
renderWithConfig( | ||
<TransformedComponent | ||
href="https://example.com" | ||
text="Example" />, | ||
mockConfig, | ||
); | ||
const link = screen.getByRole('link', { name: /Example/i }); | ||
expect(link).toHaveAttribute('href', 'https://example.com'); | ||
}); | ||
|
||
|
||
test('transforms nested props correctly', () => { | ||
// eslint-disable-next-line react/prop-types | ||
const NestedComponent = ({ data }) => ( | ||
<div> | ||
{/* eslint-disable-next-line react/prop-types */} | ||
<a href={data.link}>{data.text}</a> | ||
{/* eslint-disable-next-line react/prop-types */} | ||
<img src={data.image} alt="Test" /> | ||
</div> | ||
); | ||
const TransformedNestedComponent = withLinkTransformer(NestedComponent); | ||
|
||
renderWithConfig( | ||
<TransformedNestedComponent | ||
data={{ | ||
link: 'https://business.adobe.com/products/magento/magento-commerce.html', | ||
text: 'Magento Commerce', | ||
image: 'https://www.adobe.com/content/dam/cc/us/en/creative-cloud/photography/discover/landscape-photography/CODERED_B1_landscape_P2d_690x455.jpg.img.jpg', | ||
}} />, | ||
mockConfig, | ||
); | ||
|
||
const link = screen.getByRole('link', { name: /Magento Commerce/i }); | ||
expect(link).toHaveAttribute('href', 'https://business.stage.adobe.com/products/magento/magento-commerce.html'); | ||
|
||
const image = screen.getByRole('img', { name: /Test/i }); | ||
expect(image).toHaveAttribute('src', 'https://www.adobe.com/content/dam/cc/us/en/creative-cloud/photography/discover/landscape-photography/CODERED_B1_landscape_P2d_690x455.jpg.img.jpg'); | ||
}); | ||
|
||
describe('Edge cases and error handling', () => { | ||
test('handles non-object props correctly', () => { | ||
const StringComponent = withLinkTransformer(({ value }) => <span>{value}</span>); | ||
renderWithConfig(<StringComponent value="Just a string" />, mockConfig); | ||
expect(screen.getByText('Just a string')).toBeInTheDocument(); | ||
}); | ||
|
||
|
||
test('handles array props correctly', () => { | ||
const ArrayComponent = withLinkTransformer(({ items }) => ( | ||
<ul> | ||
{items.map((item, index) => ( | ||
// eslint-disable-next-line react/no-array-index-key | ||
<li key={index}><a href={item}>{item}</a></li> | ||
))} | ||
</ul> | ||
)); | ||
renderWithConfig(<ArrayComponent items={['https://business.adobe.com', 'https://www.adobe.com/max']} />, mockConfig); | ||
const links = screen.getAllByRole('link'); | ||
expect(links[0]).toHaveAttribute('href', 'https://business.stage.adobe.com'); | ||
expect(links[1]).toHaveAttribute('href', 'https://www.stage.adobe.com/max'); | ||
}); | ||
|
||
|
||
test('handles localStorage errors gracefully', () => { | ||
const mockGetItem = jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { | ||
throw new Error('localStorage error'); | ||
}); | ||
|
||
renderWithConfig( | ||
<TransformedComponent | ||
href="https://business.adobe.com/products/magento/magento-commerce.html" | ||
text="Magento Commerce" />, | ||
mockConfig, | ||
); | ||
|
||
const link = screen.getByRole('link', { name: /Magento Commerce/i }); | ||
expect(link).toHaveAttribute('href', 'https://business.stage.adobe.com/products/magento/magento-commerce.html'); | ||
expect(logLana).toHaveBeenCalledWith({ message: 'Error reading from localStorage:', tags: 'linkTransformer', e: expect.any(Error) }); | ||
|
||
mockGetItem.mockRestore(); | ||
}); | ||
}); | ||
}); | ||
|
||
const settings = { | ||
enabled: true, | ||
hostnameTransforms: [ | ||
{ | ||
from: '(https?:\\/\\/)business\\.adobe\\.com((?!\\/content\\/dam).*)', | ||
to: '$1business.stage.adobe.com$2', | ||
}, | ||
{ | ||
from: '(https?:\\/\\/)(www\\.adobe\\.com)(\\/max.*)', | ||
to: '$1www.stage.adobe.com$3', | ||
}, | ||
], | ||
}; | ||
localStorage.setItem('linkTransformerSettings', JSON.stringify(settings)); |
81 changes: 81 additions & 0 deletions
81
react/src/js/components/Consonant/Helpers/withLinkTransformer.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import React from 'react'; | ||
import { useConfig } from '../Helpers/hooks'; | ||
import { logLana } from '../Helpers/lana'; | ||
|
||
function isValidURL(string) { | ||
try { | ||
// eslint-disable-next-line no-new | ||
new URL(string); | ||
return true; | ||
} catch (_) { | ||
return false; | ||
} | ||
} | ||
|
||
function transformLink(url, patterns) { | ||
for (const pattern of patterns) { | ||
const regex = new RegExp(pattern.from); | ||
if (regex.test(url)) { | ||
return url.replace(regex, pattern.to); | ||
} | ||
} | ||
return url; | ||
} | ||
|
||
function transformNestedProps(obj, hostnameTransforms) { | ||
if (typeof obj !== 'object' || obj === null) { | ||
return typeof obj === 'string' && isValidURL(obj) ? transformLink(obj, hostnameTransforms) : obj; | ||
} | ||
if (Array.isArray(obj)) { | ||
return obj.map(item => transformNestedProps(item, hostnameTransforms)); | ||
} | ||
|
||
const newObj = {}; | ||
for (const [key, value] of Object.entries(obj)) { | ||
if (typeof value === 'string' && isValidURL(value)) { | ||
newObj[key] = transformLink(value, hostnameTransforms); | ||
} else if (typeof value === 'object' && value !== null) { | ||
newObj[key] = transformNestedProps(value, hostnameTransforms); | ||
} else { | ||
newObj[key] = value; | ||
} | ||
} | ||
return newObj; | ||
} | ||
|
||
function getLocalStorageSettings() { | ||
try { | ||
const settings = localStorage.getItem('linkTransformerSettings'); | ||
return settings ? JSON.parse(settings) : {}; | ||
} catch (error) { | ||
logLana({ message: 'Error reading from localStorage:', tags: 'linkTransformer', e: error }); // here | ||
return {}; // here | ||
} | ||
} | ||
|
||
function withLinkTransformer(Component) { | ||
return function WrappedComponent(props) { | ||
const getConfig = useConfig(); | ||
const configEnabled = getConfig('linkTransformer', 'enabled') || false; | ||
const configHostnameTransforms = getConfig('linkTransformer', 'hostnameTransforms') || []; | ||
|
||
const localStorageSettings = getLocalStorageSettings(); | ||
// eslint-disable-next-line max-len | ||
const localStorageEnabled = localStorageSettings && localStorageSettings.enabled !== undefined ? localStorageSettings.enabled : false; | ||
|
||
const enabled = configEnabled || localStorageEnabled; | ||
// eslint-disable-next-line max-len | ||
const haveLocalStorageHostnameTransforms = localStorageEnabled && localStorageSettings.hostnameTransforms; | ||
// eslint-disable-next-line max-len | ||
const hostnameTransforms = haveLocalStorageHostnameTransforms ? localStorageSettings.hostnameTransforms : configHostnameTransforms; | ||
|
||
const transformedProps = React.useMemo(() => { | ||
if (!enabled) return props; | ||
return transformNestedProps(props, hostnameTransforms); | ||
}, [enabled, hostnameTransforms, props]); | ||
|
||
return <Component {...transformedProps} />; | ||
}; | ||
} | ||
|
||
export default withLinkTransformer; |