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 = ` +

FSE template styles

+` + +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 => {