Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions nala/studio/copy-field/specs/copy_field.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default {
FeatureName: 'M@S Studio Copy Field',
features: [
{
tcid: '0',
name: '@studio-copy-field-popover-tax-label',
// MWPW-193548 reproducer fragment in the acom surface, fr_FR locale.
// FR_fr is in DISPLAY_ALL_TAX_COUNTRIES so the rendered card preview
// carries a `.price-tax-inclusivity` "TTC" label; the Copy Field popover
// preview must mirror that — before the fix it dropped to `26,21 €/mois`.
path: '/studio.html',
browserParams: '#locale=fr_FR&page=fragment-editor&path=acom&fragmentId=',
data: {
cardid: '7f72b2a4-2ebc-48e6-bcec-4239b5aa35b2',
priceField: 'Prices',
},
tags: '@mas-studio @copy-field',
},
],
};
49 changes: 49 additions & 0 deletions nala/studio/copy-field/tests/copy_field.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { test, expect, studio, editor, miloLibs, setTestPage } from '../../../libs/mas-test.js';
import CopyFieldSpec from '../specs/copy_field.spec.js';

const { features } = CopyFieldSpec;

test.describe('M@S Studio Copy Field test suite', () => {
// @studio-copy-field-popover-tax-label - MWPW-193548:
// The Copy Field popover preview must include the locale-driven tax label
// ("TTC" for fr_FR) that the rendered card preview shows. Before the fix
// the popover dropped the label and emitted only the bare price.
test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => {
const { data } = features[0];
const testPage = `${baseURL}${features[0].path}${miloLibs}${features[0].browserParams}${data.cardid}`;
setTestPage(testPage);

await test.step('step-1: Go to MAS Studio fragment editor page', async () => {
await page.goto(testPage);
await page.waitForLoadState('domcontentloaded');
});

let renderedTaxLabel;

await test.step('step-2: Wait for editor panel and rendered card preview', async () => {
await expect(editor.panel).toBeVisible({ timeout: 15000 });
await expect(await studio.getCard(data.cardid)).toBeVisible({ timeout: 15000 });
});

await test.step('step-3: Verify rendered card preview shows the locale tax label', async () => {
const card = await studio.getCard(data.cardid);
const taxLabel = card.locator('span[is="inline-price"][data-template="price"] .price-tax-inclusivity').first();
await expect(taxLabel).toBeVisible();
await expect(taxLabel).not.toHaveClass(/(^|\s)disabled(\s|$)/);
renderedTaxLabel = (await taxLabel.textContent())?.trim();
expect(renderedTaxLabel).toBeTruthy();
});

await test.step('step-4: Open Copy Field popover from the side nav', async () => {
await expect(studio.copyFieldButton).toBeVisible();
await studio.copyFieldButton.click();
await expect(studio.copyFieldPopover).toBeVisible({ timeout: 5000 });
});

await test.step('step-5: Verify popover preview includes the same tax label', async () => {
const valueLocator = studio.copyFieldRowValue(data.priceField);
await expect(valueLocator).toBeVisible();
await expect(valueLocator).toContainText(renderedTaxLabel);
});
});
});
6 changes: 6 additions & 0 deletions nala/studio/studio.page.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@
this.publishCardButton = this.sideNav.locator('mas-side-nav-item[label="Publish"]');
this.createVariationButton = this.sideNav.locator('mas-side-nav-item[label="Create Variation"]');
this.versionHistoryButton = this.sideNav.locator('mas-side-nav-item[label="History"]');
this.copyFieldButton = this.sideNav.locator('mas-side-nav-item[label="Copy Field"]');
// Side-nav renders the Copy Field popover inside its shadow root; Playwright CSS selectors pierce shadow.
this.copyFieldPopover = this.sideNav.locator('sp-popover[open]');
this.copyFieldRow = (label) =>
this.copyFieldPopover.locator('sp-menu-item', { has: this.page.locator(`.field-label:text-is("${label}")`) });
this.copyFieldRowValue = (label) => this.copyFieldRow(label).locator('.field-value');
this.homeButton = this.sideNav.locator('mas-side-nav-item[label="Home"]');
this.offersButton = this.sideNav.locator('mas-side-nav-item[label="Offers"]');
this.fragmentsButton = this.sideNav.locator('mas-side-nav-item[label="Fragments"]');
Expand Down Expand Up @@ -123,7 +129,7 @@
]);

