Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MWPW-153962: Introduce maslibs query parameter #2544

Merged
merged 14 commits into from
Jul 10, 2024
57 changes: 47 additions & 10 deletions libs/blocks/merch/merch.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,31 @@ const LOADING_ENTITLEMENTS = 'loading-entitlements';
let log;
let upgradeOffer = null;

/**
* Given a url, calculates the hostname of MAS platform.
* Supports, www prod, stage, local and feature branches.
* @param {string} hostname
* @param {string} maslibs
* @returns base url for mas platform
*/
export function getMasBase(hostname, maslibs) {
narcis-radu marked this conversation as resolved.
Show resolved Hide resolved
let { baseUrl } = getMasBase;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would always be empty on the first pass, right? I think our pattern here is to expose a let baseUrl outside the method and then update the value. https://github.com/adobecom/milo/pull/2544/files#diff-01476d99c8ba4d00b7e767d0afbc424902d0d40e75c4ec4272a966edd0d4124dR149 is a bit confusing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We used this pattern in the other functions in merch block. Thus, we can cache the calculated value, and erase it when needed (mostly in tests). Using a module scope variable makes testing harder. You need to provide a utility method to reset the module scope variables.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But still, on the first pass the baseUrl value would be empty and I don't see a reason why you would need to call the method multiple times.

Copy link
Contributor Author

@yesil yesil Jul 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@narcis-radu it is not going to be called to many times but several times definitely.
later after commerce.js, we will start adding maslibs feature to merch-card, merch-card-collection and other merch blocks as well.
merch being auto block it will be the first one to call this method and have the base url initialised, then the other blocks can use the value.
@3ch023 I'm tempted to add merch-card in this PR, thus we can give a better shape to the code, wdyt? cc: @npeltier

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still not happy with the baseUrl approach since it doesn't follow the pattern used in other Milo blocks, but I don't think this is a blocker or reason to request changes. Thanks for addressing the important changes 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, something like getMasLibs setMasLibs would be better. I'll give it a try tomorrow my morning.

if (!baseUrl) {
if (maslibs === 'stage') {
baseUrl = 'https://www.stage.adobe.com/mas';
} else if (maslibs === 'local') {
baseUrl = 'http://localhost:9001';
} else if (maslibs) {
const extension = /.page$/.test(hostname) ? 'page' : 'live';
baseUrl = `https://${maslibs}.hlx.${extension}`;
} else {
baseUrl = 'https://www.adobe.com/mas';
}
getMasBase.baseUrl = baseUrl;
}
return baseUrl;
}

