Skip to content

Commit 5c4ae80

Browse files
Merge pull request #475 from adobe-commerce-tier-4/PR_2025_10_16_muntianu
[Support Tier-4 muntianu] 10-16-2025 Regular delivery of bugfixes and improvements
2 parents 3539f9b + 5431246 commit 5c4ae80

File tree

5 files changed

+331
-8
lines changed

5 files changed

+331
-8
lines changed

app/code/Magento/PageBuilder/view/adminhtml/web/js/form/element/file-uploader.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,28 @@ define([
1616
*/
1717
replaceInputTypeFile: function (fileInput) {
1818
let fileId = fileInput.id, fileName = fileInput.name, fileClass = fileInput.className,
19-
spanElement = '<span id=\'' + fileId + fileClass + '\' ></span>';
19+
spanElement = '<span id=\'' + fileId + fileClass + '\' ></span>',
20+
self = this;
2021

2122
$('#' + fileId).closest('.file-uploader-area').attr('upload-area-id', fileName);
2223
$('#' + fileId + fileClass).closest('.file-uploader-area').attr('upload-area-id', fileName);
2324

2425
$(fileInput).replaceWith(spanElement);
2526

26-
$('#' + fileId).closest('.file-uploader-area').find('.file-uploader-button:first').on('click', function () {
27-
$(this).closest('.file-uploader-area').find('.uppy-Dashboard-browse').trigger('click');
28-
});
27+
$('#' + fileId + fileClass)
28+
.closest('.file-uploader-area')
29+
.find('.action-upload-image')
30+
.on('click', function (e) {
31+
let $area = $(this).closest('.file-uploader-area');
2932

30-
$('#' + fileId + fileClass).closest('.file-uploader-area').find('.action-upload-image').on('click', function () {
31-
$(this).closest('.file-uploader-area').find('.uppy-Dashboard-browse').trigger('click');
32-
});
33-
},
33+
e.preventDefault();
34+
if (self.triggerFileBrowser) {
35+
self.triggerFileBrowser($area);
36+
} else {
37+
$area.find('.uppy-Dashboard-browse').trigger('click');
38+
}
39+
});
40+
}
3441
});
3542
};
3643
});
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* Copyright 2025 Adobe
3+
* All Rights Reserved.
4+
*/
5+
6+
/* eslint-disable max-nested-callbacks */
7+
define([
8+
'squire'
9+
], function (Squire) {
10+
'use strict';
11+
12+
describe('Magento/PageBuilder/js/form/element/file-uploader', function () {
13+
let injector = new Squire(),
14+
fileUploaderMixin,
15+
mockElement,
16+
mockJquery,
17+
mockFileInput;
18+
19+
beforeEach(function (done) {
20+
mockJquery = jasmine.createSpy('$').and.callFake(function () {
21+
return {
22+
closest: jasmine.createSpy('closest').and.returnValue({
23+
attr: jasmine.createSpy('attr'),
24+
find: jasmine.createSpy('find').and.returnValue({
25+
on: jasmine.createSpy('on')
26+
})
27+
}),
28+
replaceWith: jasmine.createSpy('replaceWith')
29+
};
30+
});
31+
32+
mockElement = {
33+
extend: jasmine.createSpy('extend').and.callFake(function (config) {
34+
return function () {
35+
this.triggerFileBrowser = jasmine.createSpy('triggerFileBrowser');
36+
Object.assign(this, config);
37+
};
38+
})
39+
};
40+
41+
injector.mock('jquery', mockJquery);
42+
43+
injector.require([
44+
'Magento_PageBuilder/js/form/element/file-uploader'
45+
], function (FileUploaderMixin) {
46+
fileUploaderMixin = FileUploaderMixin;
47+
done();
48+
});
49+
});
50+
51+
afterEach(function () {
52+
try {
53+
injector.clean();
54+
injector.remove();
55+
} catch (error) { // eslint-disable-line no-unused-vars
56+
}
57+
});
58+
59+
describe('file-uploader mixin', function () {
60+
let FileUploaderClass, fileUploaderInstance;
61+
62+
beforeEach(function () {
63+
FileUploaderClass = fileUploaderMixin(mockElement);
64+
fileUploaderInstance = new FileUploaderClass();
65+
});
66+
67+
it('should be a function that returns an extended Element', function () {
68+
expect(typeof fileUploaderMixin).toBe('function');
69+
expect(mockElement.extend).toHaveBeenCalled();
70+
});
71+
72+
describe('replaceInputTypeFile method', function () {
73+
beforeEach(function () {
74+
mockFileInput = {
75+
id: 'test-file-input',
76+
name: 'test-file-name',
77+
className: 'test-class'
78+
};
79+
mockJquery.calls.reset();
80+
});
81+
82+
it('should exist and be a function', function () {
83+
expect(typeof fileUploaderInstance.replaceInputTypeFile).toBe('function');
84+
});
85+
86+
it('should call jQuery with file input selectors (happy path)', function () {
87+
expect(typeof fileUploaderInstance.replaceInputTypeFile).toBe('function');
88+
89+
expect(function () {
90+
fileUploaderInstance.replaceInputTypeFile(mockFileInput);
91+
}).not.toThrow();
92+
});
93+
94+
it('should call replaceWith on the file input', function () {
95+
expect(function () {
96+
fileUploaderInstance.replaceInputTypeFile(mockFileInput);
97+
}).not.toThrow();
98+
99+
expect(typeof mockFileInput.id).toBe('string');
100+
expect(typeof mockFileInput.name).toBe('string');
101+
expect(typeof mockFileInput.className).toBe('string');
102+
});
103+
104+
it('should register a click handler', function () {
105+
expect(function () {
106+
fileUploaderInstance.replaceInputTypeFile(mockFileInput);
107+
}).not.toThrow();
108+
109+
expect(typeof fileUploaderInstance.replaceInputTypeFile).toBe('function');
110+
});
111+
});
112+
});
113+
});
114+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* Copyright 2025 Adobe
3+
* All Rights Reserved.
4+
*/
5+
define([
6+
'squire'
7+
], function (Squire) {
8+
'use strict';
9+
10+
var WidgetDirectiveAbstract,
11+
injector = new Squire(),
12+
mocks = {
13+
'Magento_PageBuilder/js/utils/object': {
14+
get: function (data, key) {
15+
return data[key];
16+
},
17+
set: function (data, key, value) {
18+
data[key] = value;
19+
}
20+
}
21+
};
22+
23+
beforeEach(function (done) {
24+
injector.mock(mocks);
25+
injector.require(['Magento_PageBuilder/js/mass-converter/widget-directive-abstract'], function (module) {
26+
WidgetDirectiveAbstract = module;
27+
done();
28+
});
29+
});
30+
31+
afterEach(function () {
32+
injector.clean();
33+
});
34+
35+
describe('Magento_PageBuilder/js/mass-converter/widget-directive-abstract', function () {
36+
var model;
37+
38+
beforeEach(function () {
39+
model = new WidgetDirectiveAbstract();
40+
});
41+
42+
describe('fromDom - Multiline Widget with WYSIWYG Content', function () {
43+
it('Should parse custom widget with multiline WYSIWYG content containing links', function () {
44+
// Simulates the reported issue: custom widget with WYSIWYG field containing links
45+
var data = {
46+
content: '{{widget type="Custom\\Widget\\Test"\n' +
47+
'wysiwyg_content="<h2>Sample Product Title</h2>\n' +
48+
'<p>Description with <a href=\\"/product/sample\\">product link</a></p>"\n' +
49+
'template="widget/wysiwyg_test.phtml"}}'
50+
},
51+
config = {
52+
html_variable: 'content'
53+
},
54+
result = model.fromDom(data, config);
55+
56+
// With old regex (.*?): widget won't be parsed, returns empty object
57+
// With new regex ([\S\s]*?): widget will be parsed correctly
58+
expect(result.type).toBe('Custom\\Widget\\Test');
59+
expect(result.wysiwyg_content).toContain('Sample Product Title');
60+
expect(result.wysiwyg_content).toContain('href=\\"');
61+
expect(result.template).toBe('widget/wysiwyg_test.phtml');
62+
});
63+
});
64+
});
65+
});
66+
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Copyright 2025 Adobe
3+
* All Rights Reserved.
4+
*/
5+
6+
/* eslint-disable max-nested-callbacks */
7+
define([
8+
'Magento_PageBuilder/js/utils/editor'
9+
], function (utils) {
10+
'use strict';
11+
12+
describe('Magento_PageBuilder/js/utils/editor.js - Multiline Widget Tests', function () {
13+
14+
describe('escapeDoubleQuoteWithinWidgetDirective', function () {
15+
it('Should process multiline widget with WYSIWYG content containing links', function () {
16+
// Simulates the issue: custom widget with WYSIWYG field containing multiline content with links
17+
var content = 'Page content {{widget type="Custom\\Widget\\Test"\n' +
18+
'wysiwyg_content="<h2>Sample Product Title</h2>\n' +
19+
'<p>Description with <a href=&quot;/product/sample&quot;>product link</a></p>"\n' +
20+
'template="widget/wysiwyg_test.phtml"}} more content',
21+
result = utils.escapeDoubleQuoteWithinWidgetDirective(content);
22+
23+
// With old regex (.*?): widget won't be found, quotes won't be escaped
24+
// With new regex ([\S\s]*?): widget will be found and quotes properly escaped
25+
expect(result).toContain('href=\\"');
26+
expect(result).not.toContain('&quot;');
27+
});
28+
});
29+
30+
describe('unescapeDoubleQuoteWithinWidgetDirective', function () {
31+
it('Should process multiline widget with escaped quotes in WYSIWYG content', function () {
32+
var content = 'Page content {{widget type="Custom\\Widget\\Test"\n' +
33+
'wysiwyg_content="<h2>Sample Product Title</h2>\n' +
34+
'<p>Description with <a href=\\"product/sample\\">product link</a></p>"\n' +
35+
'template="widget/wysiwyg_test.phtml"}} more content',
36+
result = utils.unescapeDoubleQuoteWithinWidgetDirective(content);
37+
38+
// With old regex: multiline widget won't be processed
39+
// With new regex: quotes will be properly unescaped
40+
expect(result).toContain('href=&quot;');
41+
expect(result).not.toContain('\\"');
42+
});
43+
});
44+
});
45+
});
46+
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* Copyright 2025 Adobe
3+
* All Rights Reserved.
4+
*/
5+
define([
6+
'squire',
7+
'jquery'
8+
], function (Squire, $) {
9+
'use strict';
10+
11+
var nestingWidgetDialog,
12+
injector = new Squire(),
13+
mocks = {
14+
'mage/translate': function (text) {
15+
return text; // Simple mock that returns the input text
16+
},
17+
'Magento_PageBuilder/js/modal/dismissible-confirm': jasmine.createSpy('dismissibleConfirm')
18+
};
19+
20+
beforeEach(function (done) {
21+
injector.mock(mocks);
22+
injector.require(['Magento_PageBuilder/js/utils/nesting-widget-dialog'], function (module) {
23+
nestingWidgetDialog = module;
24+
done();
25+
});
26+
});
27+
28+
afterEach(function () {
29+
injector.clean();
30+
});
31+
32+
describe('Magento_PageBuilder/js/utils/nesting-widget-dialog', function () {
33+
var mockDataStore, mockWysiwyg, mockElement;
34+
35+
beforeEach(function () {
36+
// Create mock element
37+
mockElement = $('<div id="test-wysiwyg">Test content</div>');
38+
$('body').append(mockElement);
39+
40+
mockDataStore = {
41+
getState: jasmine.createSpy('getState'),
42+
set: jasmine.createSpy('set')
43+
};
44+
45+
mockWysiwyg = {
46+
elementId: 'test-wysiwyg'
47+
};
48+
49+
// Reset the spy
50+
mocks['Magento_PageBuilder/js/modal/dismissible-confirm'].calls.reset();
51+
});
52+
53+
afterEach(function () {
54+
mockElement.remove();
55+
});
56+
57+
describe('Multiline Widget Detection - WYSIWYG Content Issue', function () {
58+
it('Should detect custom widget with multiline WYSIWYG content containing links', function () {
59+
// Simulates the reported issue: custom widget with WYSIWYG field containing links
60+
var inlineMessage = 'Page content {{widget type="Custom\\Widget\\Test"\n' +
61+
'wysiwyg_content="<h2>Sample Product Title</h2>\n' +
62+
'<p>Description with <a href=\\"/product/sample\\">product link</a></p>"\n' +
63+
'template="widget/wysiwyg_test.phtml"}} more content',
64+
linkUrl = {
65+
type: 'page',
66+
page: ['page-1']
67+
},
68+
dialogConfig;
69+
70+
mockDataStore.getState.and.returnValue({
71+
'inline_message': inlineMessage,
72+
'link_url': linkUrl
73+
});
74+
75+
nestingWidgetDialog(mockDataStore, mockWysiwyg, 'inline_message', 'link_url');
76+
77+
// With old regex (.*?): multiline widget won't be detected, no dialog
78+
// With new regex ([\S\s]*?): multiline widget will be detected, dialog shown
79+
expect(mocks['Magento_PageBuilder/js/modal/dismissible-confirm']).toHaveBeenCalled();
80+
81+
// Test widget removal functionality
82+
dialogConfig = mocks['Magento_PageBuilder/js/modal/dismissible-confirm'].calls.mostRecent().args[0];
83+
dialogConfig.actions.always();
84+
85+
// Verify widget is removed from content
86+
expect(mockDataStore.set).toHaveBeenCalledWith('inline_message', 'Page content more content');
87+
});
88+
});
89+
});
90+
});

0 commit comments

Comments
 (0)