if (winner === 'content') {
await fragmentRender.scrollIntoViewIfNeeded();

Check failure on line 132 in nala/studio/studio.page.js

View workflow job for this annotation

GitHub Actions / Running Nala Studio E2E UI Tests (20.x)

[mas-studio-chromium] › nala/studio/studio.test.js:58:5 › M@S Studio feature test suite › @studio-search-field

6) [mas-studio-chromium] › nala/studio/studio.test.js:58:5 › M@S Studio feature test suite › @studio-search-field,@Mas-Studio › step-3: Validate search feature Error: locator.scrollIntoViewIfNeeded: Element is not attached to the DOM Call log: - attempting scroll into view action - waiting for element to be stable - element is not stable - retrying scroll into view action - waiting for element to be stable at studio/studio.page.js:132 130 | 131 | if (winner === 'content') { > 132 | await fragmentRender.scrollIntoViewIfNeeded(); | ^ 133 | } 134 | 135 | await this.page.locator('merch-card').first().waitFor({ state: 'visible', timeout: 30000 }); at StudioPage.waitForCardsLoaded (/root/actions-runner/_work/mas/mas/nala/studio/studio.page.js:132:34) at /root/actions-runner/_work/mas/mas/nala/studio/studio.test.js:80:13 at /root/actions-runner/_work/mas/mas/nala/studio/studio.test.js:77:9
}

await this.page.locator('merch-card').first().waitFor({ state: 'visible', timeout: 30000 });
Expand Down Expand Up @@ -566,7 +572,7 @@
await this.fragmentsTable.scrollIntoViewIfNeeded();
await this.fragmentsTable.click();
// await this.page.goBack();
await expect(await this.confirmationDialog).toBeVisible();

Check failure on line 575 in nala/studio/studio.page.js

View workflow job for this annotation

GitHub Actions / Running Nala Studio E2E UI Tests (20.x)

[mas-studio-chromium] › nala/studio/acom/plans/individuals/tests/individuals_edit_and_discard.test.js:460:5 › M@S Studio ACOM Plans Individuals card test suite › @studio-plans-individuals-edit-discard-price

3) [mas-studio-chromium] › nala/studio/acom/plans/individuals/tests/individuals_edit_and_discard.test.js:460:5 › M@S Studio ACOM Plans Individuals card test suite › @studio-plans-individuals-edit-discard-price,@Mas-Studio @ACOM @acom-edit @acom-plans @acom-plans-edit @acom-plans-individuals @acom-plans-individuals-edit › step-5: Close the editor and verify discard is triggered Error: expect(locator).toBeVisible() failed Locator: locator('sp-dialog[variant="confirmation"]') Expected: visible Timeout: 30000ms Error: element(s) not found Call log: - Expect "toBeVisible" with timeout 30000ms - waiting for locator('sp-dialog[variant="confirmation"]') at studio/studio.page.js:575 573 | await this.fragmentsTable.click(); 574 | // await this.page.goBack(); > 575 | await expect(await this.confirmationDialog).toBeVisible(); | ^ 576 | await this.discardDialog.click(); 577 | await expect(await editor.panel).not.toBeVisible(); 578 | await this.page.goto(fragmentUrl); at StudioPage.discardEditorChanges (/root/actions-runner/_work/mas/mas/nala/studio/studio.page.js:575:53) at /root/actions-runner/_work/mas/mas/nala/studio/acom/plans/individuals/tests/individuals_edit_and_discard.test.js:529:13 at /root/actions-runner/_work/mas/mas/nala/studio/acom/plans/individuals/tests/individuals_edit_and_discard.test.js:528:9
await this.discardDialog.click();
await expect(await editor.panel).not.toBeVisible();
await this.page.goto(fragmentUrl);
Expand Down
11 changes: 6 additions & 5 deletions studio/src/mas-side-nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -474,12 +474,13 @@ class MasSideNav extends LitElement {
if (candidateIdx === -1) return;
usedIndices.add(candidateIdx);
const candidate = resolvedInlinePrices[candidateIdx];
// Use full text (with per-unit/tax) when the source inline-price
// requested those labels; otherwise use just the core price amount.
const wantsExtras =
sourceAttrs.get('data-display-per-unit') === 'true' || sourceAttrs.get('data-display-tax') === 'true';
// Mirror the rendered preview: per-unit and tax labels are resolved
// from locale defaults (e.g. FR_fr → "TTC"), not authored on the
// source span, so the rendered DOM is the source of truth. Fall
// back to coreText only if the rendered output had no labels.
const renderedText = candidate.formattedText || candidate.coreText;
const temp = doc.createElement('span');
temp.innerHTML = wantsExtras ? candidate.formattedText : candidate.coreText;
temp.innerHTML = renderedText;
inlinePrice.replaceWith(...temp.childNodes);
});
return doc.body.innerHTML;
Expand Down
39 changes: 39 additions & 0 deletions studio/test/mas-side-nav.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,45 @@ describe('MasSideNav – Copy Field', () => {
expect(priceField.preview).to.equal('$9.99/mo');
});

