diff --git a/changelog/fix-style-extraction-in-editor b/changelog/fix-style-extraction-in-editor
new file mode 100644
index 00000000000..dc191757fe1
--- /dev/null
+++ b/changelog/fix-style-extraction-in-editor
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Properly extract styles when using the site editor.
diff --git a/client/checkout/blocks/payment-elements.js b/client/checkout/blocks/payment-elements.js
index f782ba13545..3d89aec45bc 100644
--- a/client/checkout/blocks/payment-elements.js
+++ b/client/checkout/blocks/payment-elements.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { useEffect, useState, RawHTML } from '@wordpress/element';
+import { useEffect, useState, RawHTML, useRef } from '@wordpress/element';
import { Elements } from '@stripe/react-stripe-js';
// eslint-disable-next-line import/no-unresolved
import { StoreNotice } from '@woocommerce/blocks-checkout';
@@ -20,6 +20,7 @@ import { getPaymentMethodTypes } from 'wcpay/checkout/utils/upe';
const PaymentElements = ( { api, ...props } ) => {
const stripeForUPE = useStripeForUPE( api, props.paymentMethodId );
+ const containerRef = useRef( null );
const [ errorMessage, setErrorMessage ] = useState( null );
const [
@@ -29,7 +30,8 @@ const PaymentElements = ( { api, ...props } ) => {
const [ appearance, setAppearance ] = useState(
getUPEConfig( 'wcBlocksUPEAppearance' )
);
- const [ fontRules ] = useState( getFontRulesFromPage() );
+ const [ fontRules, setFontRules ] = useState( [] );
+
const [ fingerprint, fingerprintErrorMessage ] = useFingerprint();
const amount = Number( getUPEConfig( 'cartTotal' ) );
const currency = getUPEConfig( 'currency' ).toLowerCase();
@@ -37,8 +39,18 @@ const PaymentElements = ( { api, ...props } ) => {
useEffect( () => {
async function generateUPEAppearance() {
+ if ( ! containerRef.current ) {
+ return;
+ }
+ setFontRules(
+ getFontRulesFromPage( containerRef.current.ownerDocument )
+ );
// Generate UPE input styles.
- let upeAppearance = getAppearance( 'blocks_checkout', false );
+ let upeAppearance = getAppearance(
+ 'blocks_checkout',
+ false,
+ containerRef.current.ownerDocument
+ );
upeAppearance = await api.saveUPEAppearance(
upeAppearance,
'blocks_checkout'
@@ -61,46 +73,48 @@ const PaymentElements = ( { api, ...props } ) => {
props.paymentMethodId,
] );
- if ( ! stripeForUPE ) {
- return ;
- }
-
return (
-
-
+
- { paymentProcessorLoadErrorMessage?.error?.message && (
-
-
-
- {
- paymentProcessorLoadErrorMessage.error
- .message
- }
-
-
-
- ) }
-
-
-
+
+ { paymentProcessorLoadErrorMessage?.error?.message && (
+
+
+
+ {
+ paymentProcessorLoadErrorMessage.error
+ .message
+ }
+
+
+
+ ) }
+
+
+
+
+
{ title }
{ isTestMode && (
diff --git a/client/checkout/upe-styles/index.js b/client/checkout/upe-styles/index.js
index 502cf370382..440c55fc5c6 100644
--- a/client/checkout/upe-styles/index.js
+++ b/client/checkout/upe-styles/index.js
@@ -50,7 +50,7 @@ export const appearanceSelectors = {
pmmeRelativeTextSizeSelector: '.wc_payment_method > label',
},
blocksCheckout: {
- appendTarget: '#contact-fields',
+ appendTarget: '.wc-block-checkout__contact-fields',
upeThemeInputSelector: '.wc-block-components-text-input #email',
upeThemeLabelSelector: '.wc-block-components-text-input label',
upeThemeTextSelectors: [
@@ -176,16 +176,17 @@ export const appearanceSelectors = {
* Update selectors to use alternate if not present on DOM.
*
* @param {Object} selectors Object of selectors for updation.
+ * @param {Object} scope The document scope to search in.
*
* @return {Object} Updated selectors.
*/
- updateSelectors: function ( selectors ) {
+ updateSelectors: function ( selectors, scope ) {
if ( selectors.hasOwnProperty( 'alternateSelectors' ) ) {
Object.entries( selectors.alternateSelectors ).forEach(
( altSelector ) => {
const [ key, value ] = altSelector;
- if ( ! document.querySelector( selectors[ key ] ) ) {
+ if ( ! scope.querySelector( selectors[ key ] ) ) {
selectors[ key ] = value;
}
}
@@ -201,10 +202,11 @@ export const appearanceSelectors = {
* Returns selectors based on checkout type.
*
* @param {boolean} elementsLocation The location of the elements.
+ * @param {Object} scope The document scope to search in.
*
* @return {Object} Selectors for checkout type specified.
*/
- getSelectors: function ( elementsLocation ) {
+ getSelectors: function ( elementsLocation, scope ) {
let appearanceSelector = this.blocksCheckout;
switch ( elementsLocation ) {
@@ -230,7 +232,7 @@ export const appearanceSelectors = {
return {
...this.default,
- ...this.updateSelectors( appearanceSelector ),
+ ...this.updateSelectors( appearanceSelector, scope ),
};
},
};
@@ -240,11 +242,12 @@ const hiddenElementsForUPE = {
* Create hidden container for generating UPE styles.
*
* @param {string} elementID ID of element to create.
+ * @param {Object} scope The document scope to search in.
*
* @return {Object} Object of the created hidden container element.
*/
- getHiddenContainer: function ( elementID ) {
- const hiddenDiv = document.createElement( 'div' );
+ getHiddenContainer: function ( elementID, scope ) {
+ const hiddenDiv = scope.createElement( 'div' );
hiddenDiv.setAttribute( 'id', this.getIDFromSelector( elementID ) );
hiddenDiv.style.border = 0;
hiddenDiv.style.clip = 'rect(0 0 0 0)';
@@ -261,12 +264,13 @@ const hiddenElementsForUPE = {
* Create invalid element row for generating UPE styles.
*
* @param {string} elementType Type of element to create.
- * @param {Array} classes Array of classes to be added to the element. Default: empty array.
+ * @param {Array} classes Array of classes to be added to the element. Default: empty array.
+ * @param {Object} scope The document scope to search in.
*
* @return {Object} Object of the created invalid row element.
*/
- createRow: function ( elementType, classes = [] ) {
- const newRow = document.createElement( elementType );
+ createRow: function ( elementType, classes = [], scope ) {
+ const newRow = scope.createElement( elementType );
if ( classes.length ) {
newRow.classList.add( ...classes );
}
@@ -276,12 +280,18 @@ const hiddenElementsForUPE = {
/**
* Append elements to target container.
*
- * @param {Object} appendTarget Element object where clone should be appended.
+ * @param {Object} appendTarget Element object where clone should be appended.
* @param {string} elementToClone Selector of the element to be cloned.
- * @param {string} newElementID Selector for the cloned element.
+ * @param {string} newElementID Selector for the cloned element.
+ * @param {Object} scope The document scope to search in.
*/
- appendClone: function ( appendTarget, elementToClone, newElementID ) {
- const cloneTarget = document.querySelector( elementToClone );
+ appendClone: function (
+ appendTarget,
+ elementToClone,
+ newElementID,
+ scope
+ ) {
+ const cloneTarget = scope.querySelector( elementToClone );
if ( cloneTarget ) {
const clone = cloneTarget.cloneNode( true );
clone.id = this.getIDFromSelector( newElementID );
@@ -309,11 +319,12 @@ const hiddenElementsForUPE = {
* Initialize hidden fields to generate UPE styles.
*
* @param {boolean} elementsLocation The location of the elements.
+ * @param {Object} scope The scope of the elements.
*/
- init: function ( elementsLocation ) {
+ init: function ( elementsLocation, scope ) {
const selectors = appearanceSelectors.getSelectors( elementsLocation ),
- appendTarget = document.querySelector( selectors.appendTarget ),
- elementToClone = document.querySelector(
+ appendTarget = scope.querySelector( selectors.appendTarget ),
+ elementToClone = scope.querySelector(
selectors.upeThemeInputSelector
);
@@ -323,70 +334,77 @@ const hiddenElementsForUPE = {
}
// Remove hidden container is already present on DOM.
- if ( document.querySelector( selectors.hiddenContainer ) ) {
- this.cleanup();
+ if ( scope.querySelector( selectors.hiddenContainer ) ) {
+ this.cleanup( scope );
}
// Create hidden container & append to target.
const hiddenContainer = this.getHiddenContainer(
- selectors.hiddenContainer
+ selectors.hiddenContainer,
+ scope
);
appendTarget.appendChild( hiddenContainer );
// Create hidden valid row & append to hidden container.
const hiddenValidRow = this.createRow(
selectors.rowElement,
- selectors.validClasses
+ selectors.validClasses,
+ scope
);
hiddenContainer.appendChild( hiddenValidRow );
// Create hidden invalid row & append to hidden container.
const hiddenInvalidRow = this.createRow(
selectors.rowElement,
- selectors.invalidClasses
+ selectors.invalidClasses,
+ scope
);
hiddenContainer.appendChild( hiddenInvalidRow );
- // Clone & append target input to hidden valid row.
+ // Clone & append target input to hidden valid row.
this.appendClone(
hiddenValidRow,
selectors.upeThemeInputSelector,
- selectors.hiddenInput
+ selectors.hiddenInput,
+ scope
);
// Clone & append target label to hidden valid row.
this.appendClone(
hiddenValidRow,
selectors.upeThemeLabelSelector,
- selectors.hiddenValidActiveLabel
+ selectors.hiddenValidActiveLabel,
+ scope
);
- // Clone & append target input to hidden invalid row.
+ // Clone & append target input to hidden invalid row.
this.appendClone(
hiddenInvalidRow,
selectors.upeThemeInputSelector,
- selectors.hiddenInvalidInput
+ selectors.hiddenInvalidInput,
+ scope
);
// Clone & append target label to hidden invalid row.
this.appendClone(
hiddenInvalidRow,
selectors.upeThemeLabelSelector,
- selectors.hiddenInvalidInput
+ selectors.hiddenInvalidInput,
+ scope
);
// Remove transitions & focus on hidden element.
- const wcpayHiddenInput = document.querySelector(
- selectors.hiddenInput
- );
+ const wcpayHiddenInput = scope.querySelector( selectors.hiddenInput );
wcpayHiddenInput.style.transition = 'none';
},
/**
- * Remove hidden container from DROM.
+ * Remove hidden container from DOM.
+ *
+ * @param {Object} scope The scope of the elements.
*/
- cleanup: function () {
- const element = document.querySelector(
+ cleanup: function ( scope ) {
+ const element = scope.querySelector(
appearanceSelectors.default.hiddenContainer
);
if ( element ) {
@@ -398,17 +416,20 @@ const hiddenElementsForUPE = {
export const getFieldStyles = (
selector,
upeElement,
- backgroundColor = null
+ backgroundColor = null,
+ scope
) => {
- if ( ! document.querySelector( selector ) ) {
+ if ( ! scope.querySelector( selector ) ) {
return {};
}
+ const windowObject = scope.defaultView || window;
+
const validProperties = upeRestrictedProperties[ upeElement ];
- const elem = document.querySelector( selector );
+ const elem = scope.querySelector( selector );
- const styles = window.getComputedStyle( elem );
+ const styles = windowObject.getComputedStyle( elem );
const filteredStyles = {};
for ( let i = 0; i < styles.length; i++ ) {
@@ -455,9 +476,9 @@ export const getFieldStyles = (
return filteredStyles;
};
-export const getFontRulesFromPage = () => {
+export const getFontRulesFromPage = ( scope = document ) => {
const fontRules = [],
- sheets = document.styleSheets,
+ sheets = scope.styleSheets,
fontDomains = [
'fonts.googleapis.com',
'fonts.gstatic.com',
@@ -485,13 +506,15 @@ export const getFontRulesFromPage = () => {
* @param {string} selector Selector of the element to be checked.
* @param {string} fontSize Pre-computed font size.
* @param {number} percentage Percentage (0-1) to be used relative to the font size of the target element.
+ * @param {Object} scope The scope of the elements.
*
* @return {string} Font size of the element.
*/
function ensureFontSizeSmallerThan(
selector,
fontSize,
- percentage = PMME_RELATIVE_TEXT_SIZE
+ percentage = PMME_RELATIVE_TEXT_SIZE,
+ scope
) {
const fontSizeNumber = parseFloat( fontSize );
@@ -500,7 +523,7 @@ function ensureFontSizeSmallerThan(
}
// If the element is not found, return the font size number multiplied by the percentage.
- const elem = document.querySelector( selector );
+ const elem = scope.querySelector( selector );
if ( ! elem ) {
return `${ fontSizeNumber * percentage }px`;
}
@@ -520,21 +543,37 @@ function ensureFontSizeSmallerThan(
return `${ fontSizeNumber }px`;
}
-export const getAppearance = ( elementsLocation, forWooPay = false ) => {
- const selectors = appearanceSelectors.getSelectors( elementsLocation );
+export const getAppearance = (
+ elementsLocation,
+ forWooPay = false,
+ scope = document
+) => {
+ const selectors = appearanceSelectors.getSelectors(
+ elementsLocation,
+ scope
+ );
// Add hidden fields to DOM for generating styles.
- hiddenElementsForUPE.init( elementsLocation );
+ hiddenElementsForUPE.init( elementsLocation, scope );
- const inputRules = getFieldStyles( selectors.hiddenInput, '.Input' );
+ const inputRules = getFieldStyles(
+ selectors.hiddenInput,
+ '.Input',
+ null,
+ scope
+ );
const inputInvalidRules = getFieldStyles(
selectors.hiddenInvalidInput,
- '.Input'
+ '.Input',
+ null,
+ scope
);
const labelRules = getFieldStyles(
selectors.upeThemeLabelSelector,
- '.Label'
+ '.Label',
+ null,
+ scope
);
const labelRestingRules = {
@@ -543,13 +582,22 @@ export const getAppearance = ( elementsLocation, forWooPay = false ) => {
const paragraphRules = getFieldStyles(
selectors.upeThemeTextSelectors,
- '.Text'
+ '.Text',
+ null,
+ scope
);
- const tabRules = getFieldStyles( selectors.upeThemeInputSelector, '.Tab' );
+ const tabRules = getFieldStyles(
+ selectors.upeThemeInputSelector,
+ '.Tab',
+ null,
+ scope
+ );
const selectedTabRules = getFieldStyles(
selectors.hiddenInput,
- '.Tab--selected'
+ '.Tab--selected',
+ null,
+ scope
);
const tabHoverRules = generateHoverRules( tabRules );
@@ -560,24 +608,57 @@ export const getAppearance = ( elementsLocation, forWooPay = false ) => {
color: selectedTabRules.color,
};
- const backgroundColor = getBackgroundColor( selectors.backgroundSelectors );
- const headingRules = getFieldStyles( selectors.headingSelectors, '.Label' );
+ const backgroundColor = getBackgroundColor(
+ selectors.backgroundSelectors,
+ scope
+ );
+ const headingRules = getFieldStyles(
+ selectors.headingSelectors,
+ '.Label',
+ null,
+ scope
+ );
const blockRules = getFieldStyles(
selectors.upeThemeLabelSelector,
'.Block',
- backgroundColor
+ backgroundColor,
+ scope
+ );
+ const buttonRules = getFieldStyles(
+ selectors.buttonSelectors,
+ '.Input',
+ null,
+ scope
+ );
+ const linkRules = getFieldStyles(
+ selectors.linkSelectors,
+ '.Label',
+ null,
+ scope
);
- const buttonRules = getFieldStyles( selectors.buttonSelectors, '.Input' );
- const linkRules = getFieldStyles( selectors.linkSelectors, '.Label' );
const containerRules = getFieldStyles(
selectors.containerSelectors,
- '.Container'
+ '.Container',
+ null,
+ scope
+ );
+ const headerRules = getFieldStyles(
+ selectors.headerSelectors,
+ '.Header',
+ null,
+ scope
+ );
+ const footerRules = getFieldStyles(
+ selectors.footerSelectors,
+ '.Footer',
+ null,
+ scope
);
- const headerRules = getFieldStyles( selectors.headerSelectors, '.Header' );
- const footerRules = getFieldStyles( selectors.footerSelectors, '.Footer' );
const footerLinkRules = getFieldStyles(
selectors.footerLink,
- '.Footer--link'
+ '.Footer--link',
+ null,
+ scope
);
const globalRules = {
colorBackground: backgroundColor,
@@ -589,7 +670,9 @@ export const getAppearance = ( elementsLocation, forWooPay = false ) => {
if ( selectors.pmmeRelativeTextSizeSelector && globalRules.fontSizeBase ) {
globalRules.fontSizeBase = ensureFontSizeSmallerThan(
selectors.pmmeRelativeTextSizeSelector,
- paragraphRules.fontSize
+ paragraphRules.fontSize,
+ PMME_RELATIVE_TEXT_SIZE,
+ scope
);
}
@@ -623,7 +706,9 @@ export const getAppearance = ( elementsLocation, forWooPay = false ) => {
appearance,
getFieldStyles(
selectors.hiddenValidActiveLabel,
- '.Label--floating'
+ '.Label--floating',
+ null,
+ scope
)
);
}
@@ -642,6 +727,6 @@ export const getAppearance = ( elementsLocation, forWooPay = false ) => {
}
// Remove hidden fields from DOM.
- hiddenElementsForUPE.cleanup();
+ hiddenElementsForUPE.cleanup( scope );
return appearance;
};
diff --git a/client/checkout/upe-styles/test/index.js b/client/checkout/upe-styles/test/index.js
index 4af3358484c..3df8e81ba1f 100644
--- a/client/checkout/upe-styles/test/index.js
+++ b/client/checkout/upe-styles/test/index.js
@@ -32,16 +32,18 @@ describe( 'Getting styles for automated theming', () => {
};
test( 'getFieldStyles returns correct styles for inputs', () => {
- jest.spyOn( document, 'querySelector' ).mockImplementation( () => {
- return mockElement;
- } );
- jest.spyOn( window, 'getComputedStyle' ).mockImplementation( () => {
- return mockCSStyleDeclaration;
- } );
+ const scope = {
+ querySelector: jest.fn( () => mockElement ),
+ defaultView: {
+ getComputedStyle: jest.fn( () => mockCSStyleDeclaration ),
+ },
+ };
const fieldStyles = upeStyles.getFieldStyles(
'.woocommerce-checkout .form-row input',
- '.Input'
+ '.Input',
+ null,
+ scope
);
expect( fieldStyles ).toEqual( {
backgroundColor: 'rgba(0, 0, 0, 0)',
@@ -55,13 +57,15 @@ describe( 'Getting styles for automated theming', () => {
} );
test( 'getFieldStyles returns empty object if it can not find the element', () => {
- jest.spyOn( document, 'querySelector' ).mockImplementation( () => {
- return undefined;
- } );
+ const scope = {
+ querySelector: jest.fn( () => undefined ),
+ };
const fieldStyles = upeStyles.getFieldStyles(
'.i-do-not-exist',
- '.Input'
+ '.Input',
+ null,
+ scope
);
expect( fieldStyles ).toEqual( {} );
} );
@@ -103,25 +107,31 @@ describe( 'Getting styles for automated theming', () => {
},
1: { href: null },
};
- jest.spyOn( document, 'styleSheets', 'get' ).mockReturnValue(
- mockStyleSheets
- );
+ const scope = {
+ styleSheets: {
+ get: jest.fn( () => mockStyleSheets ),
+ },
+ };
- const fontRules = upeStyles.getFontRulesFromPage();
+ const fontRules = upeStyles.getFontRulesFromPage( scope );
expect( fontRules ).toEqual( [] );
} );
test( 'getAppearance returns the object with filtered CSS rules for UPE theming', () => {
- jest.spyOn( document, 'querySelector' ).mockImplementation( () => {
- return mockElement;
- } );
- jest.spyOn( window, 'getComputedStyle' ).mockImplementation( () => {
- return mockCSStyleDeclaration;
- } );
+ const scope = {
+ querySelector: jest.fn( () => mockElement ),
+ createElement: jest.fn( ( htmlTag ) =>
+ document.createElement( htmlTag )
+ ),
+ defaultView: {
+ getComputedStyle: jest.fn( () => mockCSStyleDeclaration ),
+ },
+ };
const appearance = upeStyles.getAppearance(
'shortcode_checkout',
- true
+ true,
+ scope
);
expect( appearance ).toEqual( {
variables: {
@@ -285,23 +295,24 @@ describe( 'Getting styles for automated theming', () => {
],
},
].forEach( ( { elementsLocation, expectedSelectors } ) => {
- afterEach( () => {
- document.querySelector.mockClear();
- } );
-
describe( `when elementsLocation is ${ elementsLocation }`, () => {
test( 'getAppearance uses the correct appearanceSelectors based on the elementsLocation', () => {
- jest.spyOn( document, 'querySelector' ).mockImplementation(
- () => mockElement
- );
- jest.spyOn( window, 'getComputedStyle' ).mockImplementation(
- () => mockCSStyleDeclaration
- );
+ const scope = {
+ querySelector: jest.fn( () => mockElement ),
+ createElement: jest.fn( ( htmlTag ) =>
+ document.createElement( htmlTag )
+ ),
+ defaultView: {
+ getComputedStyle: jest.fn(
+ () => mockCSStyleDeclaration
+ ),
+ },
+ };
- upeStyles.getAppearance( elementsLocation );
+ upeStyles.getAppearance( elementsLocation, false, scope );
expectedSelectors.forEach( ( selector ) => {
- expect( document.querySelector ).toHaveBeenCalledWith(
+ expect( scope.querySelector ).toHaveBeenCalledWith(
selector
);
} );
diff --git a/client/checkout/upe-styles/utils.js b/client/checkout/upe-styles/utils.js
index 7546a899653..0f02995b5fb 100644
--- a/client/checkout/upe-styles/utils.js
+++ b/client/checkout/upe-styles/utils.js
@@ -105,21 +105,25 @@ export const dashedToCamelCase = ( string ) => {
/**
* Searches through array of CSS selectors and returns first visible background color.
*
- * @param {Array} selectors List of CSS selectors to check.
+ * @param {Array} selectors List of CSS selectors to check.
+ * @param {Object} scope The document scope to search in.
* @return {string} CSS color value.
*/
-export const getBackgroundColor = ( selectors ) => {
+export const getBackgroundColor = ( selectors, scope = document ) => {
const defaultColor = '#ffffff';
let color = null;
let i = 0;
while ( ! color && i < selectors.length ) {
- const element = document.querySelector( selectors[ i ] );
+ const element = scope.querySelector( selectors[ i ] );
if ( ! element ) {
i++;
continue;
}
- const bgColor = window.getComputedStyle( element ).backgroundColor;
+ const windowObject = scope.defaultView || window;
+
+ const bgColor = windowObject.getComputedStyle( element )
+ .backgroundColor;
// If backgroundColor property present and alpha > 0.
if ( bgColor && tinycolor( bgColor ).getAlpha() > 0 ) {
color = bgColor;
diff --git a/tests/js/jest.config.js b/tests/js/jest.config.js
index 7665524e08d..f6afe00fa8d 100644
--- a/tests/js/jest.config.js
+++ b/tests/js/jest.config.js
@@ -45,6 +45,14 @@ module.exports = {
'/docker/',
'/tests/e2e',
],
+ watchPathIgnorePatterns: [
+ '/node_modules/',
+ '/vendor/',
+ '/.*/build/',
+ '/.*/build-module/',
+ '/docker/',
+ '/tests/e2e',
+ ],
transform: {
...tsjPreset.transform,
'^.+\\.(jpg|svg|png|gif)(\\?.*)?$': '/tests/js/fileMock.js',