Skip to content
Merged
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
2 changes: 1 addition & 1 deletion e2e/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Fail if a panel still shows `ProControl` / "Get Premium" while premium is mocked
|---------|------|
| Design System | Sidebar opens; Preview; Color Schemes / Font pairs / Size presets / Icon Library are live controls |
| Global Block Styles | Save a named style from one Text block, apply it to a second; canvas uses the style, inspector stays at defaults and can override; rename and delete from Design System; updating a style applies the change to other blocks that use it |
| Dynamic Content | Post title / meta / featured image resolve on the frontend |
| Dynamic Content | Post title / meta / featured image resolve on the frontend; custom date format apply path (`e2e/tests/dynamic-content-custom-date.spec.ts`) |
| Conditional display | Logged-in condition visible on frontend; logged-out condition hidden while logged in |
| Motion / Transform / Custom CSS | Entrance class, hover transform CSS, applied custom CSS on frontend |
| Columns / Posts / Image / Icon / Separator | Arrangement (2+ columns), Offset after layout pick, circle shape, gradient, extra separator layer |
Expand Down
97 changes: 97 additions & 0 deletions e2e/tests/dynamic-content-custom-date.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {
test,
expect,
openInspectorPanel,
publishAndVisitFrontend,
waitForBlockEditor,
} from 'e2e/test-utils'

// Issue #3748 / PR #3754: applying a custom date format writes `&` into
// `data-stk-dynamic`. Typography must keep that generated span, or the
// editor and frontend show escaped HTML instead of the formatted date.
//
// Premium suite only (see playwright.premium.config.js). Dynamic Fields
// apply lives in premium; the escape bug is in free typography.
test.describe( 'Dynamic Content custom date format', () => {
const createdPostIds: Array<string | number> = []

test.afterEach( async ( { requestUtils } ) => {
for ( const id of createdPostIds.splice( 0 ) ) {
await requestUtils.deletePost( id ).catch( () => undefined )
}
} )

test( 'custom formatted post date renders as a date, not escaped HTML', async ( {
page,
admin,
editor,
requestUtils,
stackable,
} ) => {
const post = await requestUtils.createPost( {
title: 'DC Custom Date Format',
status: 'draft',
date: '2026-03-15T12:00:00',
date_gmt: '2026-03-15T12:00:00',
} )
createdPostIds.push( post.id )

await admin.editPost( String( post.id ) )
await stackable.dismissToursAndNotices()
await waitForBlockEditor( editor )

await editor.insertBlock( { name: 'stackable/text' } )
await stackable.pickDefaultLayout( editor )
await stackable.selectBlockByName( editor, 'stackable/text' )

await stackable.openInspectorTab( 'Style' )
await openInspectorPanel( page, 'Typography' )

const inspector = page.getByRole( 'region', { name: 'Editor settings' } )
const contentControl = inspector.locator( '.stk-control' ).filter( {
has: page.locator( '.stk-control-label', { hasText: /^Content$/ } ),
} )
await contentControl.locator( '.stk-dynamic-content-control__button' ).click()

const popover = page.locator( '.stackable-dynamic-content__popover' )
await expect( popover ).toBeVisible()
await expect( popover.getByRole( 'button', { name: 'Apply' } ) ).toBeVisible()

const fieldControl = popover.locator( '.ugb-advanced-autosuggest-control' )
.filter( { hasText: /^Field$/ } )
await fieldControl.locator( 'input' ).click()
await page.locator( '.ugb-autosuggest-option[data-value="post-date"]' ).click()

await expect( popover.getByLabel( 'Date Format' ) ).toBeVisible()
await popover.getByLabel( 'Date Format' ).selectOption( 'custom' )
await popover.getByLabel( 'Custom Format' ).fill( 'F' )
await expect( popover.getByRole( 'button', { name: 'Apply' } ) ).toBeEnabled()
await popover.getByRole( 'button', { name: 'Apply' } ).click()
await expect( popover ).toBeHidden()

const canvasBlock = editor.canvas.locator( '[data-type="stackable/text"]' ).first()
await expect( canvasBlock ).toHaveText( 'March' )
await expect( canvasBlock ).not.toContainText( 'data-stk-dynamic' )
await expect( canvasBlock ).not.toContainText( '<span' )

const clientId = await canvasBlock.getAttribute( 'data-block' )
let uniqueId = ''
await expect( async () => {
const attributes = await editor.getBlockAttributes( clientId )
uniqueId = attributes.uniqueId
expect( attributes.text ).toContain( '<span data-stk-dynamic' )
expect( attributes.text ).toContain( 'custom_format=F' )
expect( attributes.text ).not.toContain( '&lt;span' )
} ).toPass( { intervals: [ 1_000, 2_000, 5_000 ] } )
expect( uniqueId ).toBeTruthy()

await publishAndVisitFrontend( page, editor, requestUtils, admin )

const block = page.locator( `[data-block-id="${ uniqueId }"]` )
await expect( block ).toBeVisible()
await expect( block ).toHaveText( 'March' )
await expect( block ).not.toContainText( 'data-stk-dynamic' )
await expect( block ).not.toContainText( '<span' )
await expect( block.locator( '.stk-dynamic-content' ) ).toHaveCount( 0 )
} )
} )
2 changes: 2 additions & 0 deletions playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const E2E_META_MU = path.join(

module.exports = defineConfig( {
testDir: './e2e/tests',
// Premium Dynamic Content apply path. Free CI has no DC popover.
testIgnore: [ '**/dynamic-content-custom-date.spec.ts' ],
globalSetup: require.resolve( './e2e/config/global-setup.js' ),
fullyParallel: false,
forbidOnly: !! process.env.CI,
Expand Down
6 changes: 5 additions & 1 deletion playwright.premium.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ const E2E_META_MU = path.join(
)

module.exports = defineConfig( {
testDir: './pro__premium_only/e2e/tests',
testDir: '.',
testMatch: [
'pro__premium_only/e2e/tests/**/*.spec.ts',
'e2e/tests/dynamic-content-custom-date.spec.ts',
],
globalSetup: require.resolve( './e2e/config/global-setup.js' ),
fullyParallel: false,
forbidOnly: !! process.env.CI,
Expand Down
5 changes: 3 additions & 2 deletions src/block-components/typography/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,12 @@ export const Controls = props => {
value={ unescape( text ) }
onChange={ onChangeContent }
/**
* Pass the unescaped Dynamic Content `onChange` function.
* Dynamic Content generates this markup itself. Preserve it because
* HTML validation normalizes string ampersands in its attribute.
*
* @param {string} text Text with dynamic content.
*/
changeDynamicContent={ onChangeContent }
changeDynamicContent={ text => updateAttribute( 'text', text ) }
isDynamic={ true }
/>
) }
Expand Down
Loading