export async function polyfills() {
if (polyfills.promise) return polyfills.promise;
let isSupported = false;
Expand Down Expand Up @@ -326,15 +351,18 @@ export async function getModalAction(offers, options) {
}

export async function getCheckoutAction(offers, options, imsSignedInPromise) {
const [downloadAction, upgradeAction, modalAction] = await Promise.all([
getDownloadAction(options, imsSignedInPromise, offers),
getUpgradeAction(options, imsSignedInPromise, offers),
getModalAction(offers, options),
]).catch((e) => {
try {
await imsSignedInPromise;
const [downloadAction, upgradeAction, modalAction] = await Promise.all([
getDownloadAction(options, imsSignedInPromise, offers),
getUpgradeAction(options, imsSignedInPromise, offers),
getModalAction(offers, options),
]);
return downloadAction || upgradeAction || modalAction;
} catch (e) {
log?.error('Failed to resolve checkout action', e);
return [];
});
return downloadAction || upgradeAction || modalAction;
}
}

/**
Expand All @@ -349,7 +377,15 @@ export async function initService(force = false) {
const { env, commerce = {}, locale } = getConfig();
commerce.priceLiteralsPromise = fetchLiterals(PRICE_LITERALS_URL);
initService.promise = initService.promise ?? polyfills().then(async () => {
const commerceLib = await import('../../deps/commerce.js');
const { hostname, searchParams } = new URL(window.location.href);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't the method itself get the hostname and searchParams? What's the value of sending these from here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept getMasBase as a pure function so that it is easier to test it.
Since we need to check whether maslibs param is present here and getMasBase only needs hostname and maslibs parameters, I limited it to those params.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we will address this in a different PR.

let commerceLibPath = '../../deps/commerce.js';
if (/hlx\.(page|live)$|localhost$|www\.stage\.adobe\.com$/.test(hostname)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider other STAGE environments like BACOM or Firefly ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Afaik BACOM doesn't have prices, and Firefly is not merch enabled yet.
This can be done later as part of merch enablement on Firefly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even with that, for now it's mostly for testing, and most of our pages are on cc/dc so i think we are good

const maslibs = searchParams.get('maslibs');
if (maslibs) {
commerceLibPath = `${getMasBase(hostname, maslibs)}/libs/commerce.js`;
}
}
const commerceLib = await import(commerceLibPath);
const service = await commerceLib.init(() => ({
env,
commerce,
Expand All @@ -376,8 +412,9 @@ export async function getCommerceContext(el, params) {
}

/**
* Checkout parameter can be set Merch link, code config (scripts.js) or be a default from tacocat.
* To get the default, 'undefinded' should be passed, empty string will trigger an error!
* Checkout parameter can be set on the merch link,
* code config (scripts.js) or be a default from tacocat.
* To get the default, 'undefined' should be passed, empty string will trigger an error!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a lint change? (fine with me, just clarifying)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also typo: undefinded :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hehe

*
* clientId - code config -> default (adobe_com)
* workflow - merch link -> metadata -> default (UCv3)
Expand Down
4 changes: 4 additions & 0 deletions test/blocks/merch/mas/libs/commerce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const mock = true;
const init = () => ({ imsSignedInPromise: Promise.resolve(), mock });
// eslint-disable-next-line import/prefer-default-export
export { init };
65 changes: 63 additions & 2 deletions test/blocks/merch/merch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import merch, {
getCheckoutAction,
PRICE_LITERALS_URL,
PRICE_TEMPLATE_REGULAR,
getMasBase,
} from '../../../libs/blocks/merch/merch.js';

import { mockFetch, unmockFetch, readMockText } from './mocks/fetch.js';
Expand Down Expand Up @@ -72,6 +73,16 @@ const config = {
placeholders: { 'upgrade-now': 'Upgrade Now', download: 'Download' },
};

const updateSearch = ({ maslibs } = {}) => {
const url = new URL(window.location);
if (!maslibs) {
url.searchParams.delete('maslibs');
} else {
url.searchParams.set('maslibs', maslibs);
}
window.history.pushState({}, '', url);
};

/**
* utility function that tests Price spans against mock HTML
*
Expand Down Expand Up @@ -147,6 +158,7 @@ describe('Merch Block', () => {

afterEach(() => {
setSubscriptionsData();
updateSearch();
});

it('does not decorate merch with bad content', async () => {
Expand Down Expand Up @@ -531,12 +543,22 @@ describe('Merch Block', () => {
});

it('getCheckoutAction: handles errors gracefully', async () => {
const action = await getCheckoutAction([{ productArrangement: {} }], {}, Promise.reject(new Error('error')));
expect(action).to.be.undefined;
const imsSignedInPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('error'));
}, 1);
});
const action = await getCheckoutAction([{ productArrangement: {} }], {}, imsSignedInPromise);
expect(action).to.be.empty;
});
});

describe('Upgrade Flow', () => {
beforeEach(() => {
getMasBase.baseUrl = undefined;
updateSearch({});
});

it('updates CTA text to Upgrade Now', async () => {
mockIms();
getUserEntitlements();
Expand Down Expand Up @@ -668,4 +690,43 @@ describe('Merch Block', () => {
});
});
});

describe('M@S consumption', () => {
describe('maslibs parameter', () => {
beforeEach(() => {
getMasBase.baseUrl = undefined;
updateSearch({});
});

it('should load commerce.js via maslibs', async () => {
initService.promise = undefined;
getMasBase.baseUrl = 'http://localhost:2000/test/blocks/merch/mas';
updateSearch({ maslibs: 'test' });
setConfig(config);
await mockIms();
const commerce = await initService(true);
expect(commerce.mock).to.be.true;
});

it('should return the default Adobe URL if no maslibs parameter is present', () => {
expect(getMasBase()).to.equal('https://www.adobe.com/mas');
});

it('should return the stage Adobe URL if maslibs=stage', () => {
expect(getMasBase('https://main--milo--adobecom.hlx.live', 'stage')).to.equal('https://www.stage.adobe.com/mas');
});

it('should return the local URL if maslibs=local', () => {
expect(getMasBase('https://main--milo--adobecom.hlx.live', 'local')).to.equal('http://localhost:9001');
});

it('should return the hlx live URL from the fork if maslibs contains double dashes', () => {
expect(getMasBase('https://main--milo--adobecom.hlx.live', 'test--mas--user')).to.equal('https://test--mas--user.hlx.live');
});

it('should return the hlx page URL from the fork if maslibs contains double dashes', () => {
expect(getMasBase('https://main--milo--adobecom.hlx.page', 'test--mas--user')).to.equal('https://test--mas--user.hlx.page');
});
});
});
});
Loading