Skip to content

Commit b6f7fae

Browse files
Misplonclaude
andcommitted
Step 1: Add e2e regression test for font round-trip without focus (#84645)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 23f7fb6 commit b6f7fae

1 file changed

Lines changed: 198 additions & 0 deletions

File tree

tests/e2e/wb-form-field-block-editor.test.js

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,114 @@ const setIframeImageFieldValue = async ( page, clientId, attachmentId ) => {
208208
);
209209
};
210210

211+
// Set the headline section's font <select> value WITHOUT focusing it, mirroring
212+
// setIframeImageFieldValue. On first insert the select holds only the PHP-rendered
213+
// <option value="default">, so the target option must be injected before .value is
214+
// set; otherwise .value = 'Arial' would not stick. Setting .value directly and
215+
// dispatching a bubbling 'change' event deliberately avoids the lazy .one('focus',...)
216+
// population path that the #84645 regression test must exercise.
217+
const setIframeFontFieldValue = async ( page, clientId, fontValue ) => {
218+
return page.evaluate(
219+
( { blockClientId, value } ) => {
220+
const iframe = document.querySelector( 'iframe[name="editor-canvas"], .edit-site-visual-editor__editor-canvas' );
221+
const frameWindow = iframe && iframe.contentWindow ? iframe.contentWindow : window;
222+
const frameDocument = frameWindow.document;
223+
const form = frameDocument
224+
.querySelector( `[data-block="${ blockClientId }"] .siteorigin-widget-form.siteorigin-widget-form-main` );
225+
226+
if ( ! form ) {
227+
return null;
228+
}
229+
230+
const fontSelect = Array.from(
231+
form.querySelectorAll( 'select.siteorigin-widget-input' )
232+
).find( ( select ) => /\[headline\]\[font\]$/.test( select.name ) );
233+
234+
if ( ! fontSelect ) {
235+
return null;
236+
}
237+
238+
// Inject the target option if it is not already present, since the
239+
// select initially holds only the default option (full list is lazy).
240+
const hasOption = Array.from( fontSelect.options ).some( ( option ) => option.value === value );
241+
if ( ! hasOption ) {
242+
const option = frameDocument.createElement( 'option' );
243+
option.value = value;
244+
option.textContent = value;
245+
fontSelect.appendChild( option );
246+
}
247+
248+
// Set the value directly and dispatch a bubbling change event. No focus.
249+
fontSelect.value = value;
250+
fontSelect.dispatchEvent( new frameWindow.Event( 'change', { bubbles: true } ) );
251+
252+
return fontSelect.value;
253+
},
254+
{
255+
blockClientId: clientId,
256+
value: fontValue,
257+
}
258+
);
259+
};
260+
261+
// Read the headline font <select> DOM state WITHOUT focusing it: how many options
262+
// are currently checked, the checked value, how many options match the target font,
263+
// and how many still carry the data-sow-saved-option marker.
264+
const getIframeFontFieldReport = async ( page, clientId, fontValue ) => {
265+
return page.evaluate(
266+
( { blockClientId, value } ) => {
267+
const iframe = document.querySelector( 'iframe[name="editor-canvas"], .edit-site-visual-editor__editor-canvas' );
268+
const frameWindow = iframe && iframe.contentWindow ? iframe.contentWindow : window;
269+
const frameDocument = frameWindow.document;
270+
const form = frameDocument
271+
.querySelector( `[data-block="${ blockClientId }"] .siteorigin-widget-form.siteorigin-widget-form-main` );
272+
273+
if ( ! form ) {
274+
return null;
275+
}
276+
277+
const fontSelect = Array.from(
278+
form.querySelectorAll( 'select.siteorigin-widget-input' )
279+
).find( ( select ) => /\[headline\]\[font\]$/.test( select.name ) );
280+
281+
if ( ! fontSelect ) {
282+
return null;
283+
}
284+
285+
const checked = fontSelect.querySelector( 'option:checked' );
286+
287+
return {
288+
optionCount: fontSelect.options.length,
289+
checkedCount: fontSelect.querySelectorAll( 'option:checked' ).length,
290+
checkedValue: checked ? checked.value : null,
291+
matchingCount: Array.from( fontSelect.options ).filter( ( option ) => option.value === value ).length,
292+
savedMarkerCount: fontSelect.querySelectorAll( 'option[data-sow-saved-option]' ).length,
293+
};
294+
},
295+
{
296+
blockClientId: clientId,
297+
value: fontValue,
298+
}
299+
);
300+
};
301+
302+
// Focus the headline font <select> via a Playwright locator to trigger the lazy
303+
// .one('focus',...) population, then wait for the full fontList to have been
304+
// appended (more than the default + saved options).
305+
const focusIframeFontFieldAndWaitForList = async ( page, admin, blockName ) => {
306+
const widget = getWidgetBlock( admin, blockName );
307+
const fontSelect = widget.locator( 'select.siteorigin-widget-input[name$="[headline][font]"]' );
308+
309+
await fontSelect.focus();
310+
311+
await expect
312+
.poll(
313+
async () => fontSelect.evaluate( ( select ) => select.options.length ),
314+
{ timeout: 20000 }
315+
)
316+
.toBeGreaterThan( 2 );
317+
};
318+
211319
const attachBlockDiagnostics = async ( testInfo, name, state ) => {
212320
await testInfo.attach( name, {
213321
body: JSON.stringify( state, null, 2 ),
@@ -944,3 +1052,93 @@ test(
9441052
}
9451053
}
9461054
);
1055+
1056+
// ---------------------------------------------------------------------------
1057+
// Regression — #84645 font round-trip
1058+
// A saved, non-default font on a never-focused font <select> must survive the
1059+
// save/reopen round-trip (the field is read back via getWidgetFormValues without
1060+
// the user ever focusing it) and must not regress to a duplicate option once the
1061+
// full font list lazily loads on focus. This locks in both Slice-1 commits:
1062+
// the PHP pre-rendered, marked saved <option> and the JS marker strip in
1063+
// setupFontField.
1064+
// ---------------------------------------------------------------------------
1065+
1066+
test(
1067+
'Headline widget saved font survives save and reopen without focusing the field, with no duplicate option after focus.',
1068+
async ( { page }, testInfo ) => {
1069+
const blockName = 'sowb/siteorigin-widget-headline-widget';
1070+
const fontValue = 'Arial';
1071+
const {
1072+
admin,
1073+
post,
1074+
requestUtils,
1075+
} = await setupPublishedPostEditor( page, 'WB headline font round-trip' );
1076+
1077+
try {
1078+
// B. Insert the widget and set the font to Arial WITHOUT focusing the select.
1079+
const widget = await insertDirectWidgetBlock( admin, blockName );
1080+
const clientId = await widget.getAttribute( 'data-block' );
1081+
expect( clientId ).toBeTruthy();
1082+
1083+
const setFontValue = await setIframeFontFieldValue( page, clientId, fontValue );
1084+
expect( setFontValue ).toBe( fontValue );
1085+
1086+
// Dirty the post so Save is enabled (mirrors the image widget test).
1087+
await page.evaluate( () => {
1088+
window.wp.data.dispatch( 'core/editor' ).editPost( {
1089+
title: 'WB headline font round-trip updated',
1090+
} );
1091+
} );
1092+
1093+
// C. Pre-save in-editor assertion: the data path before save reads Arial.
1094+
const formSnapshot = await getIframeWidgetFormValues( page, clientId );
1095+
expect( formSnapshot.formCount ).toBeGreaterThan( 0 );
1096+
expect( formSnapshot.values.headline.font ).toBe( fontValue );
1097+
1098+
// D. Save and capture content.
1099+
const savedContent = await clickSaveAndCaptureContent( page, post.id );
1100+
expect( savedContent ).toContain( '"font":"Arial"' );
1101+
1102+
const postSaveBlockState = await findDirectBlockState( page, blockName );
1103+
expect( postSaveBlockState.attributes.widgetData.headline.font ).toBe( fontValue );
1104+
await attachBlockDiagnostics( testInfo, 'headline-font-attrs.json', postSaveBlockState );
1105+
1106+
// E. Reopen the post and the saved widget form — Slice-1's PHP pre-render
1107+
// is under test from here on.
1108+
await admin.editPost( post.id );
1109+
await reopenSavedWidgetForm( page, admin, blockName );
1110+
const reloadedBlockState = await findDirectBlockState( page, blockName );
1111+
1112+
// F. Assertion (1) — BEFORE focusing the font select, the DOM has exactly
1113+
// one selected option and it is Arial.
1114+
const preFocusReport = await getIframeFontFieldReport( page, reloadedBlockState.clientId, fontValue );
1115+
expect( preFocusReport ).not.toBeNull();
1116+
expect( preFocusReport.checkedCount ).toBe( 1 );
1117+
expect( preFocusReport.checkedValue ).toBe( fontValue );
1118+
1119+
// G. Assertion (2) — the data round-trip that regressed: getWidgetFormValues
1120+
// returns Arial WITHOUT focusing the field.
1121+
const reloadedSnapshot = await getIframeWidgetFormValues( page, reloadedBlockState.clientId );
1122+
expect( reloadedSnapshot.values.headline.font ).toBe( fontValue );
1123+
expect( reloadedSnapshot.values.headline.font ).not.toBe( 'default' );
1124+
1125+
// H. Assertion (3) — AFTER focusing the reopened select, still exactly one
1126+
// selected, still Arial, no duplicate Arial option, marker stripped.
1127+
await focusIframeFontFieldAndWaitForList( page, admin, blockName );
1128+
1129+
const postFocusReport = await getIframeFontFieldReport( page, reloadedBlockState.clientId, fontValue );
1130+
expect( postFocusReport ).not.toBeNull();
1131+
expect( postFocusReport.checkedCount ).toBe( 1 );
1132+
expect( postFocusReport.checkedValue ).toBe( fontValue );
1133+
expect( postFocusReport.matchingCount ).toBe( 1 );
1134+
expect( postFocusReport.savedMarkerCount ).toBe( 0 );
1135+
await attachBlockDiagnostics( testInfo, 'headline-font-after-focus.json', postFocusReport );
1136+
} finally {
1137+
await requestUtils.rest( {
1138+
method: 'DELETE',
1139+
path: `/wp/v2/posts/${ post.id }`,
1140+
params: { force: true },
1141+
} ).catch( () => {} );
1142+
}
1143+
}
1144+
);

0 commit comments

Comments
 (0)