diff --git a/e2e/tests/site-editor.spec.ts b/e2e/tests/site-editor.spec.ts
new file mode 100644
index 000000000..13c50a3e9
--- /dev/null
+++ b/e2e/tests/site-editor.spec.ts
@@ -0,0 +1,155 @@
+import { test, expect } from 'e2e/test-utils'
+
+const TEMPLATE_SLUG = 'stk-e2e-fse-constructable-styles'
+const TEMPLATE_TITLE = 'STK E2E FSE Styles'
+const EDITOR_CRASH = 'The editor has encountered an unexpected error.'
+const STYLESHEET_ERROR = /adoptedStyleSheets|Sharing constructed stylesheets in multiple documents/i
+
+const TEMPLATE_TEXT_BLOCK = `
+
+`
+
+const activateBlockTheme = async requestUtils => {
+ const themes = await requestUtils.rest( { path: '/wp/v2/themes' } )
+ const active = themes.find( theme => theme.status === 'active' )
+ if ( active?.is_block_theme ) {
+ return
+ }
+
+ for ( const slug of [ 'twentytwentyfive', 'twentytwentyfour' ] ) {
+ try {
+ await requestUtils.activateTheme( slug )
+ return
+ } catch {
+ // Try the next bundled block theme.
+ }
+ }
+
+ throw new Error( 'Site Editor e2e needs a block theme (Twenty Twenty-Five or Twenty Twenty-Four).' )
+}
+
+const templateTitle = template =>
+ typeof template.title === 'string' ? template.title : template.title?.rendered || template.title?.raw || ''
+
+const deleteE2eTemplate = async requestUtils => {
+ try {
+ const templates = await requestUtils.rest( { path: '/wp/v2/templates' } )
+ for ( const template of templates ) {
+ if ( ! template.wp_id ) {
+ continue
+ }
+ if ( template.slug !== TEMPLATE_SLUG && templateTitle( template ) !== TEMPLATE_TITLE ) {
+ continue
+ }
+ await requestUtils.rest( {
+ method: 'DELETE',
+ path: `/wp/v2/templates/${ template.id }`,
+ params: { force: true },
+ } )
+ }
+ } catch {
+ // Playground may already be gone during teardown.
+ }
+}
+
+const templatesSidebar = page =>
+ page.locator( '.edit-site-layout__sidebar, .edit-site-sidebar-dataviews, .edit-site-sidebar-navigation-screen' )
+
+const sidebarItem = ( page, name ) =>
+ templatesSidebar( page ).getByRole( 'button', { name, exact: true } )
+ .or( templatesSidebar( page ).getByRole( 'link', { name, exact: true } ) )
+ .or( templatesSidebar( page ).getByText( name, { exact: true } ) )
+ .or( page.getByRole( 'button', { name, exact: true } ) )
+ .or( page.getByRole( 'link', { name, exact: true } ) )
+
+test.describe( 'Site Editor', () => {
+ test.afterEach( async ( { requestUtils } ) => {
+ await deleteE2eTemplate( requestUtils )
+ } )
+
+ test( 'switching to user templates after editing one does not crash the Site Editor', async ( {
+ page,
+ admin,
+ editor,
+ requestUtils,
+ stackable,
+ } ) => {
+ test.setTimeout( 120_000 )
+
+ let stylesheetError = ''
+ page.on( 'pageerror', error => {
+ if ( STYLESHEET_ERROR.test( error.message ) ) {
+ stylesheetError = error.message
+ }
+ } )
+ page.on( 'console', message => {
+ if ( message.type() === 'error' && STYLESHEET_ERROR.test( message.text() ) ) {
+ stylesheetError = message.text()
+ }
+ } )
+
+ await activateBlockTheme( requestUtils )
+ await deleteE2eTemplate( requestUtils )
+
+ const author = process.env.WP_USERNAME || 'admin'
+ const template = await requestUtils.createTemplate( 'wp_template', {
+ slug: TEMPLATE_SLUG,
+ title: TEMPLATE_TITLE,
+ content: TEMPLATE_TEXT_BLOCK,
+ } )
+ expect( template.wp_id ).toBeTruthy()
+
+ await admin.visitAdminPage( 'site-editor.php', '' )
+ await stackable.dismissToursAndNotices()
+ await editor.setPreferences( 'core/edit-site', {
+ welcomeGuide: false,
+ welcomeGuideStyles: false,
+ welcomeGuidePage: false,
+ welcomeGuideTemplate: false,
+ } )
+
+ const templatesNav = page.getByRole( 'button', { name: 'Templates', exact: true } )
+ .or( page.getByRole( 'link', { name: 'Templates', exact: true } ) )
+ await expect( templatesNav.first() ).toBeVisible( { timeout: 60_000 } )
+ await templatesNav.first().click()
+ await expect( page.getByRole( 'button', { name: 'Add Template' } ) ).toBeVisible( { timeout: 30_000 } )
+ await expect( page.getByText( TEMPLATE_TITLE, { exact: true } ).first() ).toBeVisible( { timeout: 30_000 } )
+
+ await sidebarItem( page, author ).first().click()
+ await expect( page.getByText( `Author is: ${ author }` ) ).toBeVisible( { timeout: 30_000 } )
+
+ await page.getByText( TEMPLATE_TITLE, { exact: true } ).first().click()
+ await expect( editor.canvas.getByText( 'FSE template styles' ) ).toBeVisible( { timeout: 30_000 } )
+
+ const welcome = page.getByRole( 'button', { name: 'Get started' } )
+ if ( await welcome.isVisible().catch( () => false ) ) {
+ await welcome.click()
+ }
+ await page.locator( '.components-modal__screen-overlay' ).waitFor( { state: 'hidden', timeout: 5_000 } ).catch( () => {} )
+
+ const editorBack = page.locator( '.editor-header__back-button button, .editor-header__back-button a, .editor-header__back-button [role="button"]' )
+ .or( page.getByRole( 'button', { name: 'Back', exact: true } ) )
+ await expect( editorBack.first() ).toBeVisible( { timeout: 15_000 } )
+ await editorBack.first().click()
+
+ await expect( page.getByRole( 'button', { name: 'Add Template' } ) ).toBeVisible( { timeout: 30_000 } )
+
+ await sidebarItem( page, 'All templates' ).first().click()
+ await sidebarItem( page, author ).first().click()
+
+ const crash = page.getByText( EDITOR_CRASH )
+ const deadline = Date.now() + 8_000
+ while ( Date.now() < deadline && ! stylesheetError ) {
+ if ( await crash.isVisible().catch( () => false ) ) {
+ stylesheetError = EDITOR_CRASH
+ break
+ }
+ await page.waitForTimeout( 200 )
+ }
+
+ expect(
+ stylesheetError,
+ stylesheetError || 'Site Editor crashed after switching to user templates'
+ ).toBe( '' )
+ } )
+} )
diff --git a/src/components/block-css/use-block-style-generator.js b/src/components/block-css/use-block-style-generator.js
index c3b43c3ba..ec7bc84ea 100644
--- a/src/components/block-css/use-block-style-generator.js
+++ b/src/components/block-css/use-block-style-generator.js
@@ -2,7 +2,9 @@ import { useQueryLoopInstanceId } from '~stackable/util'
import {
useLayoutEffect, useMemo, useRef,
} from '@wordpress/element'
-import { dispatch, select } from '@wordpress/data'
+import {
+ dispatch, select, useSelect,
+} from '@wordpress/data'
import { useRafEffect } from '~stackable/hooks'
import CssSaveCompiler from './css-save-compiler'
import { createStyleDependencyFingerprint } from './util'
@@ -79,6 +81,18 @@ export const useBlockCssGenerator = props => {
}, [ styleFingerprint, version, blockStyles, setAttributes ] )
const styleKey = `${ clientId }-${ instanceId }`
+ const editorDom = useSelect( select => {
+ return select( 'stackable/editor-dom' )?.getEditorDom()
+ } )
+
+ // Returning null for every block left template-preview iframes without CSS.
+ // Use the unified stylesheet only for a current editor document, otherwise
+ // return CSS so each preview is styled inside its own document.
+ const editorCanvasDocument = document.querySelector( 'iframe[name="editor-canvas"]' )?.contentDocument
+ const isCurrentEditorDom = editorDom?.isConnected && (
+ editorDom.ownerDocument === document ||
+ editorDom.ownerDocument === editorCanvasDocument
+ )
useLayoutEffect( () => {
dispatch( 'stackable/editor-block-css' ).setBlockCss( styleKey, editCss || '' )
@@ -92,7 +106,5 @@ export const useBlockCssGenerator = props => {
}
}, [ styleKey, editCss, clientId ] )
- // We used to return the CSS here, but for optimization, now
- // CSS is injected via the unified editor stylesheet plugin.
- return null
+ return isCurrentEditorDom ? null : editCss
}
diff --git a/src/plugins/editor-block-css/block-style-sheets.js b/src/plugins/editor-block-css/block-style-sheets.js
index 5aa3dc7d8..3b165a396 100644
--- a/src/plugins/editor-block-css/block-style-sheets.js
+++ b/src/plugins/editor-block-css/block-style-sheets.js
@@ -59,9 +59,8 @@ export const shouldUseConstructableStyleSheets = editorDom => {
}
const targetDoc = getTargetEditorDocument( editorDom )
- const canvasDoc = getEditorCanvasDocument()
- return ! canvasDoc || targetDoc === document
+ return targetDoc === document
}
const getFallbackStyleId = key => {