it('should preserve locale-driven tax label rendered on the price (e.g. FR_fr "TTC")', () => {
// Reproduces MWPW-193548: the source span has no data-display-tax,
// but the rendered preview shows the locale-default tax label.
// The Copy Field popover preview must mirror the rendered output.
const sourceFragment = mockFragment([
{
name: 'prices',
values: ['<p><span is="inline-price" data-template="price" data-wcs-osi="abc"></span></p>'],
},
]);
const previewFragment = mockFragment([
{
name: 'prices',
values: ['<p><span is="inline-price" data-template="price" data-wcs-osi="abc"></span></p>'],
},
]);
const editor = mockEditor(sourceFragment, previewFragment);
const card = document.createElement('merch-card');
const resolvedPrice = document.createElement('span');
resolvedPrice.setAttribute('is', 'inline-price');
resolvedPrice.setAttribute('data-template', 'price');
resolvedPrice.setAttribute('data-wcs-osi', 'abc');
const priceInner = document.createElement('span');
priceInner.className = 'price';
priceInner.append(document.createTextNode('26,21 €/mois'));
const taxLabel = document.createElement('span');
taxLabel.className = 'price-tax-inclusivity';
taxLabel.textContent = 'TTC';
priceInner.append(taxLabel);
resolvedPrice.append(priceInner);
card.append(resolvedPrice);
editor.querySelector = sandbox.stub().withArgs('merch-card').returns(card);
editorStub.withArgs('mas-fragment-editor').returns(editor);

const priceField = el.copyableFields.find((f) => f.name === 'prices');
expect(priceField.preview).to.include('TTC');
expect(priceField.preview).to.include('26,21');
});

it('should resolve inline-price tokens inside description from rendered preview card', () => {
const sourceFragment = mockFragment([
{
Expand Down
4 changes: 2 additions & 2 deletions web-components/dist/mas-field.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion web-components/src/mas-field.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
import { EVENT_AEM_LOAD } from './constants.js';
import { EVENT_AEM_LOAD, FF_DEFAULTS } from './constants.js';
import { getService } from './utils.js';

const MAS_FIELD_TAG = 'mas-field';
const CHECKOUT_STYLE_PATTERN = /(accent|primary|secondary)(-(outline|link))?/;

/**
* Opts headless mas-field-hosted inline-prices into FF_DEFAULTS so they
* resolve displayTax / displayPerUnit from country+language defaults
* (the same way merch-card does for its aem-fragment-backed prices).
* Without this, prices rendered through <mas-field field="prices"> miss
* locale-driven labels like the FR_fr "TTC" tax indicator.
*/
export function priceOptionsProvider(element, options) {
if (!element?.closest?.(MAS_FIELD_TAG)) return options;
options[FF_DEFAULTS] = true;
}

function registerPriceOptionsProvider(service) {
if (!service?.providers || service.providers.has(priceOptionsProvider))
return;
service.providers.price(priceOptionsProvider);
}

const MAS_FIELD_STYLES = `
mas-field div[slot="footer"] {
display: flex;
Expand Down Expand Up @@ -49,6 +68,7 @@ class MasField extends HTMLElement {
this.addEventListener(EVENT_AEM_LOAD, this.#onFragmentLoad);
this.#ensureContentElement();
this.aemFragment?.setAttribute('hidden', '');
registerPriceOptionsProvider(getService());
}

/** Cleans up the event listener when removed from the DOM. */
Expand Down
Loading
Loading