Skip to content

Commit

Permalink
Merge pull request #176 from adobecom/linkTransformerPoc
Browse files Browse the repository at this point in the history
feat(MWPW-155317): Add dynamic link transformation component
  • Loading branch information
sanrai authored Sep 17, 2024
2 parents 57e413b + f1fee12 commit 1753693
Show file tree
Hide file tree
Showing 8 changed files with 794 additions and 333 deletions.
2 changes: 1 addition & 1 deletion dist/app.css

Large diffs are not rendered by default.

577 changes: 367 additions & 210 deletions dist/main.js

Large diffs are not rendered by default.

42 changes: 21 additions & 21 deletions dist/main.min.js

Large diffs are not rendered by default.

205 changes: 106 additions & 99 deletions dist/main.source.js

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
}
},
resultsPerPage: '5',
endpoint: location.hostname === "localhost" ? "../../mock-json/all-art-board-15-cards-show.json" : "../../caas/mock-json/all-art-board-15-cards-show.json",
endpoint: location.hostname === "localhost" ? "../../mock-json/all-art-board-15-cards-show.json" : "https://14257-chimera.adobeioruntime.net/api/v1/web/chimera-0.0.1/collection?origin=northstar&size=20",
totalCardsToShow: '55',
cardStyle: "1:2", // available options: "1:2", "3:4", "full-card", "half-height", "custom-card", "product", "double-wide";
showTotalResults: 'true',
Expand Down Expand Up @@ -261,6 +261,19 @@
}
}
},
linkTransformer: {
enabled: false,
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',
}
]
},
bookmarks: {
showOnCards: 'true',
leftFilterPanel: {
Expand Down
5 changes: 4 additions & 1 deletion react/src/js/components/Consonant/Grid/Grid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { cardType } from '../types/card';
import { getByPath } from '../Helpers/general';
import { useConfig } from '../Helpers/hooks';
import Card from '../Cards/Card';
import withLinkTransformer from '../Helpers/withLinkTransformer';

import {
CARD_STYLES,
Expand All @@ -23,6 +24,8 @@ import {
GUTTER_SIZE,
} from '../Helpers/constants';

const TransformedCard = withLinkTransformer(Card);

const cardsGridType = {
pages: number,
resultsPerPage: number,
Expand Down Expand Up @@ -223,7 +226,7 @@ const Grid = (props) => {
return parseHTML(customCard(card));
default:
return (
<Card
<TransformedCard
cardStyle={cardStyle}
lh={`Card ${cardNumber} | ${cleanTitle(title)} | ${id}`}
key={card.id}
Expand Down
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 react/src/js/components/Consonant/Helpers/withLinkTransformer.js
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;

0 comments on commit 1753693

Please sign in to comment.