From 27094bc3fea191700358faaa75a8b3afb73e1dab Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Jan 2026 12:51:57 +0000 Subject: [PATCH 1/3] Add parallax support to PHP render_block filter The JavaScript code already supported parallax attributes (parallaxEnabled, parallaxStrength) for static block output, but the PHP render_block filter was missing this support for dynamic blocks on the frontend. This change ensures that when parallax is enabled on a block: - The data-parallax and data-parallax-strength attributes are added - The --parallax-strength CSS custom property is set inline - Frontend styles are properly enqueued This was identified as a missing feature from the READING-PROGRESS-BAR.md roadmap documentation. --- my-scroll-block.php | 99 +++++++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 31 deletions(-) diff --git a/my-scroll-block.php b/my-scroll-block.php index 6f24221..4708f6c 100644 --- a/my-scroll-block.php +++ b/my-scroll-block.php @@ -99,59 +99,96 @@ function my_scroll_block_register_assets() { return $block_content; } $attrs = $block['attrs']; - if ( isset( $attrs['animationType'] ) && 'none' !== $attrs['animationType'] ) { + + $has_animation = isset( $attrs['animationType'] ) && 'none' !== $attrs['animationType']; + $parallax_enabled = isset( $attrs['parallaxEnabled'] ) && $attrs['parallaxEnabled']; + + if ( $has_animation || $parallax_enabled ) { if ( wp_style_is( 'my-scroll-block-style', 'registered' ) ) { wp_enqueue_style( 'my-scroll-block-style' ); } // No view script on frontend when relying on CSS scroll timelines. // Also ensure outer wrapper receives classes and attributes if missing (covers dynamic blocks). - if ( is_string( $block_content ) && $block_content !== '' && strpos( $block_content, 'scroll-anim-block' ) === false ) { - $animation_type = sanitize_key( (string) $attrs['animationType'] ); - $animation_range = isset( $attrs['animationRange'] ) ? sanitize_key( (string) $attrs['animationRange'] ) : 'default'; - - $add_classes = sprintf( 'scroll-anim-block scroll-anim-%s', strtolower( str_replace( ' ', '-', $animation_type ) ) ); - - // Build data attributes - $data_attrs = ' data-scroll-anim="1" data-anim-range="' . esc_attr( $animation_range ) . '"'; - - // Add custom range values if using custom range - if ( $animation_range === 'custom' ) { - if ( isset( $attrs['animationEntryStart'] ) ) { - $data_attrs .= ' data-entry-start="' . absint( $attrs['animationEntryStart'] ) . '"'; - } - if ( isset( $attrs['animationEntryEnd'] ) ) { - $data_attrs .= ' data-entry-end="' . absint( $attrs['animationEntryEnd'] ) . '"'; - } - // Add exit range for in-out animations - if ( strpos( $animation_type, 'in-out' ) !== false ) { - if ( isset( $attrs['animationExitStart'] ) ) { - $data_attrs .= ' data-exit-start="' . absint( $attrs['animationExitStart'] ) . '"'; + $needs_injection = is_string( $block_content ) && $block_content !== ''; + $missing_anim = $has_animation && strpos( $block_content, 'scroll-anim-block' ) === false; + $missing_parallax = $parallax_enabled && strpos( $block_content, 'data-parallax' ) === false; + + if ( $needs_injection && ( $missing_anim || $missing_parallax ) ) { + $add_classes = ''; + $data_attrs = ''; + $style_attr = ''; + + // Handle animation classes and data attributes + if ( $has_animation && $missing_anim ) { + $animation_type = sanitize_key( (string) $attrs['animationType'] ); + $animation_range = isset( $attrs['animationRange'] ) ? sanitize_key( (string) $attrs['animationRange'] ) : 'default'; + + $add_classes = sprintf( 'scroll-anim-block scroll-anim-%s', strtolower( str_replace( ' ', '-', $animation_type ) ) ); + + // Build data attributes + $data_attrs = ' data-scroll-anim="1" data-anim-range="' . esc_attr( $animation_range ) . '"'; + + // Add custom range values if using custom range + if ( $animation_range === 'custom' ) { + if ( isset( $attrs['animationEntryStart'] ) ) { + $data_attrs .= ' data-entry-start="' . absint( $attrs['animationEntryStart'] ) . '"'; + } + if ( isset( $attrs['animationEntryEnd'] ) ) { + $data_attrs .= ' data-entry-end="' . absint( $attrs['animationEntryEnd'] ) . '"'; } - if ( isset( $attrs['animationExitEnd'] ) ) { - $data_attrs .= ' data-exit-end="' . absint( $attrs['animationExitEnd'] ) . '"'; + // Add exit range for in-out animations + if ( strpos( $animation_type, 'in-out' ) !== false ) { + if ( isset( $attrs['animationExitStart'] ) ) { + $data_attrs .= ' data-exit-start="' . absint( $attrs['animationExitStart'] ) . '"'; + } + if ( isset( $attrs['animationExitEnd'] ) ) { + $data_attrs .= ' data-exit-end="' . absint( $attrs['animationExitEnd'] ) . '"'; + } } } } + // Handle parallax data attributes and styles + if ( $parallax_enabled && $missing_parallax ) { + $parallax_strength = isset( $attrs['parallaxStrength'] ) ? absint( $attrs['parallaxStrength'] ) : 50; + $data_attrs .= ' data-parallax="1" data-parallax-strength="' . $parallax_strength . '"'; + $style_attr = '--parallax-strength: ' . $parallax_strength . 'px;'; + } + // Inject into first element tag. if ( preg_match( '/^\s*<([a-zA-Z0-9:-]+)([^>]*)>/', $block_content, $m, PREG_OFFSET_CAPTURE ) ) { $full = $m[0][0]; $attrsStr = $m[2][0]; $updated = $attrsStr; - if ( preg_match( '/\sclass\s*=\s*"([^"]*)"/i', $attrsStr, $cm ) ) { - $newClass = trim( $cm[1] . ' ' . $add_classes ); - $updated = preg_replace( '/\sclass\s*=\s*"([^"]*)"/i', ' class="' . esc_attr( $newClass ) . '"', $updated, 1 ); - } else { - $updated .= ' class="' . esc_attr( $add_classes ) . '"'; + // Add or merge classes + if ( $add_classes ) { + if ( preg_match( '/\sclass\s*=\s*"([^"]*)"/i', $attrsStr, $cm ) ) { + $newClass = trim( $cm[1] . ' ' . $add_classes ); + $updated = preg_replace( '/\sclass\s*=\s*"([^"]*)"/i', ' class="' . esc_attr( $newClass ) . '"', $updated, 1 ); + } else { + $updated .= ' class="' . esc_attr( $add_classes ) . '"'; + } } - if ( strpos( $updated, 'data-scroll-anim' ) === false ) { + // Add data attributes + if ( $data_attrs && strpos( $updated, 'data-scroll-anim' ) === false && strpos( $updated, 'data-parallax' ) === false ) { $updated .= $data_attrs; } - $newOpen = '<' . $m[1][0] . $updated . '>'; + // Add or merge style attribute for parallax + if ( $style_attr ) { + if ( preg_match( '/\sstyle\s*=\s*"([^"]*)"/i', $attrsStr, $sm ) ) { + $existing_style = rtrim( $sm[1], '; ' ); + $new_style = $existing_style . '; ' . $style_attr; + $updated = preg_replace( '/\sstyle\s*=\s*"([^"]*)"/i', ' style="' . esc_attr( $new_style ) . '"', $updated, 1 ); + } else { + $updated .= ' style="' . esc_attr( $style_attr ) . '"'; + } + } + + $newOpen = '<' . $m[1][0] . $updated . '>'; $block_content = substr_replace( $block_content, $newOpen, $m[0][1], strlen( $full ) ); } } From cc26b0f451d67c988b87e8bb52ee3d26bf74760d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Jan 2026 12:52:41 +0000 Subject: [PATCH 2/3] Apply code formatting from linters Ran npm run lint:js and npm run lint:css to apply consistent code formatting across the codebase per project conventions. --- playwright.config.js | 76 ++-- src/editor.css | 48 +-- src/index.js | 771 ++++++++++++++++++---------------- src/progress-block/editor.css | 38 +- src/progress-block/index.js | 277 ++++++------ src/progress-block/style.css | 218 +++++----- src/style.css | 619 ++++++++++++++------------- tests/global-setup.ts | 84 ++-- tests/global-teardown.ts | 16 +- tests/reduced-motion.spec.ts | 726 ++++++++++++++++---------------- tests/scroll-block.spec.ts | 622 +++++++++++++-------------- webpack.config.js | 28 +- 12 files changed, 1796 insertions(+), 1727 deletions(-) diff --git a/playwright.config.js b/playwright.config.js index 945828e..4b7de00 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -4,54 +4,54 @@ import { defineConfig, devices } from '@playwright/test'; * @see https://playwright.dev/docs/test-configuration */ export default defineConfig({ - testDir: './tests', + testDir: './tests', - /* Global setup to start WordPress Playground */ - globalSetup: './tests/global-setup.ts', - globalTeardown: './tests/global-teardown.ts', + /* Global setup to start WordPress Playground */ + globalSetup: './tests/global-setup.ts', + globalTeardown: './tests/global-teardown.ts', - /* Run tests in files in parallel */ - fullyParallel: true, + /* Run tests in files in parallel */ + fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://127.0.0.1:9400', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://127.0.0.1:9400', - /* Maximum time for each action (e.g. click, fill, etc.) */ - actionTimeout: 5000, + /* Maximum time for each action (e.g. click, fill, etc.) */ + actionTimeout: 5000, - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, - /* Maximum time one test can run for */ - timeout: 30000, + /* Maximum time one test can run for */ + timeout: 30000, - /* Configure projects for Chromium only */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - ], + /* Configure projects for Chromium only */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:9400', - // reuseExistingServer: !process.env.CI, - // }, + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:9400', + // reuseExistingServer: !process.env.CI, + // }, }); diff --git a/src/editor.css b/src/editor.css index 2f14c21..737683c 100644 --- a/src/editor.css +++ b/src/editor.css @@ -1,30 +1,30 @@ /* Animation indicator icon */ .scroll-anim-indicator-wrapper { - position: relative; + position: relative; - .scroll-anim-indicator { - position: absolute; - top: 0; - left: 0; - background: #ff69b4; - border-radius: 100%; - aspect-ratio: 1; - width: 24px; - padding: 5px; - display: flex; - justify-content: center; - align-items: center; - transform: translate(-50%, -50%); - opacity: 0.3; - transition: opacity 0.2s ease; + .scroll-anim-indicator { + position: absolute; + top: 0; + left: 0; + background: #ff69b4; + border-radius: 100%; + aspect-ratio: 1; + width: 24px; + padding: 5px; + display: flex; + justify-content: center; + align-items: center; + transform: translate(-50%, -50%); + opacity: 0.3; + transition: opacity 0.2s ease; - &:hover { - opacity: 1; - } + &:hover { + opacity: 1; + } - svg { - width: 12px; - height: 12px; - } - } + svg { + width: 12px; + height: 12px; + } + } } diff --git a/src/index.js b/src/index.js index 59faffd..d02a2ef 100644 --- a/src/index.js +++ b/src/index.js @@ -15,405 +15,440 @@ import './progress-block/index.js'; * Internal dependencies */ const SUPPORTED_BLOCKS = [ - 'core/image', - 'core/paragraph', - 'core/columns', - 'core/group', - 'core/heading', + 'core/image', + 'core/paragraph', + 'core/columns', + 'core/group', + 'core/heading', ]; const ANIMATION_OPTIONS = [ - { label: __('None', 'my-scroll-block'), value: 'none' }, - { label: __('Fade In', 'my-scroll-block'), value: 'fade-in' }, - { label: __('Slide In Left', 'my-scroll-block'), value: 'slide-in-left' }, - { label: __('Slide In Right', 'my-scroll-block'), value: 'slide-in-right' }, - { label: __('Slide In Up', 'my-scroll-block'), value: 'slide-in-up' }, - { label: __('Slide In Down', 'my-scroll-block'), value: 'slide-in-down' }, - { label: __('Scale Up', 'my-scroll-block'), value: 'scale-up' }, - { label: __('Rotate In', 'my-scroll-block'), value: 'rotate-in' }, - { label: __('Blur In', 'my-scroll-block'), value: 'blur-in' }, - { label: __('3D Rotate In', 'my-scroll-block'), value: 'rotate-3d-in' }, - { label: __('Circle Reveal', 'my-scroll-block'), value: 'circle-reveal' }, - { label: __('Curtain Reveal', 'my-scroll-block'), value: 'curtain-reveal' }, - { label: __('🔄 Fade In & Out', 'my-scroll-block'), value: 'fade-in-out' }, - { label: __('🔄 Slide Up In & Out', 'my-scroll-block'), value: 'slide-up-in-out' }, - { label: __('🔄 Scale In & Out', 'my-scroll-block'), value: 'scale-in-out' }, - { label: __('🔄 Rotate In & Out', 'my-scroll-block'), value: 'rotate-in-out' }, - { label: __('🔄 3D Rotate In & Out', 'my-scroll-block'), value: 'rotate-3d-in-out' }, + { label: __('None', 'my-scroll-block'), value: 'none' }, + { label: __('Fade In', 'my-scroll-block'), value: 'fade-in' }, + { label: __('Slide In Left', 'my-scroll-block'), value: 'slide-in-left' }, + { label: __('Slide In Right', 'my-scroll-block'), value: 'slide-in-right' }, + { label: __('Slide In Up', 'my-scroll-block'), value: 'slide-in-up' }, + { label: __('Slide In Down', 'my-scroll-block'), value: 'slide-in-down' }, + { label: __('Scale Up', 'my-scroll-block'), value: 'scale-up' }, + { label: __('Rotate In', 'my-scroll-block'), value: 'rotate-in' }, + { label: __('Blur In', 'my-scroll-block'), value: 'blur-in' }, + { label: __('3D Rotate In', 'my-scroll-block'), value: 'rotate-3d-in' }, + { label: __('Circle Reveal', 'my-scroll-block'), value: 'circle-reveal' }, + { label: __('Curtain Reveal', 'my-scroll-block'), value: 'curtain-reveal' }, + { label: __('🔄 Fade In & Out', 'my-scroll-block'), value: 'fade-in-out' }, + { label: __('🔄 Slide Up In & Out', 'my-scroll-block'), value: 'slide-up-in-out' }, + { label: __('🔄 Scale In & Out', 'my-scroll-block'), value: 'scale-in-out' }, + { label: __('🔄 Rotate In & Out', 'my-scroll-block'), value: 'rotate-in-out' }, + { label: __('🔄 3D Rotate In & Out', 'my-scroll-block'), value: 'rotate-3d-in-out' }, ]; const RANGE_OPTIONS = [ - { label: __('Default (20% - 100%)', 'my-scroll-block'), value: 'default' }, - { label: __('Quick (0% - 50%)', 'my-scroll-block'), value: 'quick' }, - { label: __('Slow (10% - 100%)', 'my-scroll-block'), value: 'slow' }, - { label: __('Late Start (50% - 100%)', 'my-scroll-block'), value: 'late' }, - { label: __('Custom', 'my-scroll-block'), value: 'custom' }, + { label: __('Default (20% - 100%)', 'my-scroll-block'), value: 'default' }, + { label: __('Quick (0% - 50%)', 'my-scroll-block'), value: 'quick' }, + { label: __('Slow (10% - 100%)', 'my-scroll-block'), value: 'slow' }, + { label: __('Late Start (50% - 100%)', 'my-scroll-block'), value: 'late' }, + { label: __('Custom', 'my-scroll-block'), value: 'custom' }, ]; // 1) Extend attributes for supported blocks. addFilter('blocks.registerBlockType', 'my-scroll-block/extend-attributes', (settings, name) => { - if (!SUPPORTED_BLOCKS.includes(name)) { - return settings; - } - return { - ...settings, - attributes: { - ...settings.attributes, - animationType: { - type: 'string', - default: 'none', - }, - animationRange: { - type: 'string', - default: 'default', - }, - animationEntryStart: { - type: 'number', - default: 20, - }, - animationEntryEnd: { - type: 'number', - default: 100, - }, - animationExitStart: { - type: 'number', - default: 0, - }, - animationExitEnd: { - type: 'number', - default: 100, - }, - parallaxEnabled: { - type: 'boolean', - default: false, - }, - parallaxStrength: { - type: 'number', - default: 50, - }, - }, - }; + if (!SUPPORTED_BLOCKS.includes(name)) { + return settings; + } + return { + ...settings, + attributes: { + ...settings.attributes, + animationType: { + type: 'string', + default: 'none', + }, + animationRange: { + type: 'string', + default: 'default', + }, + animationEntryStart: { + type: 'number', + default: 20, + }, + animationEntryEnd: { + type: 'number', + default: 100, + }, + animationExitStart: { + type: 'number', + default: 0, + }, + animationExitEnd: { + type: 'number', + default: 100, + }, + parallaxEnabled: { + type: 'boolean', + default: false, + }, + parallaxStrength: { + type: 'number', + default: 50, + }, + }, + }; }); // 2) Inject InspectorControls. const withAnimationControls = createHigherOrderComponent((BlockEdit) => { - return (props) => { - if (!SUPPORTED_BLOCKS.includes(props.name)) { - return ; - } - const { - attributes: { - animationType = 'none', - animationRange = 'default', - animationEntryStart = 20, - animationEntryEnd = 100, - animationExitStart = 0, - animationExitEnd = 100, - parallaxEnabled = false, - parallaxStrength = 50, - }, - setAttributes, - } = props; - - const isInOutAnimation = animationType.includes('in-out'); - - return ( - <> - - - setAttributes({ animationType: value })} - help={ - animationType.includes('in-out') - ? __('🔄 This animation plays on both entry and exit', 'my-scroll-block') - : '' - } - /> - - {animationType !== 'none' && ( - <> - { - const updates = { animationRange: value }; - // Set preset values - if (value === 'quick') { - updates.animationEntryStart = 0; - updates.animationEntryEnd = 50; - } else if (value === 'slow') { - updates.animationEntryStart = 10; - updates.animationEntryEnd = 100; - } else if (value === 'late') { - updates.animationEntryStart = 50; - updates.animationEntryEnd = 100; - } else if (value === 'default') { - updates.animationEntryStart = 20; - updates.animationEntryEnd = 100; - } - setAttributes(updates); - }} - help={__('When should the animation start and finish', 'my-scroll-block')} - /> - - {animationRange === 'custom' && ( - <> - setAttributes({ animationEntryStart: value })} - min={0} - max={100} - step={5} - help={__('When to start the entry animation', 'my-scroll-block')} - /> - setAttributes({ animationEntryEnd: value })} - min={0} - max={100} - step={5} - help={__('When to complete the entry animation', 'my-scroll-block')} - /> - - {isInOutAnimation && ( - <> - setAttributes({ animationExitStart: value })} - min={0} - max={100} - step={5} - help={__('When to start the exit animation', 'my-scroll-block')} - /> - setAttributes({ animationExitEnd: value })} - min={0} - max={100} - step={5} - help={__('When to complete the exit animation', 'my-scroll-block')} - /> - - )} - - )} - - )} - - setAttributes({ parallaxEnabled: value })} - help={__( - 'Adds a parallax scrolling effect to the block background or content.', - 'my-scroll-block' - )} - /> - - {parallaxEnabled && ( - setAttributes({ parallaxStrength: value })} - min={10} - max={200} - step={10} - help={__('Higher values create more movement.', 'my-scroll-block')} - /> - )} - - - - - ); - }; + return (props) => { + if (!SUPPORTED_BLOCKS.includes(props.name)) { + return ; + } + const { + attributes: { + animationType = 'none', + animationRange = 'default', + animationEntryStart = 20, + animationEntryEnd = 100, + animationExitStart = 0, + animationExitEnd = 100, + parallaxEnabled = false, + parallaxStrength = 50, + }, + setAttributes, + } = props; + + const isInOutAnimation = animationType.includes('in-out'); + + return ( + <> + + + setAttributes({ animationType: value })} + help={ + animationType.includes('in-out') + ? __( + '🔄 This animation plays on both entry and exit', + 'my-scroll-block' + ) + : '' + } + /> + + {animationType !== 'none' && ( + <> + { + const updates = { animationRange: value }; + // Set preset values + if (value === 'quick') { + updates.animationEntryStart = 0; + updates.animationEntryEnd = 50; + } else if (value === 'slow') { + updates.animationEntryStart = 10; + updates.animationEntryEnd = 100; + } else if (value === 'late') { + updates.animationEntryStart = 50; + updates.animationEntryEnd = 100; + } else if (value === 'default') { + updates.animationEntryStart = 20; + updates.animationEntryEnd = 100; + } + setAttributes(updates); + }} + help={__( + 'When should the animation start and finish', + 'my-scroll-block' + )} + /> + + {animationRange === 'custom' && ( + <> + + setAttributes({ animationEntryStart: value }) + } + min={0} + max={100} + step={5} + help={__( + 'When to start the entry animation', + 'my-scroll-block' + )} + /> + + setAttributes({ animationEntryEnd: value }) + } + min={0} + max={100} + step={5} + help={__( + 'When to complete the entry animation', + 'my-scroll-block' + )} + /> + + {isInOutAnimation && ( + <> + + setAttributes({ animationExitStart: value }) + } + min={0} + max={100} + step={5} + help={__( + 'When to start the exit animation', + 'my-scroll-block' + )} + /> + + setAttributes({ animationExitEnd: value }) + } + min={0} + max={100} + step={5} + help={__( + 'When to complete the exit animation', + 'my-scroll-block' + )} + /> + + )} + + )} + + )} + + setAttributes({ parallaxEnabled: value })} + help={__( + 'Adds a parallax scrolling effect to the block background or content.', + 'my-scroll-block' + )} + /> + + {parallaxEnabled && ( + setAttributes({ parallaxStrength: value })} + min={10} + max={200} + step={10} + help={__('Higher values create more movement.', 'my-scroll-block')} + /> + )} + + + + + ); + }; }, 'withAnimationControls'); addFilter('editor.BlockEdit', 'my-scroll-block/with-controls', withAnimationControls); // 3) Add classes/styles to the saved content markup. addFilter( - 'blocks.getSaveContent.extraProps', - 'my-scroll-block/save-props', - (extraProps, blockType, attributes) => { - if (!SUPPORTED_BLOCKS.includes(blockType.name)) { - return extraProps; - } - const { - animationType = 'none', - animationRange = 'default', - animationEntryStart = 20, - animationEntryEnd = 100, - animationExitStart = 0, - animationExitEnd = 100, - parallaxEnabled = false, - parallaxStrength = 50, - } = attributes; - - if (animationType === 'none' && !parallaxEnabled) { - return extraProps; - } - - // Class & data attributes - extraProps.className = [ - extraProps.className, - 'scroll-anim-block', - `scroll-anim-${String(animationType).replace(/\s+/g, '-').toLowerCase()}`, - ] - .filter(Boolean) - .join(' '); - - extraProps['data-scroll-anim'] = '1'; - extraProps['data-anim-range'] = animationRange; - - // Add custom range values as data attributes if using custom range - if (animationRange === 'custom') { - extraProps['data-entry-start'] = animationEntryStart; - extraProps['data-entry-end'] = animationEntryEnd; - if (animationType.includes('in-out')) { - extraProps['data-exit-start'] = animationExitStart; - extraProps['data-exit-end'] = animationExitEnd; - } - } - - if (parallaxEnabled) { - extraProps['data-parallax'] = '1'; - extraProps['data-parallax-strength'] = parallaxStrength; - extraProps.style = { - ...extraProps.style, - '--parallax-strength': `${parallaxStrength}px`, - }; - } - - return extraProps; - } + 'blocks.getSaveContent.extraProps', + 'my-scroll-block/save-props', + (extraProps, blockType, attributes) => { + if (!SUPPORTED_BLOCKS.includes(blockType.name)) { + return extraProps; + } + const { + animationType = 'none', + animationRange = 'default', + animationEntryStart = 20, + animationEntryEnd = 100, + animationExitStart = 0, + animationExitEnd = 100, + parallaxEnabled = false, + parallaxStrength = 50, + } = attributes; + + if (animationType === 'none' && !parallaxEnabled) { + return extraProps; + } + + // Class & data attributes + extraProps.className = [ + extraProps.className, + 'scroll-anim-block', + `scroll-anim-${String(animationType).replace(/\s+/g, '-').toLowerCase()}`, + ] + .filter(Boolean) + .join(' '); + + extraProps['data-scroll-anim'] = '1'; + extraProps['data-anim-range'] = animationRange; + + // Add custom range values as data attributes if using custom range + if (animationRange === 'custom') { + extraProps['data-entry-start'] = animationEntryStart; + extraProps['data-entry-end'] = animationEntryEnd; + if (animationType.includes('in-out')) { + extraProps['data-exit-start'] = animationExitStart; + extraProps['data-exit-end'] = animationExitEnd; + } + } + + if (parallaxEnabled) { + extraProps['data-parallax'] = '1'; + extraProps['data-parallax-strength'] = parallaxStrength; + extraProps.style = { + ...extraProps.style, + '--parallax-strength': `${parallaxStrength}px`, + }; + } + + return extraProps; + } ); // 4) Also reflect classes/attributes in the editor canvas for live preview. addFilter( - 'editor.BlockListBlock', - 'my-scroll-block/list-props', - createHigherOrderComponent((BlockListBlock) => { - return (props) => { - if (!SUPPORTED_BLOCKS.includes(props.name)) { - return ; - } - const { - animationType = 'none', - animationRange = 'default', - animationEntryStart = 20, - animationEntryEnd = 100, - animationExitStart = 0, - animationExitEnd = 100, - parallaxEnabled = false, - parallaxStrength = 50, - } = props.attributes; - - const extraProps = {}; - - if (animationType !== 'none') { - extraProps.className = [ - props.className, - 'scroll-anim-block', - `scroll-anim-${String(animationType).replace(/\s+/g, '-').toLowerCase()}`, - ] - .filter(Boolean) - .join(' '); - extraProps['data-scroll-anim'] = '1'; - extraProps['data-anim-range'] = animationRange; - - if (animationRange === 'custom') { - extraProps['data-entry-start'] = animationEntryStart; - extraProps['data-entry-end'] = animationEntryEnd; - if (animationType.includes('in-out')) { - extraProps['data-exit-start'] = animationExitStart; - extraProps['data-exit-end'] = animationExitEnd; - } - } - } - - if (parallaxEnabled) { - extraProps['data-parallax'] = '1'; - extraProps['data-parallax-strength'] = parallaxStrength; - extraProps.style = { - ...props.style, - '--parallax-strength': `${parallaxStrength}px`, - }; - } - return ; - }; - }, 'withListExtraProps') + 'editor.BlockListBlock', + 'my-scroll-block/list-props', + createHigherOrderComponent((BlockListBlock) => { + return (props) => { + if (!SUPPORTED_BLOCKS.includes(props.name)) { + return ; + } + const { + animationType = 'none', + animationRange = 'default', + animationEntryStart = 20, + animationEntryEnd = 100, + animationExitStart = 0, + animationExitEnd = 100, + parallaxEnabled = false, + parallaxStrength = 50, + } = props.attributes; + + const extraProps = {}; + + if (animationType !== 'none') { + extraProps.className = [ + props.className, + 'scroll-anim-block', + `scroll-anim-${String(animationType).replace(/\s+/g, '-').toLowerCase()}`, + ] + .filter(Boolean) + .join(' '); + extraProps['data-scroll-anim'] = '1'; + extraProps['data-anim-range'] = animationRange; + + if (animationRange === 'custom') { + extraProps['data-entry-start'] = animationEntryStart; + extraProps['data-entry-end'] = animationEntryEnd; + if (animationType.includes('in-out')) { + extraProps['data-exit-start'] = animationExitStart; + extraProps['data-exit-end'] = animationExitEnd; + } + } + } + + if (parallaxEnabled) { + extraProps['data-parallax'] = '1'; + extraProps['data-parallax-strength'] = parallaxStrength; + extraProps.style = { + ...props.style, + '--parallax-strength': `${parallaxStrength}px`, + }; + } + return ; + }; + }, 'withListExtraProps') ); function openBlockInspector(clientId) { - try { - // Ensure the block is selected - dispatch('core/block-editor').selectBlock(clientId); - } catch (e) {} - try { - // Post editor (classic block editor screen) - dispatch('core/edit-post').openGeneralSidebar('edit-post/block'); - } catch (e) {} - try { - // Site editor (FSE) - dispatch('core/edit-site').openGeneralSidebar('edit-site/block-inspector'); - } catch (e) {} + try { + // Ensure the block is selected + dispatch('core/block-editor').selectBlock(clientId); + } catch (e) {} + try { + // Post editor (classic block editor screen) + dispatch('core/edit-post').openGeneralSidebar('edit-post/block'); + } catch (e) {} + try { + // Site editor (FSE) + dispatch('core/edit-site').openGeneralSidebar('edit-site/block-inspector'); + } catch (e) {} } // 5) Add animation indicator icon to blocks with animations and make it clickable. addFilter( - 'editor.BlockListBlock', - 'my-scroll-block/animation-indicator', - createHigherOrderComponent((BlockListBlock) => { - return (props) => { - if (!SUPPORTED_BLOCKS.includes(props.name)) { - return ; - } - const { animationType = 'none', parallaxEnabled = false } = props.attributes; - - if (animationType === 'none' && !parallaxEnabled) { - return ; - } - - const handleActivate = (event) => { - event.preventDefault(); - event.stopPropagation(); - openBlockInspector(props.clientId); - }; - - const handleKeyDown = (event) => { - if (event.key === 'Enter' || event.key === ' ') { - handleActivate(event); - } - }; - - return ( -
- -
- -
-
- ); - }; - }, 'withAnimationIndicator') + 'editor.BlockListBlock', + 'my-scroll-block/animation-indicator', + createHigherOrderComponent((BlockListBlock) => { + return (props) => { + if (!SUPPORTED_BLOCKS.includes(props.name)) { + return ; + } + const { animationType = 'none', parallaxEnabled = false } = props.attributes; + + if (animationType === 'none' && !parallaxEnabled) { + return ; + } + + const handleActivate = (event) => { + event.preventDefault(); + event.stopPropagation(); + openBlockInspector(props.clientId); + }; + + const handleKeyDown = (event) => { + if (event.key === 'Enter' || event.key === ' ') { + handleActivate(event); + } + }; + + return ( +
+ +
+ +
+
+ ); + }; + }, 'withAnimationIndicator') ); // No standalone block registration; this plugin extends core blocks only. diff --git a/src/progress-block/editor.css b/src/progress-block/editor.css index cf1e224..f2fce00 100644 --- a/src/progress-block/editor.css +++ b/src/progress-block/editor.css @@ -3,46 +3,46 @@ */ .reading-progress-preview { - padding: 20px; - background: #f9f9f9; - border: 2px dashed #ddd; - border-radius: 8px; + padding: 20px; + background: #f9f9f9; + border: 2px dashed #ddd; + border-radius: 8px; } .reading-progress-preview .reading-progress-track { - margin-bottom: 16px; + margin-bottom: 16px; } .reading-progress-info { - font-size: 14px; - line-height: 1.6; + font-size: 14px; + line-height: 1.6; } .reading-progress-info p { - margin: 8px 0; + margin: 8px 0; } .reading-progress-info strong { - color: #1e1e1e; - font-size: 16px; + color: #1e1e1e; + font-size: 16px; } .reading-progress-info em { - color: #666; - font-style: normal; - background: #e0e0e0; - padding: 2px 8px; - border-radius: 4px; - font-size: 13px; + color: #666; + font-style: normal; + background: #e0e0e0; + padding: 2px 8px; + border-radius: 4px; + font-size: 13px; } /* Block icon in inserter */ .wp-block-my-scroll-block-reading-progress { - min-height: 100px; + min-height: 100px; } /* Color picker labels */ .components-base-control__label { - font-weight: 500; - margin-bottom: 8px; + font-weight: 500; + margin-bottom: 8px; } diff --git a/src/progress-block/index.js b/src/progress-block/index.js index 1cf7899..138a4fa 100644 --- a/src/progress-block/index.js +++ b/src/progress-block/index.js @@ -3,159 +3,164 @@ import { __ } from '@wordpress/i18n'; import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; import { useInstanceId } from '@wordpress/compose'; import { - PanelBody, - ColorPicker, - RangeControl, - SelectControl, - ToggleControl, - BaseControl, + PanelBody, + ColorPicker, + RangeControl, + SelectControl, + ToggleControl, + BaseControl, } from '@wordpress/components'; import './editor.css'; import './style.css'; const Edit = ({ attributes, setAttributes }) => { - const { barColor, barHeight, position, backgroundColor, showPercentage } = attributes; - const blockProps = useBlockProps(); - const instanceId = useInstanceId(Edit); + const { barColor, barHeight, position, backgroundColor, showPercentage } = attributes; + const blockProps = useBlockProps(); + const instanceId = useInstanceId(Edit); - return ( - <> - - - setAttributes({ position: value })} - help={__('Where to display the progress bar', 'my-scroll-block')} - /> + return ( + <> + + + setAttributes({ position: value })} + help={__('Where to display the progress bar', 'my-scroll-block')} + /> - setAttributes({ barHeight: value })} - min={2} - max={20} - step={1} - /> + setAttributes({ barHeight: value })} + min={2} + max={20} + step={1} + /> - - setAttributes({ barColor: value })} - enableAlpha - /> - + + setAttributes({ barColor: value })} + enableAlpha + /> + - - setAttributes({ backgroundColor: value })} - enableAlpha - /> - + + setAttributes({ backgroundColor: value })} + enableAlpha + /> + - setAttributes({ showPercentage: value })} - help={__('Display scroll percentage number', 'my-scroll-block')} - /> - - + setAttributes({ showPercentage: value })} + help={__('Display scroll percentage number', 'my-scroll-block')} + /> + + -
-
-
-
-
-
-

- 📊 {__('Reading Progress Bar', 'my-scroll-block')} -

-

- {__('Position:', 'my-scroll-block')}{' '} - - {position === 'top' - ? __('Top', 'my-scroll-block') - : __('Bottom', 'my-scroll-block')} - -

-

- {__('This bar will be fixed at the', 'my-scroll-block')}{' '} - {position === 'top' ? __('top', 'my-scroll-block') : __('bottom', 'my-scroll-block')}{' '} - {__( - 'of the page and track scroll progress using CSS scroll timeline.', - 'my-scroll-block' - )} -

- {showPercentage && ( -

- ✓ {__('Percentage display enabled', 'my-scroll-block')} -

- )} -
-
-
- - ); +
+
+
+
+
+
+

+ 📊 {__('Reading Progress Bar', 'my-scroll-block')} +

+

+ {__('Position:', 'my-scroll-block')}{' '} + + {position === 'top' + ? __('Top', 'my-scroll-block') + : __('Bottom', 'my-scroll-block')} + +

+

+ {__('This bar will be fixed at the', 'my-scroll-block')}{' '} + {position === 'top' + ? __('top', 'my-scroll-block') + : __('bottom', 'my-scroll-block')}{' '} + {__( + 'of the page and track scroll progress using CSS scroll timeline.', + 'my-scroll-block' + )} +

+ {showPercentage && ( +

+ ✓ {__('Percentage display enabled', 'my-scroll-block')} +

+ )} +
+
+
+ + ); }; const Save = ({ attributes }) => { - const { barColor, barHeight, position, backgroundColor, showPercentage } = attributes; - const blockProps = useBlockProps.save({ - className: `reading-progress-container position-${position}`, - style: { - '--progress-bar-color': barColor, - '--progress-bar-height': `${barHeight}px`, - '--progress-bg-color': backgroundColor, - }, - }); + const { barColor, barHeight, position, backgroundColor, showPercentage } = attributes; + const blockProps = useBlockProps.save({ + className: `reading-progress-container position-${position}`, + style: { + '--progress-bar-color': barColor, + '--progress-bar-height': `${barHeight}px`, + '--progress-bg-color': backgroundColor, + }, + }); - return ( -
-
-
-
- {showPercentage && ( -
- 0% -
- )} -
- ); + return ( +
+
+
+
+ {showPercentage && ( +
+ 0% +
+ )} +
+ ); }; registerBlockType('my-scroll-block/reading-progress', { - edit: Edit, - save: Save, + edit: Edit, + save: Save, }); diff --git a/src/progress-block/style.css b/src/progress-block/style.css index f3eca03..c582e0f 100644 --- a/src/progress-block/style.css +++ b/src/progress-block/style.css @@ -4,156 +4,164 @@ */ .reading-progress-container { - position: fixed; - left: 0; - right: 0; - width: 100%; - z-index: 999999; - pointer-events: none; - animation: fadeIn 0.3s ease-out; + position: fixed; + left: 0; + right: 0; + width: 100%; + z-index: 999999; + pointer-events: none; + animation: fadeIn 0.3s ease-out; } .reading-progress-container.position-top { - top: 0; + top: 0; } .reading-progress-container.position-bottom { - bottom: 0; + bottom: 0; } .reading-progress-track { - height: var(--progress-bar-height, 4px); - background-color: var(--progress-bg-color, #e0e0e0); - position: relative; - overflow: hidden; + height: var(--progress-bar-height, 4px); + background-color: var(--progress-bg-color, #e0e0e0); + position: relative; + overflow: hidden; } .reading-progress-bar { - height: 100%; - background-color: var(--progress-bar-color, #3858e9); - transform-origin: 0 50%; - transform: scaleX(0); + height: 100%; + background-color: var(--progress-bar-color, #3858e9); + transform-origin: 0 50%; + transform: scaleX(0); } /* Scroll Progress Timeline - The Magic! */ @supports (animation-timeline: scroll()) { - .reading-progress-bar { - animation: progress-bar linear; - animation-timeline: scroll(root block); - animation-range: 0% 100%; - } + + .reading-progress-bar { + animation: progress-bar linear; + animation-timeline: scroll(root block); + animation-range: 0% 100%; + } } @keyframes progress-bar { - from { - transform: scaleX(0); - } - to { - transform: scaleX(1); - } + from { + transform: scaleX(0); + } + + to { + transform: scaleX(1); + } } /* Percentage Display */ .reading-progress-percentage { - position: absolute; - right: 16px; - top: 50%; - transform: translateY(-50%); - pointer-events: auto; - background: var(--progress-bar-color, #3858e9); - color: #fff; - padding: 4px 12px; - border-radius: 12px; - font-size: 12px; - font-weight: 600; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - opacity: 0; - transition: opacity 0.3s ease; + position: absolute; + right: 16px; + top: 50%; + transform: translateY(-50%); + pointer-events: auto; + background: var(--progress-bar-color, #3858e9); + color: #fff; + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + opacity: 0; + transition: opacity 0.3s ease; } .reading-progress-container:hover .reading-progress-percentage { - opacity: 1; + opacity: 1; } /* Animate percentage value using scroll timeline */ @supports (animation-timeline: scroll()) { - .percentage-value::before { - content: '0'; - animation: percentage-counter linear; - animation-timeline: scroll(root block); - animation-range: 0% 100%; - counter-reset: percentage 0; - } - - @keyframes percentage-counter { - to { - counter-increment: percentage 100; - content: counter(percentage); - } - } + + .percentage-value::before { + content: "0"; + animation: percentage-counter linear; + animation-timeline: scroll(root block); + animation-range: 0% 100%; + counter-reset: percentage 0; + } + + @keyframes percentage-counter { + + to { + counter-increment: percentage 100; + content: counter(percentage); + } + } } /* Fallback for browsers without scroll timeline support */ @supports not (animation-timeline: scroll()) { - .reading-progress-bar { - transform: scaleX(0); - } - - .reading-progress-percentage { - display: none; - } - - body::after { - content: '⚠️ This browser does not support CSS Scroll Timelines. Please use Chrome 115+, Edge 115+, or Opera 101+ for the full experience.'; - position: fixed; - bottom: 20px; - left: 50%; - transform: translateX(-50%); - background: #ff9800; - color: #fff; - padding: 12px 20px; - border-radius: 8px; - font-size: 14px; - z-index: 999999; - max-width: 90%; - text-align: center; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); - } + + .reading-progress-bar { + transform: scaleX(0); + } + + .reading-progress-percentage { + display: none; + } + + body::after { + content: "⚠️ This browser does not support CSS Scroll Timelines. Please use Chrome 115+, Edge 115+, or Opera 101+ for the full experience."; + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background: #ff9800; + color: #fff; + padding: 12px 20px; + border-radius: 8px; + font-size: 14px; + z-index: 999999; + max-width: 90%; + text-align: center; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + } } @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } + from { + opacity: 0; + } + + to { + opacity: 1; + } } /* Respect reduced motion preferences */ @media (prefers-reduced-motion: reduce) { - .reading-progress-bar { - animation: none !important; - transition: none !important; - } - - .reading-progress-container { - animation: none !important; - } - - .reading-progress-percentage { - transition: none !important; - } + + .reading-progress-bar { + animation: none !important; + transition: none !important; + } + + .reading-progress-container { + animation: none !important; + } + + .reading-progress-percentage { + transition: none !important; + } } /* Mobile optimizations */ @media (max-width: 768px) { - .reading-progress-percentage { - right: 8px; - padding: 3px 8px; - font-size: 11px; - } + + .reading-progress-percentage { + right: 8px; + padding: 3px 8px; + font-size: 11px; + } } diff --git a/src/style.css b/src/style.css index 523f117..832d1d2 100644 --- a/src/style.css +++ b/src/style.css @@ -7,447 +7,466 @@ /* Base defaults */ @property --anim-displacement-horizontal { - syntax: ''; - inherits: true; - initial-value: 10vw; + syntax: ""; + inherits: true; + initial-value: 10vw; } @property --anim-displacement-vertical { - syntax: ''; - inherits: true; - initial-value: 5vh; + syntax: ""; + inherits: true; + initial-value: 5vh; } @property --parallax-strength { - syntax: ''; - inherits: true; - initial-value: 50px; + syntax: ""; + inherits: true; + initial-value: 50px; } @supports (animation-timeline: view()) { - /* Entry-only animations */ - .scroll-anim-fade-in, - .scroll-anim-slide-in-left, - .scroll-anim-slide-in-right, - .scroll-anim-slide-in-up, - .scroll-anim-slide-in-down, - .scroll-anim-scale-up, - .scroll-anim-rotate-in, - .scroll-anim-blur-in, - .scroll-anim-rotate-3d-in, - .scroll-anim-circle-reveal, - .scroll-anim-curtain-reveal { - animation-timeline: view(); - animation-range: entry 20% cover 100%; - } - - /* Quick timing preset */ - [data-anim-range='quick'].scroll-anim-fade-in, - [data-anim-range='quick'].scroll-anim-slide-in-left, - [data-anim-range='quick'].scroll-anim-slide-in-right, - [data-anim-range='quick'].scroll-anim-slide-in-up, - [data-anim-range='quick'].scroll-anim-slide-in-down, - [data-anim-range='quick'].scroll-anim-scale-up, - [data-anim-range='quick'].scroll-anim-rotate-in, - [data-anim-range='quick'].scroll-anim-blur-in, - [data-anim-range='quick'].scroll-anim-rotate-3d-in, - [data-anim-range='quick'].scroll-anim-circle-reveal, - [data-anim-range='quick'].scroll-anim-curtain-reveal { - animation-range: entry 0% cover 50%; - } - - /* Slow timing preset */ - [data-anim-range='slow'].scroll-anim-fade-in, - [data-anim-range='slow'].scroll-anim-slide-in-left, - [data-anim-range='slow'].scroll-anim-slide-in-right, - [data-anim-range='slow'].scroll-anim-slide-in-up, - [data-anim-range='slow'].scroll-anim-slide-in-down, - [data-anim-range='slow'].scroll-anim-scale-up, - [data-anim-range='slow'].scroll-anim-rotate-in, - [data-anim-range='slow'].scroll-anim-blur-in, - [data-anim-range='slow'].scroll-anim-rotate-3d-in, - [data-anim-range='slow'].scroll-anim-circle-reveal, - [data-anim-range='slow'].scroll-anim-curtain-reveal { - animation-range: entry 10% cover 100%; - } - - /* Late start timing preset */ - [data-anim-range='late'].scroll-anim-fade-in, - [data-anim-range='late'].scroll-anim-slide-in-left, - [data-anim-range='late'].scroll-anim-slide-in-right, - [data-anim-range='late'].scroll-anim-slide-in-up, - [data-anim-range='late'].scroll-anim-slide-in-down, - [data-anim-range='late'].scroll-anim-scale-up, - [data-anim-range='late'].scroll-anim-rotate-in, - [data-anim-range='late'].scroll-anim-blur-in, - [data-anim-range='late'].scroll-anim-rotate-3d-in, - [data-anim-range='late'].scroll-anim-circle-reveal, - [data-anim-range='late'].scroll-anim-curtain-reveal { - animation-range: entry 50% cover 100%; - } - - /* In-and-out animations */ - .scroll-anim-fade-in-out, - .scroll-anim-slide-up-in-out, - .scroll-anim-scale-in-out, - .scroll-anim-rotate-in-out, - .scroll-anim-rotate-3d-in-out { - animation-timeline: view(); - } - - /* Parallax Effect */ - [data-parallax='1'] { - animation-name: scrollParallax; - animation-timeline: scroll(root block); - animation-range: 0% 100%; - } + + /* Entry-only animations */ + .scroll-anim-fade-in, + .scroll-anim-slide-in-left, + .scroll-anim-slide-in-right, + .scroll-anim-slide-in-up, + .scroll-anim-slide-in-down, + .scroll-anim-scale-up, + .scroll-anim-rotate-in, + .scroll-anim-blur-in, + .scroll-anim-rotate-3d-in, + .scroll-anim-circle-reveal, + .scroll-anim-curtain-reveal { + animation-timeline: view(); + animation-range: entry 20% cover 100%; + } + + /* Quick timing preset */ + [data-anim-range="quick"].scroll-anim-fade-in, + [data-anim-range="quick"].scroll-anim-slide-in-left, + [data-anim-range="quick"].scroll-anim-slide-in-right, + [data-anim-range="quick"].scroll-anim-slide-in-up, + [data-anim-range="quick"].scroll-anim-slide-in-down, + [data-anim-range="quick"].scroll-anim-scale-up, + [data-anim-range="quick"].scroll-anim-rotate-in, + [data-anim-range="quick"].scroll-anim-blur-in, + [data-anim-range="quick"].scroll-anim-rotate-3d-in, + [data-anim-range="quick"].scroll-anim-circle-reveal, + [data-anim-range="quick"].scroll-anim-curtain-reveal { + animation-range: entry 0% cover 50%; + } + + /* Slow timing preset */ + [data-anim-range="slow"].scroll-anim-fade-in, + [data-anim-range="slow"].scroll-anim-slide-in-left, + [data-anim-range="slow"].scroll-anim-slide-in-right, + [data-anim-range="slow"].scroll-anim-slide-in-up, + [data-anim-range="slow"].scroll-anim-slide-in-down, + [data-anim-range="slow"].scroll-anim-scale-up, + [data-anim-range="slow"].scroll-anim-rotate-in, + [data-anim-range="slow"].scroll-anim-blur-in, + [data-anim-range="slow"].scroll-anim-rotate-3d-in, + [data-anim-range="slow"].scroll-anim-circle-reveal, + [data-anim-range="slow"].scroll-anim-curtain-reveal { + animation-range: entry 10% cover 100%; + } + + /* Late start timing preset */ + [data-anim-range="late"].scroll-anim-fade-in, + [data-anim-range="late"].scroll-anim-slide-in-left, + [data-anim-range="late"].scroll-anim-slide-in-right, + [data-anim-range="late"].scroll-anim-slide-in-up, + [data-anim-range="late"].scroll-anim-slide-in-down, + [data-anim-range="late"].scroll-anim-scale-up, + [data-anim-range="late"].scroll-anim-rotate-in, + [data-anim-range="late"].scroll-anim-blur-in, + [data-anim-range="late"].scroll-anim-rotate-3d-in, + [data-anim-range="late"].scroll-anim-circle-reveal, + [data-anim-range="late"].scroll-anim-curtain-reveal { + animation-range: entry 50% cover 100%; + } + + /* In-and-out animations */ + .scroll-anim-fade-in-out, + .scroll-anim-slide-up-in-out, + .scroll-anim-scale-in-out, + .scroll-anim-rotate-in-out, + .scroll-anim-rotate-3d-in-out { + animation-timeline: view(); + } + + /* Parallax Effect */ + [data-parallax="1"] { + animation-name: scrollParallax; + animation-timeline: scroll(root block); + animation-range: 0% 100%; + } } /* Apply animations - Entry Only */ .scroll-anim-fade-in { - animation-name: scrollFadeIn; - opacity: 0; - transform: translateY(var(--anim-displacement-vertical)); + animation-name: scrollFadeIn; + opacity: 0; + transform: translateY(var(--anim-displacement-vertical)); } .scroll-anim-slide-in-left { - animation-name: scrollSlideInLeft; - opacity: 0; - transform: translateX(calc(-1 * var(--anim-displacement-horizontal))); + animation-name: scrollSlideInLeft; + opacity: 0; + transform: translateX(calc(-1 * var(--anim-displacement-horizontal))); } .scroll-anim-slide-in-right { - animation-name: scrollSlideInRight; - opacity: 0; - transform: translateX(var(--anim-displacement-horizontal)); + animation-name: scrollSlideInRight; + opacity: 0; + transform: translateX(var(--anim-displacement-horizontal)); } .scroll-anim-slide-in-up { - animation-name: scrollSlideInUp; - opacity: 0; - transform: translateY(var(--anim-displacement-vertical)); + animation-name: scrollSlideInUp; + opacity: 0; + transform: translateY(var(--anim-displacement-vertical)); } .scroll-anim-slide-in-down { - animation-name: scrollSlideInDown; - opacity: 0; - transform: translateY(calc(-1 * var(--anim-displacement-vertical))); + animation-name: scrollSlideInDown; + opacity: 0; + transform: translateY(calc(-1 * var(--anim-displacement-vertical))); } .scroll-anim-scale-up { - animation-name: scrollScaleUp; - opacity: 0; - transform: scale(0.3); + animation-name: scrollScaleUp; + opacity: 0; + transform: scale(0.3); } .scroll-anim-rotate-in { - animation-name: scrollRotateIn; - opacity: 0; - transform: rotate(25deg); + animation-name: scrollRotateIn; + opacity: 0; + transform: rotate(25deg); } .scroll-anim-blur-in { - animation-name: scrollBlurIn; - opacity: 0; - filter: blur(10px); + animation-name: scrollBlurIn; + opacity: 0; + filter: blur(10px); } .scroll-anim-rotate-3d-in { - animation-name: scrollRotate3DIn; - opacity: 0; - transform: perspective(1000px) rotateX(45deg); + animation-name: scrollRotate3DIn; + opacity: 0; + transform: perspective(1000px) rotateX(45deg); } .scroll-anim-circle-reveal { - animation-name: scrollCircleReveal; - clip-path: circle(0% at 50% 50%); + animation-name: scrollCircleReveal; + clip-path: circle(0% at 50% 50%); } .scroll-anim-curtain-reveal { - animation-name: scrollCurtainReveal; - clip-path: inset(0 50% 0 50%); + animation-name: scrollCurtainReveal; + clip-path: inset(0 50% 0 50%); } /* Apply animations - In and Out */ .scroll-anim-fade-in-out { - animation-name: scrollFadeInOut; + animation-name: scrollFadeInOut; } .scroll-anim-slide-up-in-out { - animation-name: scrollSlideUpInOut; + animation-name: scrollSlideUpInOut; } .scroll-anim-scale-in-out { - animation-name: scrollScaleInOut; + animation-name: scrollScaleInOut; } .scroll-anim-rotate-in-out { - animation-name: scrollRotateInOut; + animation-name: scrollRotateInOut; } .scroll-anim-rotate-3d-in-out { - animation-name: scrollRotate3DInOut; + animation-name: scrollRotate3DInOut; } /* CSS animations using keyframes - Entry Only */ @keyframes scrollFadeIn { - from { - opacity: 0; - transform: translateY(var(--anim-displacement-vertical)); - } - to { - opacity: 1; - transform: translateY(0); - } + from { + opacity: 0; + transform: translateY(var(--anim-displacement-vertical)); + } + + to { + opacity: 1; + transform: translateY(0); + } } @keyframes scrollSlideInLeft { - from { - opacity: 0; - transform: translateX(calc(-1 * var(--anim-displacement-horizontal))); - } - to { - opacity: 1; - transform: translateX(0); - } + from { + opacity: 0; + transform: translateX(calc(-1 * var(--anim-displacement-horizontal))); + } + + to { + opacity: 1; + transform: translateX(0); + } } @keyframes scrollSlideInRight { - from { - opacity: 0; - transform: translateX(var(--anim-displacement-horizontal)); - } - to { - opacity: 1; - transform: translateX(0); - } + from { + opacity: 0; + transform: translateX(var(--anim-displacement-horizontal)); + } + + to { + opacity: 1; + transform: translateX(0); + } } @keyframes scrollSlideInUp { - from { - opacity: 0; - transform: translateY(var(--anim-displacement-vertical)); - } - to { - opacity: 1; - transform: translateY(0); - } + from { + opacity: 0; + transform: translateY(var(--anim-displacement-vertical)); + } + + to { + opacity: 1; + transform: translateY(0); + } } @keyframes scrollSlideInDown { - from { - opacity: 0; - transform: translateY(calc(-1 * var(--anim-displacement-vertical))); - } - to { - opacity: 1; - transform: translateY(0); - } + from { + opacity: 0; + transform: translateY(calc(-1 * var(--anim-displacement-vertical))); + } + + to { + opacity: 1; + transform: translateY(0); + } } @keyframes scrollScaleUp { - from { - opacity: 0; - transform: scale(0.3); - } - to { - opacity: 1; - transform: scale(1); - } + from { + opacity: 0; + transform: scale(0.3); + } + + to { + opacity: 1; + transform: scale(1); + } } @keyframes scrollRotateIn { - from { - opacity: 0; - transform: rotate(25deg); - } - to { - opacity: 1; - transform: rotate(0); - } + from { + opacity: 0; + transform: rotate(25deg); + } + + to { + opacity: 1; + transform: rotate(0); + } } @keyframes scrollBlurIn { - from { - opacity: 0; - filter: blur(10px); - } - to { - opacity: 1; - filter: blur(0); - } + from { + opacity: 0; + filter: blur(10px); + } + + to { + opacity: 1; + filter: blur(0); + } } @keyframes scrollRotate3DIn { - from { - opacity: 0; - transform: perspective(1000px) rotateX(45deg); - } - to { - opacity: 1; - transform: perspective(1000px) rotateX(0); - } + from { + opacity: 0; + transform: perspective(1000px) rotateX(45deg); + } + + to { + opacity: 1; + transform: perspective(1000px) rotateX(0); + } } @keyframes scrollCircleReveal { - from { - clip-path: circle(0% at 50% 50%); - } - to { - clip-path: circle(100% at 50% 50%); - } + from { + clip-path: circle(0% at 50% 50%); + } + + to { + clip-path: circle(100% at 50% 50%); + } } @keyframes scrollCurtainReveal { - from { - clip-path: inset(0 50% 0 50%); - } - to { - clip-path: inset(0 0 0 0); - } + from { + clip-path: inset(0 50% 0 50%); + } + + to { + clip-path: inset(0 0 0 0); + } } /* CSS animations using keyframes - In and Out */ @keyframes scrollFadeInOut { - entry 0% { - opacity: 0; - transform: translateY(var(--anim-displacement-vertical)); - } - entry 100% { - opacity: 1; - transform: translateY(0); - } + entry 0% { + opacity: 0; + transform: translateY(var(--anim-displacement-vertical)); + } + + entry 100% { + opacity: 1; + transform: translateY(0); + } - exit 0% { - opacity: 1; - transform: translateY(0); - } + exit 0% { + opacity: 1; + transform: translateY(0); + } - exit 100% { - opacity: 0; - transform: translateY(calc(-1 * var(--anim-displacement-vertical))); - } + exit 100% { + opacity: 0; + transform: translateY(calc(-1 * var(--anim-displacement-vertical))); + } } @keyframes scrollSlideUpInOut { - entry 0% { - opacity: 0; - transform: translateY(var(--anim-displacement-vertical)); - } - entry 100% { - opacity: 1; - transform: translateY(0); - } + entry 0% { + opacity: 0; + transform: translateY(var(--anim-displacement-vertical)); + } - exit 0% { - opacity: 1; - transform: translateY(0); - } + entry 100% { + opacity: 1; + transform: translateY(0); + } - exit 100% { - opacity: 0; - transform: translateY(calc(-1 * var(--anim-displacement-vertical))); - } + exit 0% { + opacity: 1; + transform: translateY(0); + } + + exit 100% { + opacity: 0; + transform: translateY(calc(-1 * var(--anim-displacement-vertical))); + } } @keyframes scrollScaleInOut { - entry 0% { - opacity: 0; - transform: scale(0.3); - } - entry 100% { - opacity: 1; - transform: scale(1); - } + entry 0% { + opacity: 0; + transform: scale(0.3); + } + + entry 100% { + opacity: 1; + transform: scale(1); + } - exit 0% { - opacity: 1; - transform: scale(1); - } + exit 0% { + opacity: 1; + transform: scale(1); + } - exit 100% { - opacity: 0; - transform: scale(0.3); - } + exit 100% { + opacity: 0; + transform: scale(0.3); + } } @keyframes scrollRotateInOut { - entry 0% { - opacity: 0; - transform: rotate(-25deg); - } - entry 100% { - opacity: 1; - transform: rotate(0); - } + entry 0% { + opacity: 0; + transform: rotate(-25deg); + } - exit 0% { - opacity: 1; - transform: rotate(0); - } + entry 100% { + opacity: 1; + transform: rotate(0); + } - exit 100% { - opacity: 0; - transform: rotate(25deg); - } + exit 0% { + opacity: 1; + transform: rotate(0); + } + + exit 100% { + opacity: 0; + transform: rotate(25deg); + } } @keyframes scrollRotate3DInOut { - entry 0% { - opacity: 0; - transform: perspective(1000px) rotateX(-45deg); - } - entry 100% { - opacity: 1; - transform: perspective(1000px) rotateX(0); - } + entry 0% { + opacity: 0; + transform: perspective(1000px) rotateX(-45deg); + } + + entry 100% { + opacity: 1; + transform: perspective(1000px) rotateX(0); + } - exit 0% { - opacity: 1; - transform: perspective(1000px) rotateX(0); - } + exit 0% { + opacity: 1; + transform: perspective(1000px) rotateX(0); + } - exit 100% { - opacity: 0; - transform: perspective(1000px) rotateX(45deg); - } + exit 100% { + opacity: 0; + transform: perspective(1000px) rotateX(45deg); + } } @keyframes scrollParallax { - from { - transform: translateY(0); - } - to { - transform: translateY(var(--parallax-strength)); - } + from { + transform: translateY(0); + } + + to { + transform: translateY(var(--parallax-strength)); + } } /* Respect reduced motion */ @media (prefers-reduced-motion: reduce) { - [data-scroll-anim], - [data-parallax] { - animation: none !important; - transition: none !important; - opacity: 1 !important; - transform: none !important; - filter: none !important; - clip-path: none !important; - } + + [data-scroll-anim], + [data-parallax] { + animation: none !important; + transition: none !important; + opacity: 1 !important; + transform: none !important; + filter: none !important; + clip-path: none !important; + } } diff --git a/tests/global-setup.ts b/tests/global-setup.ts index 1368eea..d89a20a 100644 --- a/tests/global-setup.ts +++ b/tests/global-setup.ts @@ -2,40 +2,40 @@ import { runCLI } from '@wp-playground/cli'; import * as path from 'path'; async function globalSetup(): Promise { - // eslint-disable-next-line no-console - console.log('Starting WordPress Playground server...'); + // eslint-disable-next-line no-console + console.log('Starting WordPress Playground server...'); - // Use process.cwd() and navigate to plugin directory - const pluginPath = path.join(process.cwd()); + // Use process.cwd() and navigate to plugin directory + const pluginPath = path.join(process.cwd()); - const cliServer = await runCLI({ - command: 'server', - php: '8.3', - wp: 'latest', - login: true, - port: 9400, - mount: [ - { - hostPath: pluginPath, - vfsPath: '/wordpress/wp-content/plugins/my-scroll-block', - }, - ], - blueprint: { - steps: [ - { - step: 'setSiteOptions', - options: { - blogname: 'WordPress Scroll-driven block', - blogdescription: 'Created by Fellyph', - }, - }, - { - step: 'activatePlugin', - pluginPath: '/wordpress/wp-content/plugins/my-scroll-block/my-scroll-block.php', - }, - { - step: 'runPHP', - code: ` 'Demo Scroll Animations Post', @@ -44,19 +44,19 @@ async function globalSetup(): Promise { 'post_status' => 'publish' )); ?>`, - }, - ], - }, - }); + }, + ], + }, + }); - // Store the server instance globally for teardown - (global as any).cliServer = cliServer; + // Store the server instance globally for teardown + (global as any).cliServer = cliServer; - // eslint-disable-next-line no-console - console.log('WordPress Playground server started on http://127.0.0.1:9400'); + // eslint-disable-next-line no-console + console.log('WordPress Playground server started on http://127.0.0.1:9400'); - // Wait a bit for the server to be fully ready - await new Promise((resolve) => setTimeout(resolve, 2000)); + // Wait a bit for the server to be fully ready + await new Promise((resolve) => setTimeout(resolve, 2000)); } export default globalSetup; diff --git a/tests/global-teardown.ts b/tests/global-teardown.ts index a3d4bf6..51ac1fe 100644 --- a/tests/global-teardown.ts +++ b/tests/global-teardown.ts @@ -1,15 +1,15 @@ async function globalTeardown(): Promise { - // eslint-disable-next-line no-console - console.log('Stopping WordPress Playground server...'); + // eslint-disable-next-line no-console + console.log('Stopping WordPress Playground server...'); - const cliServer = (global as any).cliServer; + const cliServer = (global as any).cliServer; - if (cliServer && typeof cliServer.exit === 'function') { - await cliServer.exit(); - } + if (cliServer && typeof cliServer.exit === 'function') { + await cliServer.exit(); + } - // eslint-disable-next-line no-console - console.log('WordPress Playground server stopped.'); + // eslint-disable-next-line no-console + console.log('WordPress Playground server stopped.'); } export default globalTeardown; diff --git a/tests/reduced-motion.spec.ts b/tests/reduced-motion.spec.ts index f0b22d3..2f4b30c 100644 --- a/tests/reduced-motion.spec.ts +++ b/tests/reduced-motion.spec.ts @@ -1,367 +1,367 @@ import { test, expect, type Page } from '@playwright/test'; test.describe('Reduced Motion Support', () => { - test('should disable animations when prefers-reduced-motion is set', async ({ - page, - }: { - page: Page; - }) => { - // Set the prefers-reduced-motion preference - await page.emulateMedia({ reducedMotion: 'reduce' }); - - // Create a post with scroll animation - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Create post with animation - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill('Reduced Motion Test', { timeout: 15000 }); - - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('This paragraph should not animate with reduced motion', { - timeout: 15000, - }); - - const animationTypeSelect = page.getByLabel('Animation Type'); - await animationTypeSelect.selectOption('Fade In'); - - // Publish the post - await page.getByRole('button', { name: 'Publish', exact: true }).click(); - const publishPanelButton = page - .getByLabel('Editor publish') - .getByRole('button', { name: 'Publish', exact: true }); - await publishPanelButton.click(); - - // Wait for post to be published - await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); - - // Get the post URL - const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); - const postUrl = await viewPostLink.getAttribute('href'); - - if (!postUrl) { - throw new Error('Could not get post URL'); - } - - // Visit the frontend post with reduced motion preference - await page.goto(postUrl); - await page.waitForLoadState('domcontentloaded'); - - // Verify the paragraph has the animation class but animations should be disabled via CSS - const animatedParagraph = page.locator('p.scroll-anim-fade-in[data-scroll-anim="1"]'); - await expect(animatedParagraph).toBeVisible(); - - // Check computed styles - with prefers-reduced-motion, the element should have: - // - opacity: 1 (not 0) - // - transform: none - // - animation: none - const computedStyles = await animatedParagraph.evaluate((el) => { - const styles = window.getComputedStyle(el); - return { - opacity: styles.opacity, - transform: styles.transform, - animation: styles.animation, - }; - }); - - // With reduced motion, the CSS should override the animation styles - expect(parseFloat(computedStyles.opacity)).toBe(1); - expect(computedStyles.transform).toBe('none'); - expect(computedStyles.animation).toContain('none'); - }); - - test('should disable parallax effect when prefers-reduced-motion is set', async ({ - page, - }: { - page: Page; - }) => { - // Set the prefers-reduced-motion preference - await page.emulateMedia({ reducedMotion: 'reduce' }); - - // Create a post with parallax - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Create post - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill('Parallax Reduced Motion Test', { timeout: 15000 }); - - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('This paragraph has parallax disabled with reduced motion', { - timeout: 15000, - }); - - // Enable parallax - const parallaxToggle = page.getByLabel('Enable Parallax Effect'); - await parallaxToggle.click(); - - // Publish the post - await page.getByRole('button', { name: 'Publish', exact: true }).click(); - const publishPanelButton = page - .getByLabel('Editor publish') - .getByRole('button', { name: 'Publish', exact: true }); - await publishPanelButton.click(); - - await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); - - // Get the post URL - const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); - const postUrl = await viewPostLink.getAttribute('href'); - - if (!postUrl) { - throw new Error('Could not get post URL'); - } - - // Visit the frontend post - await page.goto(postUrl); - await page.waitForLoadState('domcontentloaded'); - - // Verify the paragraph has parallax data attribute but effect is disabled - const parallaxParagraph = page.locator('p[data-parallax="1"]'); - await expect(parallaxParagraph).toBeVisible(); - - // Check that animations are disabled - const computedStyles = await parallaxParagraph.evaluate((el) => { - const styles = window.getComputedStyle(el); - return { - animation: styles.animation, - transform: styles.transform, - }; - }); - - expect(computedStyles.animation).toContain('none'); - expect(computedStyles.transform).toBe('none'); - }); - - test('should disable clip-path animations when prefers-reduced-motion is set', async ({ - page, - }: { - page: Page; - }) => { - // Set the prefers-reduced-motion preference - await page.emulateMedia({ reducedMotion: 'reduce' }); - - // Create a post with circle reveal animation - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Create post - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill('Circle Reveal Reduced Motion Test', { timeout: 15000 }); - - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('Circle reveal should be disabled with reduced motion', { - timeout: 15000, - }); - - const animationTypeSelect = page.getByLabel('Animation Type'); - await animationTypeSelect.selectOption('Circle Reveal'); - - // Publish the post - await page.getByRole('button', { name: 'Publish', exact: true }).click(); - const publishPanelButton = page - .getByLabel('Editor publish') - .getByRole('button', { name: 'Publish', exact: true }); - await publishPanelButton.click(); - - await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); - - // Get the post URL - const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); - const postUrl = await viewPostLink.getAttribute('href'); - - if (!postUrl) { - throw new Error('Could not get post URL'); - } - - // Visit the frontend post - await page.goto(postUrl); - await page.waitForLoadState('domcontentloaded'); - - // Verify clip-path is disabled - const revealParagraph = page.locator('p.scroll-anim-circle-reveal[data-scroll-anim="1"]'); - await expect(revealParagraph).toBeVisible(); - - const computedStyles = await revealParagraph.evaluate((el) => { - const styles = window.getComputedStyle(el); - return { - clipPath: styles.clipPath, - animation: styles.animation, - }; - }); - - expect(computedStyles.clipPath).toBe('none'); - expect(computedStyles.animation).toContain('none'); - }); - - test('should disable blur filter when prefers-reduced-motion is set', async ({ - page, - }: { - page: Page; - }) => { - // Set the prefers-reduced-motion preference - await page.emulateMedia({ reducedMotion: 'reduce' }); - - // Create a post with blur-in animation - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Create post - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill('Blur In Reduced Motion Test', { timeout: 15000 }); - - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('Blur effect should be disabled with reduced motion', { - timeout: 15000, - }); - - const animationTypeSelect = page.getByLabel('Animation Type'); - await animationTypeSelect.selectOption('Blur In'); - - // Publish the post - await page.getByRole('button', { name: 'Publish', exact: true }).click(); - const publishPanelButton = page - .getByLabel('Editor publish') - .getByRole('button', { name: 'Publish', exact: true }); - await publishPanelButton.click(); - - await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); - - // Get the post URL - const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); - const postUrl = await viewPostLink.getAttribute('href'); - - if (!postUrl) { - throw new Error('Could not get post URL'); - } - - // Visit the frontend post - await page.goto(postUrl); - await page.waitForLoadState('domcontentloaded'); - - // Verify filter is disabled - const blurParagraph = page.locator('p.scroll-anim-blur-in[data-scroll-anim="1"]'); - await expect(blurParagraph).toBeVisible(); - - const computedStyles = await blurParagraph.evaluate((el) => { - const styles = window.getComputedStyle(el); - return { - filter: styles.filter, - opacity: styles.opacity, - animation: styles.animation, - }; - }); - - expect(computedStyles.filter).toBe('none'); - expect(parseFloat(computedStyles.opacity)).toBe(1); - expect(computedStyles.animation).toContain('none'); - }); - - test('should allow animations when prefers-reduced-motion is not set', async ({ - page, - }: { - page: Page; - }) => { - // Do NOT set reduced motion preference (default is 'no-preference') - await page.emulateMedia({ reducedMotion: 'no-preference' }); - - // Create a post with animation - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Create post - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill('Normal Animation Test', { timeout: 15000 }); - - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('This paragraph should animate normally', { timeout: 15000 }); - - const animationTypeSelect = page.getByLabel('Animation Type'); - await animationTypeSelect.selectOption('Fade In'); - - // Publish the post - await page.getByRole('button', { name: 'Publish', exact: true }).click(); - const publishPanelButton = page - .getByLabel('Editor publish') - .getByRole('button', { name: 'Publish', exact: true }); - await publishPanelButton.click(); - - await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); - - // Get the post URL - const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); - const postUrl = await viewPostLink.getAttribute('href'); - - if (!postUrl) { - throw new Error('Could not get post URL'); - } - - // Visit the frontend post - await page.goto(postUrl); - await page.waitForLoadState('domcontentloaded'); - - // Verify the paragraph has the animation classes - const animatedParagraph = page.locator('p.scroll-anim-fade-in[data-scroll-anim="1"]'); - await expect(animatedParagraph).toBeVisible(); - - // Animation name should be defined (not 'none') - const animationName = await animatedParagraph.evaluate((el) => { - const styles = window.getComputedStyle(el); - return styles.animationName; - }); - - // Should have the scrollFadeIn animation name - expect(animationName).not.toBe('none'); - expect(animationName).toContain('scrollFadeIn'); - }); + test('should disable animations when prefers-reduced-motion is set', async ({ + page, + }: { + page: Page; + }) => { + // Set the prefers-reduced-motion preference + await page.emulateMedia({ reducedMotion: 'reduce' }); + + // Create a post with scroll animation + await page.goto('/wp-admin/post-new.php'); + await page.waitForLoadState('domcontentloaded'); + + // Close welcome dialog if present + const closeButton = page.getByRole('button', { name: 'Close' }); + if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { + await closeButton.click(); + } + + // Create post with animation + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill('Reduced Motion Test', { timeout: 15000 }); + + const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); + await addBlockButton.click({ timeout: 15000 }); + + const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); + await blockEditor.fill('This paragraph should not animate with reduced motion', { + timeout: 15000, + }); + + const animationTypeSelect = page.getByLabel('Animation Type'); + await animationTypeSelect.selectOption('Fade In'); + + // Publish the post + await page.getByRole('button', { name: 'Publish', exact: true }).click(); + const publishPanelButton = page + .getByLabel('Editor publish') + .getByRole('button', { name: 'Publish', exact: true }); + await publishPanelButton.click(); + + // Wait for post to be published + await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); + + // Get the post URL + const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); + const postUrl = await viewPostLink.getAttribute('href'); + + if (!postUrl) { + throw new Error('Could not get post URL'); + } + + // Visit the frontend post with reduced motion preference + await page.goto(postUrl); + await page.waitForLoadState('domcontentloaded'); + + // Verify the paragraph has the animation class but animations should be disabled via CSS + const animatedParagraph = page.locator('p.scroll-anim-fade-in[data-scroll-anim="1"]'); + await expect(animatedParagraph).toBeVisible(); + + // Check computed styles - with prefers-reduced-motion, the element should have: + // - opacity: 1 (not 0) + // - transform: none + // - animation: none + const computedStyles = await animatedParagraph.evaluate((el) => { + const styles = window.getComputedStyle(el); + return { + opacity: styles.opacity, + transform: styles.transform, + animation: styles.animation, + }; + }); + + // With reduced motion, the CSS should override the animation styles + expect(parseFloat(computedStyles.opacity)).toBe(1); + expect(computedStyles.transform).toBe('none'); + expect(computedStyles.animation).toContain('none'); + }); + + test('should disable parallax effect when prefers-reduced-motion is set', async ({ + page, + }: { + page: Page; + }) => { + // Set the prefers-reduced-motion preference + await page.emulateMedia({ reducedMotion: 'reduce' }); + + // Create a post with parallax + await page.goto('/wp-admin/post-new.php'); + await page.waitForLoadState('domcontentloaded'); + + // Close welcome dialog if present + const closeButton = page.getByRole('button', { name: 'Close' }); + if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { + await closeButton.click(); + } + + // Create post + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill('Parallax Reduced Motion Test', { timeout: 15000 }); + + const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); + await addBlockButton.click({ timeout: 15000 }); + + const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); + await blockEditor.fill('This paragraph has parallax disabled with reduced motion', { + timeout: 15000, + }); + + // Enable parallax + const parallaxToggle = page.getByLabel('Enable Parallax Effect'); + await parallaxToggle.click(); + + // Publish the post + await page.getByRole('button', { name: 'Publish', exact: true }).click(); + const publishPanelButton = page + .getByLabel('Editor publish') + .getByRole('button', { name: 'Publish', exact: true }); + await publishPanelButton.click(); + + await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); + + // Get the post URL + const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); + const postUrl = await viewPostLink.getAttribute('href'); + + if (!postUrl) { + throw new Error('Could not get post URL'); + } + + // Visit the frontend post + await page.goto(postUrl); + await page.waitForLoadState('domcontentloaded'); + + // Verify the paragraph has parallax data attribute but effect is disabled + const parallaxParagraph = page.locator('p[data-parallax="1"]'); + await expect(parallaxParagraph).toBeVisible(); + + // Check that animations are disabled + const computedStyles = await parallaxParagraph.evaluate((el) => { + const styles = window.getComputedStyle(el); + return { + animation: styles.animation, + transform: styles.transform, + }; + }); + + expect(computedStyles.animation).toContain('none'); + expect(computedStyles.transform).toBe('none'); + }); + + test('should disable clip-path animations when prefers-reduced-motion is set', async ({ + page, + }: { + page: Page; + }) => { + // Set the prefers-reduced-motion preference + await page.emulateMedia({ reducedMotion: 'reduce' }); + + // Create a post with circle reveal animation + await page.goto('/wp-admin/post-new.php'); + await page.waitForLoadState('domcontentloaded'); + + // Close welcome dialog if present + const closeButton = page.getByRole('button', { name: 'Close' }); + if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { + await closeButton.click(); + } + + // Create post + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill('Circle Reveal Reduced Motion Test', { timeout: 15000 }); + + const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); + await addBlockButton.click({ timeout: 15000 }); + + const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); + await blockEditor.fill('Circle reveal should be disabled with reduced motion', { + timeout: 15000, + }); + + const animationTypeSelect = page.getByLabel('Animation Type'); + await animationTypeSelect.selectOption('Circle Reveal'); + + // Publish the post + await page.getByRole('button', { name: 'Publish', exact: true }).click(); + const publishPanelButton = page + .getByLabel('Editor publish') + .getByRole('button', { name: 'Publish', exact: true }); + await publishPanelButton.click(); + + await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); + + // Get the post URL + const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); + const postUrl = await viewPostLink.getAttribute('href'); + + if (!postUrl) { + throw new Error('Could not get post URL'); + } + + // Visit the frontend post + await page.goto(postUrl); + await page.waitForLoadState('domcontentloaded'); + + // Verify clip-path is disabled + const revealParagraph = page.locator('p.scroll-anim-circle-reveal[data-scroll-anim="1"]'); + await expect(revealParagraph).toBeVisible(); + + const computedStyles = await revealParagraph.evaluate((el) => { + const styles = window.getComputedStyle(el); + return { + clipPath: styles.clipPath, + animation: styles.animation, + }; + }); + + expect(computedStyles.clipPath).toBe('none'); + expect(computedStyles.animation).toContain('none'); + }); + + test('should disable blur filter when prefers-reduced-motion is set', async ({ + page, + }: { + page: Page; + }) => { + // Set the prefers-reduced-motion preference + await page.emulateMedia({ reducedMotion: 'reduce' }); + + // Create a post with blur-in animation + await page.goto('/wp-admin/post-new.php'); + await page.waitForLoadState('domcontentloaded'); + + // Close welcome dialog if present + const closeButton = page.getByRole('button', { name: 'Close' }); + if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { + await closeButton.click(); + } + + // Create post + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill('Blur In Reduced Motion Test', { timeout: 15000 }); + + const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); + await addBlockButton.click({ timeout: 15000 }); + + const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); + await blockEditor.fill('Blur effect should be disabled with reduced motion', { + timeout: 15000, + }); + + const animationTypeSelect = page.getByLabel('Animation Type'); + await animationTypeSelect.selectOption('Blur In'); + + // Publish the post + await page.getByRole('button', { name: 'Publish', exact: true }).click(); + const publishPanelButton = page + .getByLabel('Editor publish') + .getByRole('button', { name: 'Publish', exact: true }); + await publishPanelButton.click(); + + await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); + + // Get the post URL + const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); + const postUrl = await viewPostLink.getAttribute('href'); + + if (!postUrl) { + throw new Error('Could not get post URL'); + } + + // Visit the frontend post + await page.goto(postUrl); + await page.waitForLoadState('domcontentloaded'); + + // Verify filter is disabled + const blurParagraph = page.locator('p.scroll-anim-blur-in[data-scroll-anim="1"]'); + await expect(blurParagraph).toBeVisible(); + + const computedStyles = await blurParagraph.evaluate((el) => { + const styles = window.getComputedStyle(el); + return { + filter: styles.filter, + opacity: styles.opacity, + animation: styles.animation, + }; + }); + + expect(computedStyles.filter).toBe('none'); + expect(parseFloat(computedStyles.opacity)).toBe(1); + expect(computedStyles.animation).toContain('none'); + }); + + test('should allow animations when prefers-reduced-motion is not set', async ({ + page, + }: { + page: Page; + }) => { + // Do NOT set reduced motion preference (default is 'no-preference') + await page.emulateMedia({ reducedMotion: 'no-preference' }); + + // Create a post with animation + await page.goto('/wp-admin/post-new.php'); + await page.waitForLoadState('domcontentloaded'); + + // Close welcome dialog if present + const closeButton = page.getByRole('button', { name: 'Close' }); + if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { + await closeButton.click(); + } + + // Create post + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill('Normal Animation Test', { timeout: 15000 }); + + const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); + await addBlockButton.click({ timeout: 15000 }); + + const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); + await blockEditor.fill('This paragraph should animate normally', { timeout: 15000 }); + + const animationTypeSelect = page.getByLabel('Animation Type'); + await animationTypeSelect.selectOption('Fade In'); + + // Publish the post + await page.getByRole('button', { name: 'Publish', exact: true }).click(); + const publishPanelButton = page + .getByLabel('Editor publish') + .getByRole('button', { name: 'Publish', exact: true }); + await publishPanelButton.click(); + + await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); + + // Get the post URL + const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); + const postUrl = await viewPostLink.getAttribute('href'); + + if (!postUrl) { + throw new Error('Could not get post URL'); + } + + // Visit the frontend post + await page.goto(postUrl); + await page.waitForLoadState('domcontentloaded'); + + // Verify the paragraph has the animation classes + const animatedParagraph = page.locator('p.scroll-anim-fade-in[data-scroll-anim="1"]'); + await expect(animatedParagraph).toBeVisible(); + + // Animation name should be defined (not 'none') + const animationName = await animatedParagraph.evaluate((el) => { + const styles = window.getComputedStyle(el); + return styles.animationName; + }); + + // Should have the scrollFadeIn animation name + expect(animationName).not.toBe('none'); + expect(animationName).toContain('scrollFadeIn'); + }); }); diff --git a/tests/scroll-block.spec.ts b/tests/scroll-block.spec.ts index 36314f8..79796f9 100644 --- a/tests/scroll-block.spec.ts +++ b/tests/scroll-block.spec.ts @@ -1,330 +1,332 @@ import { test, expect, type Page } from '@playwright/test'; test.describe('WordPress Playground Setup', () => { - test('should load WordPress homepage', async ({ page }: { page: Page }) => { - await page.goto('/'); + test('should load WordPress homepage', async ({ page }: { page: Page }) => { + await page.goto('/'); - // Wait for the page to be fully loaded - await page.waitForLoadState('networkidle'); + // Wait for the page to be fully loaded + await page.waitForLoadState('networkidle'); - // Check that WordPress loaded successfully - await expect(page).toHaveTitle(/WordPress Scroll-driven block/); + // Check that WordPress loaded successfully + await expect(page).toHaveTitle(/WordPress Scroll-driven block/); - // Verify we can see WordPress content - const body = await page.locator('body'); - await expect(body).toBeVisible(); - }); + // Verify we can see WordPress content + const body = await page.locator('body'); + await expect(body).toBeVisible(); + }); - test('should have My Scroll Block plugin activated', async ({ page }: { page: Page }) => { - await page.goto('/wp-admin/plugins.php'); - await page.waitForLoadState('networkidle'); + test('should have My Scroll Block plugin activated', async ({ page }: { page: Page }) => { + await page.goto('/wp-admin/plugins.php'); + await page.waitForLoadState('networkidle'); - // Look for the plugin row - const pluginRow = page.locator('tr').filter({ hasText: 'My Scroll Block' }); - await expect(pluginRow).toBeVisible(); + // Look for the plugin row + const pluginRow = page.locator('tr').filter({ hasText: 'My Scroll Block' }); + await expect(pluginRow).toBeVisible(); - // Verify the plugin is active (shows "Deactivate" link) - const deactivateLink = pluginRow.getByRole('link', { name: 'Deactivate My Scroll Block' }); - await expect(deactivateLink).toBeVisible(); + // Verify the plugin is active (shows "Deactivate" link) + const deactivateLink = pluginRow.getByRole('link', { name: 'Deactivate My Scroll Block' }); + await expect(deactivateLink).toBeVisible(); - // Verify plugin description - await expect(pluginRow).toContainText( - 'Adds a Scroll Animation panel to supported core blocks.' - ); - }); + // Verify plugin description + await expect(pluginRow).toContainText( + 'Adds a Scroll Animation panel to supported core blocks.' + ); + }); }); test.describe('Block Editor - Scroll Animation Panel', () => { - test('should show Scroll Animation panel for paragraph block', async ({ - page, - }: { - page: Page; - }) => { - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Add a paragraph block - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - // Type some text - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('Test paragraph with scroll animation', { timeout: 15000 }); - - // Check for Scroll Animation panel in the sidebar - const scrollAnimationHeading = page.getByRole('heading', { name: 'Scroll Animation' }); - await expect(scrollAnimationHeading).toBeVisible(); - - // Verify Animation Type dropdown exists - const animationTypeSelect = page.getByLabel('Animation Type'); - await expect(animationTypeSelect).toBeVisible(); - }); - - test('should apply Fade In animation to paragraph', async ({ page }: { page: Page }) => { - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Add title - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill('Test Animation Post'); - - // Add paragraph - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click(); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('This paragraph will fade in'); - - // Select Fade In animation - const animationTypeSelect = page.getByLabel('Animation Type'); - await animationTypeSelect.selectOption('Fade In'); - - // Verify the animation was applied (look for the indicator button in the frame) - const animationIndicator = editorFrame.getByRole('button', { - name: /Scroll Animation Applied/, - }); - await expect(animationIndicator).toBeVisible(); - }); - - test('should have all animation type options available', async ({ page }: { page: Page }) => { - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Add a paragraph block - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - // Check animation options - const animationTypeSelect = page.getByLabel('Animation Type'); - - // Verify all expected animation types are available - const expectedOptions: string[] = [ - 'None', - 'Fade In', - 'Slide In Left', - 'Slide In Right', - 'Slide In Up', - 'Slide In Down', - 'Scale Up', - 'Rotate In', - ]; - - for (const option of expectedOptions) { - const optionElement = animationTypeSelect.locator(`option:has-text("${option}")`); - await expect(optionElement).toBeAttached(); - } - }); + test('should show Scroll Animation panel for paragraph block', async ({ + page, + }: { + page: Page; + }) => { + await page.goto('/wp-admin/post-new.php'); + await page.waitForLoadState('domcontentloaded'); + + // Close welcome dialog if present + const closeButton = page.getByRole('button', { name: 'Close' }); + if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { + await closeButton.click(); + } + + // Add a paragraph block + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); + await addBlockButton.click({ timeout: 15000 }); + + // Type some text + const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); + await blockEditor.fill('Test paragraph with scroll animation', { timeout: 15000 }); + + // Check for Scroll Animation panel in the sidebar + const scrollAnimationHeading = page.getByRole('heading', { name: 'Scroll Animation' }); + await expect(scrollAnimationHeading).toBeVisible(); + + // Verify Animation Type dropdown exists + const animationTypeSelect = page.getByLabel('Animation Type'); + await expect(animationTypeSelect).toBeVisible(); + }); + + test('should apply Fade In animation to paragraph', async ({ page }: { page: Page }) => { + await page.goto('/wp-admin/post-new.php'); + await page.waitForLoadState('domcontentloaded'); + + // Close welcome dialog if present + const closeButton = page.getByRole('button', { name: 'Close' }); + if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { + await closeButton.click(); + } + + // Add title + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill('Test Animation Post'); + + // Add paragraph + const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); + await addBlockButton.click(); + + const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); + await blockEditor.fill('This paragraph will fade in'); + + // Select Fade In animation + const animationTypeSelect = page.getByLabel('Animation Type'); + await animationTypeSelect.selectOption('Fade In'); + + // Verify the animation was applied (look for the indicator button in the frame) + const animationIndicator = editorFrame.getByRole('button', { + name: /Scroll Animation Applied/, + }); + await expect(animationIndicator).toBeVisible(); + }); + + test('should have all animation type options available', async ({ page }: { page: Page }) => { + await page.goto('/wp-admin/post-new.php'); + await page.waitForLoadState('domcontentloaded'); + + // Close welcome dialog if present + const closeButton = page.getByRole('button', { name: 'Close' }); + if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { + await closeButton.click(); + } + + // Add a paragraph block + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); + await addBlockButton.click({ timeout: 15000 }); + + // Check animation options + const animationTypeSelect = page.getByLabel('Animation Type'); + + // Verify all expected animation types are available + const expectedOptions: string[] = [ + 'None', + 'Fade In', + 'Slide In Left', + 'Slide In Right', + 'Slide In Up', + 'Slide In Down', + 'Scale Up', + 'Rotate In', + ]; + + for (const option of expectedOptions) { + const optionElement = animationTypeSelect.locator(`option:has-text("${option}")`); + await expect(optionElement).toBeAttached(); + } + }); }); test.describe('Frontend - Scroll Animation Rendering', () => { - test('should render published post with scroll animation classes', async ({ - page, - }: { - page: Page; - }) => { - // First create and publish a post with animation - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Create post with animation - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill('Frontend Animation Test', { timeout: 15000 }); - - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('This paragraph has a fade in animation', { timeout: 15000 }); - - const animationTypeSelect = page.getByLabel('Animation Type'); - await animationTypeSelect.selectOption('Fade In'); - - // Publish the post - await page.getByRole('button', { name: 'Publish', exact: true }).click(); - const publishPanelButton = page - .getByLabel('Editor publish') - .getByRole('button', { name: 'Publish', exact: true }); - await publishPanelButton.click(); - - // Wait for post to be published - await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); - - // Get the post URL - const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); - const postUrl = await viewPostLink.getAttribute('href'); - - if (!postUrl) { - throw new Error('Could not get post URL'); - } - - // Visit the frontend post - await page.goto(postUrl); - await page.waitForLoadState('domcontentloaded'); - - // Verify the paragraph has correct scroll animation classes and attributes - const animatedParagraph = page.locator( - 'p.scroll-anim-block.scroll-anim-fade-in[data-scroll-anim="1"]' - ); - await expect(animatedParagraph).toBeVisible(); - await expect(animatedParagraph).toContainText('This paragraph has a fade in animation'); - }); - - test('should apply different animation types correctly on frontend', async ({ - page, - }: { - page: Page; - }) => { - interface AnimationType { - type: string; - class: string; - } - - const animations: AnimationType[] = [ - { type: 'Slide In Left', class: 'scroll-anim-slide-in-left' }, - { type: 'Scale Up', class: 'scroll-anim-scale-up' }, - ]; - - for (const animation of animations) { - // Create post - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill(`Test ${animation.type}`, { timeout: 15000 }); - - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill(`Testing ${animation.type}`, { timeout: 15000 }); - - const animationTypeSelect = page.getByLabel('Animation Type'); - await animationTypeSelect.selectOption(animation.type); - - // Publish - await page.getByRole('button', { name: 'Publish', exact: true }).click(); - const publishPanelButton = page - .getByLabel('Editor publish') - .getByRole('button', { name: 'Publish', exact: true }); - await publishPanelButton.click(); - - await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); - - // Visit frontend - const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); - const postUrl = await viewPostLink.getAttribute('href'); - - if (!postUrl) { - throw new Error('Could not get post URL'); - } - - await page.goto(postUrl); - await page.waitForLoadState('domcontentloaded'); - - // Verify animation class - look for any paragraph with the animation class - const animatedElement = page.locator(`p.${animation.class}`); - const isVisible = await animatedElement.isVisible({ timeout: 5000 }).catch(() => false); - - // If not found, check if post content exists at all - if (!isVisible) { - const anyParagraph = page.locator('p').first(); - await expect(anyParagraph).toBeVisible(); - } else { - await expect(animatedElement).toBeVisible(); - } - } - }); - - test('should load scroll animation CSS styles', async ({ page }: { page: Page }) => { - // First go to homepage to get a valid URL - await page.goto('/'); - await page.waitForLoadState('domcontentloaded'); - - // Try to find the first post link - const postLink = page.locator('a[rel="bookmark"]').first(); - const firstPostUrl = await postLink.getAttribute('href').catch(() => null); - - if (!firstPostUrl) { - // Skip this test if there are no posts - return; - } - - await page.goto(firstPostUrl); - await page.waitForLoadState('domcontentloaded'); - - // Check if the style-index.css is loaded - const stylesheets = await page.evaluate((): string[] => { - return Array.from(document.styleSheets) - .map((sheet) => sheet.href) - .filter((href): href is string => href !== null && href.includes('style-index.css')); - }); - - expect(stylesheets.length).toBeGreaterThanOrEqual(0); // CSS may not be loaded if no animations are present - }); + test('should render published post with scroll animation classes', async ({ + page, + }: { + page: Page; + }) => { + // First create and publish a post with animation + await page.goto('/wp-admin/post-new.php'); + await page.waitForLoadState('domcontentloaded'); + + // Close welcome dialog if present + const closeButton = page.getByRole('button', { name: 'Close' }); + if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { + await closeButton.click(); + } + + // Create post with animation + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill('Frontend Animation Test', { timeout: 15000 }); + + const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); + await addBlockButton.click({ timeout: 15000 }); + + const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); + await blockEditor.fill('This paragraph has a fade in animation', { timeout: 15000 }); + + const animationTypeSelect = page.getByLabel('Animation Type'); + await animationTypeSelect.selectOption('Fade In'); + + // Publish the post + await page.getByRole('button', { name: 'Publish', exact: true }).click(); + const publishPanelButton = page + .getByLabel('Editor publish') + .getByRole('button', { name: 'Publish', exact: true }); + await publishPanelButton.click(); + + // Wait for post to be published + await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); + + // Get the post URL + const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); + const postUrl = await viewPostLink.getAttribute('href'); + + if (!postUrl) { + throw new Error('Could not get post URL'); + } + + // Visit the frontend post + await page.goto(postUrl); + await page.waitForLoadState('domcontentloaded'); + + // Verify the paragraph has correct scroll animation classes and attributes + const animatedParagraph = page.locator( + 'p.scroll-anim-block.scroll-anim-fade-in[data-scroll-anim="1"]' + ); + await expect(animatedParagraph).toBeVisible(); + await expect(animatedParagraph).toContainText('This paragraph has a fade in animation'); + }); + + test('should apply different animation types correctly on frontend', async ({ + page, + }: { + page: Page; + }) => { + interface AnimationType { + type: string; + class: string; + } + + const animations: AnimationType[] = [ + { type: 'Slide In Left', class: 'scroll-anim-slide-in-left' }, + { type: 'Scale Up', class: 'scroll-anim-scale-up' }, + ]; + + for (const animation of animations) { + // Create post + await page.goto('/wp-admin/post-new.php'); + await page.waitForLoadState('domcontentloaded'); + + const closeButton = page.getByRole('button', { name: 'Close' }); + if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { + await closeButton.click(); + } + + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill(`Test ${animation.type}`, { timeout: 15000 }); + + const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); + await addBlockButton.click({ timeout: 15000 }); + + const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); + await blockEditor.fill(`Testing ${animation.type}`, { timeout: 15000 }); + + const animationTypeSelect = page.getByLabel('Animation Type'); + await animationTypeSelect.selectOption(animation.type); + + // Publish + await page.getByRole('button', { name: 'Publish', exact: true }).click(); + const publishPanelButton = page + .getByLabel('Editor publish') + .getByRole('button', { name: 'Publish', exact: true }); + await publishPanelButton.click(); + + await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); + + // Visit frontend + const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); + const postUrl = await viewPostLink.getAttribute('href'); + + if (!postUrl) { + throw new Error('Could not get post URL'); + } + + await page.goto(postUrl); + await page.waitForLoadState('domcontentloaded'); + + // Verify animation class - look for any paragraph with the animation class + const animatedElement = page.locator(`p.${animation.class}`); + const isVisible = await animatedElement.isVisible({ timeout: 5000 }).catch(() => false); + + // If not found, check if post content exists at all + if (!isVisible) { + const anyParagraph = page.locator('p').first(); + await expect(anyParagraph).toBeVisible(); + } else { + await expect(animatedElement).toBeVisible(); + } + } + }); + + test('should load scroll animation CSS styles', async ({ page }: { page: Page }) => { + // First go to homepage to get a valid URL + await page.goto('/'); + await page.waitForLoadState('domcontentloaded'); + + // Try to find the first post link + const postLink = page.locator('a[rel="bookmark"]').first(); + const firstPostUrl = await postLink.getAttribute('href').catch(() => null); + + if (!firstPostUrl) { + // Skip this test if there are no posts + return; + } + + await page.goto(firstPostUrl); + await page.waitForLoadState('domcontentloaded'); + + // Check if the style-index.css is loaded + const stylesheets = await page.evaluate((): string[] => { + return Array.from(document.styleSheets) + .map((sheet) => sheet.href) + .filter( + (href): href is string => href !== null && href.includes('style-index.css') + ); + }); + + expect(stylesheets.length).toBeGreaterThanOrEqual(0); // CSS may not be loaded if no animations are present + }); }); test.describe('Plugin Compatibility', () => { - test('should work with WordPress 6.8+', async ({ page }: { page: Page }) => { - await page.goto('/wp-admin'); - await page.waitForLoadState('domcontentloaded'); - - // Get WordPress version from the page HTML - const version = await page.evaluate((): string => { - const versionElement = Array.from(document.querySelectorAll('*')).find((el) => - el.textContent?.match(/Version \d+\.\d+/) - ); - return versionElement?.textContent || ''; - }); - - // If we can't find version text, at least verify the admin page loaded - const adminPageBody = page.locator('.wp-admin'); - await expect(adminPageBody).toBeVisible(); - - if (version) { - // Extract version like "Version 6.8.3" or just "6.8" - const match = version.match(/Version?\s*(\d+)\.(\d+)/); - if (match) { - const major = parseInt(match[1], 10); - const minor = parseInt(match[2], 10); - expect(major).toBeGreaterThanOrEqual(6); - if (major === 6) { - expect(minor).toBeGreaterThanOrEqual(7); - } - } - } - }); + test('should work with WordPress 6.8+', async ({ page }: { page: Page }) => { + await page.goto('/wp-admin'); + await page.waitForLoadState('domcontentloaded'); + + // Get WordPress version from the page HTML + const version = await page.evaluate((): string => { + const versionElement = Array.from(document.querySelectorAll('*')).find((el) => + el.textContent?.match(/Version \d+\.\d+/) + ); + return versionElement?.textContent || ''; + }); + + // If we can't find version text, at least verify the admin page loaded + const adminPageBody = page.locator('.wp-admin'); + await expect(adminPageBody).toBeVisible(); + + if (version) { + // Extract version like "Version 6.8.3" or just "6.8" + const match = version.match(/Version?\s*(\d+)\.(\d+)/); + if (match) { + const major = parseInt(match[1], 10); + const minor = parseInt(match[2], 10); + expect(major).toBeGreaterThanOrEqual(6); + if (major === 6) { + expect(minor).toBeGreaterThanOrEqual(7); + } + } + } + }); }); diff --git a/webpack.config.js b/webpack.config.js index 6924cd7..5e3d726 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,19 +2,19 @@ const defaultConfig = require('@wordpress/scripts/config/webpack.config'); // If defaultConfig is an array, extend each config if (Array.isArray(defaultConfig)) { - module.exports = defaultConfig.map((config) => ({ - ...config, - entry: { - index: './src/index.js', - ...(typeof config.entry === 'object' ? config.entry : {}), - }, - })); + module.exports = defaultConfig.map((config) => ({ + ...config, + entry: { + index: './src/index.js', + ...(typeof config.entry === 'object' ? config.entry : {}), + }, + })); } else { - module.exports = { - ...defaultConfig, - entry: { - index: './src/index.js', - ...(typeof defaultConfig.entry === 'object' ? defaultConfig.entry : {}), - }, - }; + module.exports = { + ...defaultConfig, + entry: { + index: './src/index.js', + ...(typeof defaultConfig.entry === 'object' ? defaultConfig.entry : {}), + }, + }; } From 3f16c7e1d573704ef368a45b347632c3b4c3115f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Jan 2026 22:22:27 +0000 Subject: [PATCH 3/3] Fix Playwright tests for WordPress block editor - Add helper functions to properly dismiss WordPress welcome modal - Handle multiple possible modal dismiss button patterns - Add helper function for adding paragraph blocks with proper waiting - Improve element waiting with explicit timeouts - Wait for sidebar to be ready before interacting with animation controls - Apply code formatting with Prettier --- .github/prompts/gemini-scheduled-triage.md | 15 +- .github/prompts/gemini-triage.md | 21 +- CLAUDE.md | 60 +- playwright.config.js | 76 +- src/editor.css | 48 +- src/index.js | 771 ++++++++++----------- src/progress-block/editor.css | 38 +- src/progress-block/index.js | 277 ++++---- src/progress-block/style.css | 218 +++--- src/style.css | 619 ++++++++--------- tests/global-setup.ts | 84 +-- tests/global-teardown.ts | 16 +- tests/reduced-motion.spec.ts | 758 ++++++++++---------- tests/scroll-block.spec.ts | 662 +++++++++--------- webpack.config.js | 28 +- 15 files changed, 1853 insertions(+), 1838 deletions(-) diff --git a/.github/prompts/gemini-scheduled-triage.md b/.github/prompts/gemini-scheduled-triage.md index 222613f..259f704 100644 --- a/.github/prompts/gemini-scheduled-triage.md +++ b/.github/prompts/gemini-scheduled-triage.md @@ -1,12 +1,14 @@ You are a GitHub Issue Triage Assistant. Your goal is to analyze a list of GitHub issues and select the most appropriate labels for each one. ## Inputs + - **Issues to Triage:** {{ ISSUES_TO_TRIAGE }} - - *Note:* This should be a JSON string. If it looks like a placeholder, run `printenv ISSUES_TO_TRIAGE`. + - _Note:_ This should be a JSON string. If it looks like a placeholder, run `printenv ISSUES_TO_TRIAGE`. - **Available Labels:** {{ AVAILABLE_LABELS }} - - *Note:* If placeholder, run `printenv AVAILABLE_LABELS`. + - _Note:_ If placeholder, run `printenv AVAILABLE_LABELS`. ## Instructions + 1. **Parse** the list of issues. 2. **Analyze** each issue to understand its intent. 3. **Select** matching labels from the "Available Labels" list for each issue. @@ -21,10 +23,11 @@ You are a GitHub Issue Triage Assistant. Your goal is to analyze a list of GitHu ] ``` 5. **Action:** Set the `TRIAGED_ISSUES` environment variable. - * Method: `run_shell_command` -> `echo "TRIAGED_ISSUES=..." >> $GITHUB_ENV` - * **Crucial:** Escape the JSON string correctly for bash. - * Example: `echo 'TRIAGED_ISSUES=[{"issue_number":1,"labels_to_set":["bug"]}]' >> $GITHUB_ENV` + - Method: `run_shell_command` -> `echo "TRIAGED_ISSUES=..." >> $GITHUB_ENV` + - **Crucial:** Escape the JSON string correctly for bash. + - Example: `echo 'TRIAGED_ISSUES=[{"issue_number":1,"labels_to_set":["bug"]}]' >> $GITHUB_ENV` ## Constraints + - Only use provided labels. -- Return valid JSON. \ No newline at end of file +- Return valid JSON. diff --git a/.github/prompts/gemini-triage.md b/.github/prompts/gemini-triage.md index f32bf99..e140c87 100644 --- a/.github/prompts/gemini-triage.md +++ b/.github/prompts/gemini-triage.md @@ -1,25 +1,28 @@ You are a GitHub Issue Triage Assistant. Your goal is to analyze a GitHub issue and select the most appropriate labels from a predefined list. ## Inputs + The following inputs are provided. If they appear as placeholders (e.g. `{{ ... }}`), use the `run_shell_command` tool to retrieve the values from the environment variables. - **Issue Title:** {{ ISSUE_TITLE }} - - *Fallback:* `echo "$ISSUE_TITLE"` + - _Fallback:_ `echo "$ISSUE_TITLE"` - **Issue Body:** {{ ISSUE_BODY }} - - *Fallback:* `echo "$ISSUE_BODY"` + - _Fallback:_ `echo "$ISSUE_BODY"` - **Available Labels:** {{ AVAILABLE_LABELS }} - - *Fallback:* `echo "$AVAILABLE_LABELS"` + - _Fallback:_ `echo "$AVAILABLE_LABELS"` ## Instructions + 1. **Analyze** the issue title and body. 2. **Review** the "Available Labels" list. 3. **Select** the labels that best categorize this issue. - * If it's a bug, use `bug` (if available). - * If it's a feature request, use `enhancement` (if available). - * Do not invent new labels. + - If it's a bug, use `bug` (if available). + - If it's a feature request, use `enhancement` (if available). + - Do not invent new labels. 4. **Action:** Set the `SELECTED_LABELS` environment variable. - * Format: Comma-separated list (e.g., `bug,high-priority`). - * Method: `run_shell_command` -> `echo "SELECTED_LABELS=label1,label2" >> $GITHUB_ENV` + - Format: Comma-separated list (e.g., `bug,high-priority`). + - Method: `run_shell_command` -> `echo "SELECTED_LABELS=label1,label2" >> $GITHUB_ENV` ## Constraints -- If no labels fit well, do not set the variable. \ No newline at end of file + +- If no labels fit well, do not set the variable. diff --git a/CLAUDE.md b/CLAUDE.md index 5ec2060..2415b71 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -113,10 +113,12 @@ const SUPPORTED_BLOCKS = [ ### Animation Types (17 total) **Entry Animations:** + - `none`, `fade-in`, `slide-in-left`, `slide-in-right`, `slide-in-up`, `slide-in-down` - `scale-up`, `rotate-in`, `blur-in`, `rotate-3d-in`, `circle-reveal`, `curtain-reveal` **In-and-Out Animations:** + - `fade-in-out`, `slide-up-in-out`, `scale-in-out`, `rotate-in-out`, `rotate-3d-in-out` ### Animation Timing Presets @@ -127,20 +129,21 @@ const SUPPORTED_BLOCKS = [ Each supported block gains these attributes: -| Attribute | Type | Default | Description | -|-----------|------|---------|-------------| -| `animationType` | string | `'none'` | Selected animation type | -| `animationRange` | string | `'default'` | Timing preset | -| `animationEntryStart` | number | `20` | Custom entry start (%) | -| `animationEntryEnd` | number | `100` | Custom entry end (%) | -| `animationExitStart` | number | `0` | Custom exit start (%) | -| `animationExitEnd` | number | `100` | Custom exit end (%) | -| `parallaxEnabled` | boolean | `false` | Enable parallax effect | -| `parallaxStrength` | number | `50` | Parallax displacement strength | +| Attribute | Type | Default | Description | +| --------------------- | ------- | ----------- | ------------------------------ | +| `animationType` | string | `'none'` | Selected animation type | +| `animationRange` | string | `'default'` | Timing preset | +| `animationEntryStart` | number | `20` | Custom entry start (%) | +| `animationEntryEnd` | number | `100` | Custom entry end (%) | +| `animationExitStart` | number | `0` | Custom exit start (%) | +| `animationExitEnd` | number | `100` | Custom exit end (%) | +| `parallaxEnabled` | boolean | `false` | Enable parallax effect | +| `parallaxStrength` | number | `50` | Parallax displacement strength | ### CSS Classes & Data Attributes **Frontend output:** + - Class: `scroll-anim-block` - Main animation class - Class: `scroll-anim-{type}` - Specific animation type (e.g., `scroll-anim-fade-in`) - Attribute: `data-scroll-anim="1"` - Animation marker @@ -149,16 +152,16 @@ Each supported block gains these attributes: ## Key Files & Their Roles -| File | Purpose | -|------|---------| -| `my-scroll-block.php` | Plugin entry; enqueues assets; `render_block` filter for frontend class injection | -| `src/index.js` | Block filters; attribute registration; UI controls; markup manipulation | -| `src/style.css` | CSS scroll timeline rules for all animation types | -| `src/editor.css` | Editor UI styles (animation indicator badge) | -| `src/progress-block/` | Reading Progress Bar custom block | -| `tests/scroll-block.spec.ts` | Main e2e tests for editor and frontend | -| `tests/reduced-motion.spec.ts` | Accessibility tests for reduced motion | -| `tests/global-setup.ts` | WordPress Playground startup with plugin mounting | +| File | Purpose | +| ------------------------------ | --------------------------------------------------------------------------------- | +| `my-scroll-block.php` | Plugin entry; enqueues assets; `render_block` filter for frontend class injection | +| `src/index.js` | Block filters; attribute registration; UI controls; markup manipulation | +| `src/style.css` | CSS scroll timeline rules for all animation types | +| `src/editor.css` | Editor UI styles (animation indicator badge) | +| `src/progress-block/` | Reading Progress Bar custom block | +| `tests/scroll-block.spec.ts` | Main e2e tests for editor and frontend | +| `tests/reduced-motion.spec.ts` | Accessibility tests for reduced motion | +| `tests/global-setup.ts` | WordPress Playground startup with plugin mounting | ## Testing Architecture @@ -238,27 +241,30 @@ npx playwright test -g "test-name-pattern" # Run specific test ## Debugging -| Issue | Solution | -|-------|----------| -| Editor issues | Check browser console; WordPress error logs | -| Rendering issues | Run `npm run lint:js` and `npm run typecheck` | -| Test failures | Run `npm run test:headed` or `npm run test:debug` | -| Build errors | Check `npm run build` output; verify imports | -| Port 9400 in use | Kill with `pkill -f "wp-playground"` | +| Issue | Solution | +| ---------------- | ------------------------------------------------- | +| Editor issues | Check browser console; WordPress error logs | +| Rendering issues | Run `npm run lint:js` and `npm run typecheck` | +| Test failures | Run `npm run test:headed` or `npm run test:debug` | +| Build errors | Check `npm run build` output; verify imports | +| Port 9400 in use | Kill with `pkill -f "wp-playground"` | ## CI/CD Workflows ### playwright.yml + - Runs on: push to main/master, PRs - Runs e2e tests with Chromium - Uploads HTML report on failure ### build-plugin.yml + - Runs on: push, PR, release, manual - Lints JS/CSS, builds assets, creates zip - Auto-uploads to releases on release events ### pr-preview.yml + - Generates WordPress Playground preview links for PRs ## WordPress Playground Testing diff --git a/playwright.config.js b/playwright.config.js index 4b7de00..945828e 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -4,54 +4,54 @@ import { defineConfig, devices } from '@playwright/test'; * @see https://playwright.dev/docs/test-configuration */ export default defineConfig({ - testDir: './tests', + testDir: './tests', - /* Global setup to start WordPress Playground */ - globalSetup: './tests/global-setup.ts', - globalTeardown: './tests/global-teardown.ts', + /* Global setup to start WordPress Playground */ + globalSetup: './tests/global-setup.ts', + globalTeardown: './tests/global-teardown.ts', - /* Run tests in files in parallel */ - fullyParallel: true, + /* Run tests in files in parallel */ + fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://127.0.0.1:9400', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://127.0.0.1:9400', - /* Maximum time for each action (e.g. click, fill, etc.) */ - actionTimeout: 5000, + /* Maximum time for each action (e.g. click, fill, etc.) */ + actionTimeout: 5000, - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, - /* Maximum time one test can run for */ - timeout: 30000, + /* Maximum time one test can run for */ + timeout: 30000, - /* Configure projects for Chromium only */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - ], + /* Configure projects for Chromium only */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:9400', - // reuseExistingServer: !process.env.CI, - // }, + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:9400', + // reuseExistingServer: !process.env.CI, + // }, }); diff --git a/src/editor.css b/src/editor.css index 737683c..2f14c21 100644 --- a/src/editor.css +++ b/src/editor.css @@ -1,30 +1,30 @@ /* Animation indicator icon */ .scroll-anim-indicator-wrapper { - position: relative; + position: relative; - .scroll-anim-indicator { - position: absolute; - top: 0; - left: 0; - background: #ff69b4; - border-radius: 100%; - aspect-ratio: 1; - width: 24px; - padding: 5px; - display: flex; - justify-content: center; - align-items: center; - transform: translate(-50%, -50%); - opacity: 0.3; - transition: opacity 0.2s ease; + .scroll-anim-indicator { + position: absolute; + top: 0; + left: 0; + background: #ff69b4; + border-radius: 100%; + aspect-ratio: 1; + width: 24px; + padding: 5px; + display: flex; + justify-content: center; + align-items: center; + transform: translate(-50%, -50%); + opacity: 0.3; + transition: opacity 0.2s ease; - &:hover { - opacity: 1; - } + &:hover { + opacity: 1; + } - svg { - width: 12px; - height: 12px; - } - } + svg { + width: 12px; + height: 12px; + } + } } diff --git a/src/index.js b/src/index.js index d02a2ef..59faffd 100644 --- a/src/index.js +++ b/src/index.js @@ -15,440 +15,405 @@ import './progress-block/index.js'; * Internal dependencies */ const SUPPORTED_BLOCKS = [ - 'core/image', - 'core/paragraph', - 'core/columns', - 'core/group', - 'core/heading', + 'core/image', + 'core/paragraph', + 'core/columns', + 'core/group', + 'core/heading', ]; const ANIMATION_OPTIONS = [ - { label: __('None', 'my-scroll-block'), value: 'none' }, - { label: __('Fade In', 'my-scroll-block'), value: 'fade-in' }, - { label: __('Slide In Left', 'my-scroll-block'), value: 'slide-in-left' }, - { label: __('Slide In Right', 'my-scroll-block'), value: 'slide-in-right' }, - { label: __('Slide In Up', 'my-scroll-block'), value: 'slide-in-up' }, - { label: __('Slide In Down', 'my-scroll-block'), value: 'slide-in-down' }, - { label: __('Scale Up', 'my-scroll-block'), value: 'scale-up' }, - { label: __('Rotate In', 'my-scroll-block'), value: 'rotate-in' }, - { label: __('Blur In', 'my-scroll-block'), value: 'blur-in' }, - { label: __('3D Rotate In', 'my-scroll-block'), value: 'rotate-3d-in' }, - { label: __('Circle Reveal', 'my-scroll-block'), value: 'circle-reveal' }, - { label: __('Curtain Reveal', 'my-scroll-block'), value: 'curtain-reveal' }, - { label: __('🔄 Fade In & Out', 'my-scroll-block'), value: 'fade-in-out' }, - { label: __('🔄 Slide Up In & Out', 'my-scroll-block'), value: 'slide-up-in-out' }, - { label: __('🔄 Scale In & Out', 'my-scroll-block'), value: 'scale-in-out' }, - { label: __('🔄 Rotate In & Out', 'my-scroll-block'), value: 'rotate-in-out' }, - { label: __('🔄 3D Rotate In & Out', 'my-scroll-block'), value: 'rotate-3d-in-out' }, + { label: __('None', 'my-scroll-block'), value: 'none' }, + { label: __('Fade In', 'my-scroll-block'), value: 'fade-in' }, + { label: __('Slide In Left', 'my-scroll-block'), value: 'slide-in-left' }, + { label: __('Slide In Right', 'my-scroll-block'), value: 'slide-in-right' }, + { label: __('Slide In Up', 'my-scroll-block'), value: 'slide-in-up' }, + { label: __('Slide In Down', 'my-scroll-block'), value: 'slide-in-down' }, + { label: __('Scale Up', 'my-scroll-block'), value: 'scale-up' }, + { label: __('Rotate In', 'my-scroll-block'), value: 'rotate-in' }, + { label: __('Blur In', 'my-scroll-block'), value: 'blur-in' }, + { label: __('3D Rotate In', 'my-scroll-block'), value: 'rotate-3d-in' }, + { label: __('Circle Reveal', 'my-scroll-block'), value: 'circle-reveal' }, + { label: __('Curtain Reveal', 'my-scroll-block'), value: 'curtain-reveal' }, + { label: __('🔄 Fade In & Out', 'my-scroll-block'), value: 'fade-in-out' }, + { label: __('🔄 Slide Up In & Out', 'my-scroll-block'), value: 'slide-up-in-out' }, + { label: __('🔄 Scale In & Out', 'my-scroll-block'), value: 'scale-in-out' }, + { label: __('🔄 Rotate In & Out', 'my-scroll-block'), value: 'rotate-in-out' }, + { label: __('🔄 3D Rotate In & Out', 'my-scroll-block'), value: 'rotate-3d-in-out' }, ]; const RANGE_OPTIONS = [ - { label: __('Default (20% - 100%)', 'my-scroll-block'), value: 'default' }, - { label: __('Quick (0% - 50%)', 'my-scroll-block'), value: 'quick' }, - { label: __('Slow (10% - 100%)', 'my-scroll-block'), value: 'slow' }, - { label: __('Late Start (50% - 100%)', 'my-scroll-block'), value: 'late' }, - { label: __('Custom', 'my-scroll-block'), value: 'custom' }, + { label: __('Default (20% - 100%)', 'my-scroll-block'), value: 'default' }, + { label: __('Quick (0% - 50%)', 'my-scroll-block'), value: 'quick' }, + { label: __('Slow (10% - 100%)', 'my-scroll-block'), value: 'slow' }, + { label: __('Late Start (50% - 100%)', 'my-scroll-block'), value: 'late' }, + { label: __('Custom', 'my-scroll-block'), value: 'custom' }, ]; // 1) Extend attributes for supported blocks. addFilter('blocks.registerBlockType', 'my-scroll-block/extend-attributes', (settings, name) => { - if (!SUPPORTED_BLOCKS.includes(name)) { - return settings; - } - return { - ...settings, - attributes: { - ...settings.attributes, - animationType: { - type: 'string', - default: 'none', - }, - animationRange: { - type: 'string', - default: 'default', - }, - animationEntryStart: { - type: 'number', - default: 20, - }, - animationEntryEnd: { - type: 'number', - default: 100, - }, - animationExitStart: { - type: 'number', - default: 0, - }, - animationExitEnd: { - type: 'number', - default: 100, - }, - parallaxEnabled: { - type: 'boolean', - default: false, - }, - parallaxStrength: { - type: 'number', - default: 50, - }, - }, - }; + if (!SUPPORTED_BLOCKS.includes(name)) { + return settings; + } + return { + ...settings, + attributes: { + ...settings.attributes, + animationType: { + type: 'string', + default: 'none', + }, + animationRange: { + type: 'string', + default: 'default', + }, + animationEntryStart: { + type: 'number', + default: 20, + }, + animationEntryEnd: { + type: 'number', + default: 100, + }, + animationExitStart: { + type: 'number', + default: 0, + }, + animationExitEnd: { + type: 'number', + default: 100, + }, + parallaxEnabled: { + type: 'boolean', + default: false, + }, + parallaxStrength: { + type: 'number', + default: 50, + }, + }, + }; }); // 2) Inject InspectorControls. const withAnimationControls = createHigherOrderComponent((BlockEdit) => { - return (props) => { - if (!SUPPORTED_BLOCKS.includes(props.name)) { - return ; - } - const { - attributes: { - animationType = 'none', - animationRange = 'default', - animationEntryStart = 20, - animationEntryEnd = 100, - animationExitStart = 0, - animationExitEnd = 100, - parallaxEnabled = false, - parallaxStrength = 50, - }, - setAttributes, - } = props; - - const isInOutAnimation = animationType.includes('in-out'); - - return ( - <> - - - setAttributes({ animationType: value })} - help={ - animationType.includes('in-out') - ? __( - '🔄 This animation plays on both entry and exit', - 'my-scroll-block' - ) - : '' - } - /> - - {animationType !== 'none' && ( - <> - { - const updates = { animationRange: value }; - // Set preset values - if (value === 'quick') { - updates.animationEntryStart = 0; - updates.animationEntryEnd = 50; - } else if (value === 'slow') { - updates.animationEntryStart = 10; - updates.animationEntryEnd = 100; - } else if (value === 'late') { - updates.animationEntryStart = 50; - updates.animationEntryEnd = 100; - } else if (value === 'default') { - updates.animationEntryStart = 20; - updates.animationEntryEnd = 100; - } - setAttributes(updates); - }} - help={__( - 'When should the animation start and finish', - 'my-scroll-block' - )} - /> - - {animationRange === 'custom' && ( - <> - - setAttributes({ animationEntryStart: value }) - } - min={0} - max={100} - step={5} - help={__( - 'When to start the entry animation', - 'my-scroll-block' - )} - /> - - setAttributes({ animationEntryEnd: value }) - } - min={0} - max={100} - step={5} - help={__( - 'When to complete the entry animation', - 'my-scroll-block' - )} - /> - - {isInOutAnimation && ( - <> - - setAttributes({ animationExitStart: value }) - } - min={0} - max={100} - step={5} - help={__( - 'When to start the exit animation', - 'my-scroll-block' - )} - /> - - setAttributes({ animationExitEnd: value }) - } - min={0} - max={100} - step={5} - help={__( - 'When to complete the exit animation', - 'my-scroll-block' - )} - /> - - )} - - )} - - )} - - setAttributes({ parallaxEnabled: value })} - help={__( - 'Adds a parallax scrolling effect to the block background or content.', - 'my-scroll-block' - )} - /> - - {parallaxEnabled && ( - setAttributes({ parallaxStrength: value })} - min={10} - max={200} - step={10} - help={__('Higher values create more movement.', 'my-scroll-block')} - /> - )} - - - - - ); - }; + return (props) => { + if (!SUPPORTED_BLOCKS.includes(props.name)) { + return ; + } + const { + attributes: { + animationType = 'none', + animationRange = 'default', + animationEntryStart = 20, + animationEntryEnd = 100, + animationExitStart = 0, + animationExitEnd = 100, + parallaxEnabled = false, + parallaxStrength = 50, + }, + setAttributes, + } = props; + + const isInOutAnimation = animationType.includes('in-out'); + + return ( + <> + + + setAttributes({ animationType: value })} + help={ + animationType.includes('in-out') + ? __('🔄 This animation plays on both entry and exit', 'my-scroll-block') + : '' + } + /> + + {animationType !== 'none' && ( + <> + { + const updates = { animationRange: value }; + // Set preset values + if (value === 'quick') { + updates.animationEntryStart = 0; + updates.animationEntryEnd = 50; + } else if (value === 'slow') { + updates.animationEntryStart = 10; + updates.animationEntryEnd = 100; + } else if (value === 'late') { + updates.animationEntryStart = 50; + updates.animationEntryEnd = 100; + } else if (value === 'default') { + updates.animationEntryStart = 20; + updates.animationEntryEnd = 100; + } + setAttributes(updates); + }} + help={__('When should the animation start and finish', 'my-scroll-block')} + /> + + {animationRange === 'custom' && ( + <> + setAttributes({ animationEntryStart: value })} + min={0} + max={100} + step={5} + help={__('When to start the entry animation', 'my-scroll-block')} + /> + setAttributes({ animationEntryEnd: value })} + min={0} + max={100} + step={5} + help={__('When to complete the entry animation', 'my-scroll-block')} + /> + + {isInOutAnimation && ( + <> + setAttributes({ animationExitStart: value })} + min={0} + max={100} + step={5} + help={__('When to start the exit animation', 'my-scroll-block')} + /> + setAttributes({ animationExitEnd: value })} + min={0} + max={100} + step={5} + help={__('When to complete the exit animation', 'my-scroll-block')} + /> + + )} + + )} + + )} + + setAttributes({ parallaxEnabled: value })} + help={__( + 'Adds a parallax scrolling effect to the block background or content.', + 'my-scroll-block' + )} + /> + + {parallaxEnabled && ( + setAttributes({ parallaxStrength: value })} + min={10} + max={200} + step={10} + help={__('Higher values create more movement.', 'my-scroll-block')} + /> + )} + + + + + ); + }; }, 'withAnimationControls'); addFilter('editor.BlockEdit', 'my-scroll-block/with-controls', withAnimationControls); // 3) Add classes/styles to the saved content markup. addFilter( - 'blocks.getSaveContent.extraProps', - 'my-scroll-block/save-props', - (extraProps, blockType, attributes) => { - if (!SUPPORTED_BLOCKS.includes(blockType.name)) { - return extraProps; - } - const { - animationType = 'none', - animationRange = 'default', - animationEntryStart = 20, - animationEntryEnd = 100, - animationExitStart = 0, - animationExitEnd = 100, - parallaxEnabled = false, - parallaxStrength = 50, - } = attributes; - - if (animationType === 'none' && !parallaxEnabled) { - return extraProps; - } - - // Class & data attributes - extraProps.className = [ - extraProps.className, - 'scroll-anim-block', - `scroll-anim-${String(animationType).replace(/\s+/g, '-').toLowerCase()}`, - ] - .filter(Boolean) - .join(' '); - - extraProps['data-scroll-anim'] = '1'; - extraProps['data-anim-range'] = animationRange; - - // Add custom range values as data attributes if using custom range - if (animationRange === 'custom') { - extraProps['data-entry-start'] = animationEntryStart; - extraProps['data-entry-end'] = animationEntryEnd; - if (animationType.includes('in-out')) { - extraProps['data-exit-start'] = animationExitStart; - extraProps['data-exit-end'] = animationExitEnd; - } - } - - if (parallaxEnabled) { - extraProps['data-parallax'] = '1'; - extraProps['data-parallax-strength'] = parallaxStrength; - extraProps.style = { - ...extraProps.style, - '--parallax-strength': `${parallaxStrength}px`, - }; - } - - return extraProps; - } + 'blocks.getSaveContent.extraProps', + 'my-scroll-block/save-props', + (extraProps, blockType, attributes) => { + if (!SUPPORTED_BLOCKS.includes(blockType.name)) { + return extraProps; + } + const { + animationType = 'none', + animationRange = 'default', + animationEntryStart = 20, + animationEntryEnd = 100, + animationExitStart = 0, + animationExitEnd = 100, + parallaxEnabled = false, + parallaxStrength = 50, + } = attributes; + + if (animationType === 'none' && !parallaxEnabled) { + return extraProps; + } + + // Class & data attributes + extraProps.className = [ + extraProps.className, + 'scroll-anim-block', + `scroll-anim-${String(animationType).replace(/\s+/g, '-').toLowerCase()}`, + ] + .filter(Boolean) + .join(' '); + + extraProps['data-scroll-anim'] = '1'; + extraProps['data-anim-range'] = animationRange; + + // Add custom range values as data attributes if using custom range + if (animationRange === 'custom') { + extraProps['data-entry-start'] = animationEntryStart; + extraProps['data-entry-end'] = animationEntryEnd; + if (animationType.includes('in-out')) { + extraProps['data-exit-start'] = animationExitStart; + extraProps['data-exit-end'] = animationExitEnd; + } + } + + if (parallaxEnabled) { + extraProps['data-parallax'] = '1'; + extraProps['data-parallax-strength'] = parallaxStrength; + extraProps.style = { + ...extraProps.style, + '--parallax-strength': `${parallaxStrength}px`, + }; + } + + return extraProps; + } ); // 4) Also reflect classes/attributes in the editor canvas for live preview. addFilter( - 'editor.BlockListBlock', - 'my-scroll-block/list-props', - createHigherOrderComponent((BlockListBlock) => { - return (props) => { - if (!SUPPORTED_BLOCKS.includes(props.name)) { - return ; - } - const { - animationType = 'none', - animationRange = 'default', - animationEntryStart = 20, - animationEntryEnd = 100, - animationExitStart = 0, - animationExitEnd = 100, - parallaxEnabled = false, - parallaxStrength = 50, - } = props.attributes; - - const extraProps = {}; - - if (animationType !== 'none') { - extraProps.className = [ - props.className, - 'scroll-anim-block', - `scroll-anim-${String(animationType).replace(/\s+/g, '-').toLowerCase()}`, - ] - .filter(Boolean) - .join(' '); - extraProps['data-scroll-anim'] = '1'; - extraProps['data-anim-range'] = animationRange; - - if (animationRange === 'custom') { - extraProps['data-entry-start'] = animationEntryStart; - extraProps['data-entry-end'] = animationEntryEnd; - if (animationType.includes('in-out')) { - extraProps['data-exit-start'] = animationExitStart; - extraProps['data-exit-end'] = animationExitEnd; - } - } - } - - if (parallaxEnabled) { - extraProps['data-parallax'] = '1'; - extraProps['data-parallax-strength'] = parallaxStrength; - extraProps.style = { - ...props.style, - '--parallax-strength': `${parallaxStrength}px`, - }; - } - return ; - }; - }, 'withListExtraProps') + 'editor.BlockListBlock', + 'my-scroll-block/list-props', + createHigherOrderComponent((BlockListBlock) => { + return (props) => { + if (!SUPPORTED_BLOCKS.includes(props.name)) { + return ; + } + const { + animationType = 'none', + animationRange = 'default', + animationEntryStart = 20, + animationEntryEnd = 100, + animationExitStart = 0, + animationExitEnd = 100, + parallaxEnabled = false, + parallaxStrength = 50, + } = props.attributes; + + const extraProps = {}; + + if (animationType !== 'none') { + extraProps.className = [ + props.className, + 'scroll-anim-block', + `scroll-anim-${String(animationType).replace(/\s+/g, '-').toLowerCase()}`, + ] + .filter(Boolean) + .join(' '); + extraProps['data-scroll-anim'] = '1'; + extraProps['data-anim-range'] = animationRange; + + if (animationRange === 'custom') { + extraProps['data-entry-start'] = animationEntryStart; + extraProps['data-entry-end'] = animationEntryEnd; + if (animationType.includes('in-out')) { + extraProps['data-exit-start'] = animationExitStart; + extraProps['data-exit-end'] = animationExitEnd; + } + } + } + + if (parallaxEnabled) { + extraProps['data-parallax'] = '1'; + extraProps['data-parallax-strength'] = parallaxStrength; + extraProps.style = { + ...props.style, + '--parallax-strength': `${parallaxStrength}px`, + }; + } + return ; + }; + }, 'withListExtraProps') ); function openBlockInspector(clientId) { - try { - // Ensure the block is selected - dispatch('core/block-editor').selectBlock(clientId); - } catch (e) {} - try { - // Post editor (classic block editor screen) - dispatch('core/edit-post').openGeneralSidebar('edit-post/block'); - } catch (e) {} - try { - // Site editor (FSE) - dispatch('core/edit-site').openGeneralSidebar('edit-site/block-inspector'); - } catch (e) {} + try { + // Ensure the block is selected + dispatch('core/block-editor').selectBlock(clientId); + } catch (e) {} + try { + // Post editor (classic block editor screen) + dispatch('core/edit-post').openGeneralSidebar('edit-post/block'); + } catch (e) {} + try { + // Site editor (FSE) + dispatch('core/edit-site').openGeneralSidebar('edit-site/block-inspector'); + } catch (e) {} } // 5) Add animation indicator icon to blocks with animations and make it clickable. addFilter( - 'editor.BlockListBlock', - 'my-scroll-block/animation-indicator', - createHigherOrderComponent((BlockListBlock) => { - return (props) => { - if (!SUPPORTED_BLOCKS.includes(props.name)) { - return ; - } - const { animationType = 'none', parallaxEnabled = false } = props.attributes; - - if (animationType === 'none' && !parallaxEnabled) { - return ; - } - - const handleActivate = (event) => { - event.preventDefault(); - event.stopPropagation(); - openBlockInspector(props.clientId); - }; - - const handleKeyDown = (event) => { - if (event.key === 'Enter' || event.key === ' ') { - handleActivate(event); - } - }; - - return ( -
- -
- -
-
- ); - }; - }, 'withAnimationIndicator') + 'editor.BlockListBlock', + 'my-scroll-block/animation-indicator', + createHigherOrderComponent((BlockListBlock) => { + return (props) => { + if (!SUPPORTED_BLOCKS.includes(props.name)) { + return ; + } + const { animationType = 'none', parallaxEnabled = false } = props.attributes; + + if (animationType === 'none' && !parallaxEnabled) { + return ; + } + + const handleActivate = (event) => { + event.preventDefault(); + event.stopPropagation(); + openBlockInspector(props.clientId); + }; + + const handleKeyDown = (event) => { + if (event.key === 'Enter' || event.key === ' ') { + handleActivate(event); + } + }; + + return ( +
+ +
+ +
+
+ ); + }; + }, 'withAnimationIndicator') ); // No standalone block registration; this plugin extends core blocks only. diff --git a/src/progress-block/editor.css b/src/progress-block/editor.css index f2fce00..cf1e224 100644 --- a/src/progress-block/editor.css +++ b/src/progress-block/editor.css @@ -3,46 +3,46 @@ */ .reading-progress-preview { - padding: 20px; - background: #f9f9f9; - border: 2px dashed #ddd; - border-radius: 8px; + padding: 20px; + background: #f9f9f9; + border: 2px dashed #ddd; + border-radius: 8px; } .reading-progress-preview .reading-progress-track { - margin-bottom: 16px; + margin-bottom: 16px; } .reading-progress-info { - font-size: 14px; - line-height: 1.6; + font-size: 14px; + line-height: 1.6; } .reading-progress-info p { - margin: 8px 0; + margin: 8px 0; } .reading-progress-info strong { - color: #1e1e1e; - font-size: 16px; + color: #1e1e1e; + font-size: 16px; } .reading-progress-info em { - color: #666; - font-style: normal; - background: #e0e0e0; - padding: 2px 8px; - border-radius: 4px; - font-size: 13px; + color: #666; + font-style: normal; + background: #e0e0e0; + padding: 2px 8px; + border-radius: 4px; + font-size: 13px; } /* Block icon in inserter */ .wp-block-my-scroll-block-reading-progress { - min-height: 100px; + min-height: 100px; } /* Color picker labels */ .components-base-control__label { - font-weight: 500; - margin-bottom: 8px; + font-weight: 500; + margin-bottom: 8px; } diff --git a/src/progress-block/index.js b/src/progress-block/index.js index 138a4fa..1cf7899 100644 --- a/src/progress-block/index.js +++ b/src/progress-block/index.js @@ -3,164 +3,159 @@ import { __ } from '@wordpress/i18n'; import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; import { useInstanceId } from '@wordpress/compose'; import { - PanelBody, - ColorPicker, - RangeControl, - SelectControl, - ToggleControl, - BaseControl, + PanelBody, + ColorPicker, + RangeControl, + SelectControl, + ToggleControl, + BaseControl, } from '@wordpress/components'; import './editor.css'; import './style.css'; const Edit = ({ attributes, setAttributes }) => { - const { barColor, barHeight, position, backgroundColor, showPercentage } = attributes; - const blockProps = useBlockProps(); - const instanceId = useInstanceId(Edit); + const { barColor, barHeight, position, backgroundColor, showPercentage } = attributes; + const blockProps = useBlockProps(); + const instanceId = useInstanceId(Edit); - return ( - <> - - - setAttributes({ position: value })} - help={__('Where to display the progress bar', 'my-scroll-block')} - /> + return ( + <> + + + setAttributes({ position: value })} + help={__('Where to display the progress bar', 'my-scroll-block')} + /> - setAttributes({ barHeight: value })} - min={2} - max={20} - step={1} - /> + setAttributes({ barHeight: value })} + min={2} + max={20} + step={1} + /> - - setAttributes({ barColor: value })} - enableAlpha - /> - + + setAttributes({ barColor: value })} + enableAlpha + /> + - - setAttributes({ backgroundColor: value })} - enableAlpha - /> - + + setAttributes({ backgroundColor: value })} + enableAlpha + /> + - setAttributes({ showPercentage: value })} - help={__('Display scroll percentage number', 'my-scroll-block')} - /> - - + setAttributes({ showPercentage: value })} + help={__('Display scroll percentage number', 'my-scroll-block')} + /> + + -
-
-
-
-
-
-

- 📊 {__('Reading Progress Bar', 'my-scroll-block')} -

-

- {__('Position:', 'my-scroll-block')}{' '} - - {position === 'top' - ? __('Top', 'my-scroll-block') - : __('Bottom', 'my-scroll-block')} - -

-

- {__('This bar will be fixed at the', 'my-scroll-block')}{' '} - {position === 'top' - ? __('top', 'my-scroll-block') - : __('bottom', 'my-scroll-block')}{' '} - {__( - 'of the page and track scroll progress using CSS scroll timeline.', - 'my-scroll-block' - )} -

- {showPercentage && ( -

- ✓ {__('Percentage display enabled', 'my-scroll-block')} -

- )} -
-
-
- - ); +
+
+
+
+
+
+

+ 📊 {__('Reading Progress Bar', 'my-scroll-block')} +

+

+ {__('Position:', 'my-scroll-block')}{' '} + + {position === 'top' + ? __('Top', 'my-scroll-block') + : __('Bottom', 'my-scroll-block')} + +

+

+ {__('This bar will be fixed at the', 'my-scroll-block')}{' '} + {position === 'top' ? __('top', 'my-scroll-block') : __('bottom', 'my-scroll-block')}{' '} + {__( + 'of the page and track scroll progress using CSS scroll timeline.', + 'my-scroll-block' + )} +

+ {showPercentage && ( +

+ ✓ {__('Percentage display enabled', 'my-scroll-block')} +

+ )} +
+
+
+ + ); }; const Save = ({ attributes }) => { - const { barColor, barHeight, position, backgroundColor, showPercentage } = attributes; - const blockProps = useBlockProps.save({ - className: `reading-progress-container position-${position}`, - style: { - '--progress-bar-color': barColor, - '--progress-bar-height': `${barHeight}px`, - '--progress-bg-color': backgroundColor, - }, - }); + const { barColor, barHeight, position, backgroundColor, showPercentage } = attributes; + const blockProps = useBlockProps.save({ + className: `reading-progress-container position-${position}`, + style: { + '--progress-bar-color': barColor, + '--progress-bar-height': `${barHeight}px`, + '--progress-bg-color': backgroundColor, + }, + }); - return ( -
-
-
-
- {showPercentage && ( -
- 0% -
- )} -
- ); + return ( +
+
+
+
+ {showPercentage && ( +
+ 0% +
+ )} +
+ ); }; registerBlockType('my-scroll-block/reading-progress', { - edit: Edit, - save: Save, + edit: Edit, + save: Save, }); diff --git a/src/progress-block/style.css b/src/progress-block/style.css index c582e0f..f3eca03 100644 --- a/src/progress-block/style.css +++ b/src/progress-block/style.css @@ -4,164 +4,156 @@ */ .reading-progress-container { - position: fixed; - left: 0; - right: 0; - width: 100%; - z-index: 999999; - pointer-events: none; - animation: fadeIn 0.3s ease-out; + position: fixed; + left: 0; + right: 0; + width: 100%; + z-index: 999999; + pointer-events: none; + animation: fadeIn 0.3s ease-out; } .reading-progress-container.position-top { - top: 0; + top: 0; } .reading-progress-container.position-bottom { - bottom: 0; + bottom: 0; } .reading-progress-track { - height: var(--progress-bar-height, 4px); - background-color: var(--progress-bg-color, #e0e0e0); - position: relative; - overflow: hidden; + height: var(--progress-bar-height, 4px); + background-color: var(--progress-bg-color, #e0e0e0); + position: relative; + overflow: hidden; } .reading-progress-bar { - height: 100%; - background-color: var(--progress-bar-color, #3858e9); - transform-origin: 0 50%; - transform: scaleX(0); + height: 100%; + background-color: var(--progress-bar-color, #3858e9); + transform-origin: 0 50%; + transform: scaleX(0); } /* Scroll Progress Timeline - The Magic! */ @supports (animation-timeline: scroll()) { - - .reading-progress-bar { - animation: progress-bar linear; - animation-timeline: scroll(root block); - animation-range: 0% 100%; - } + .reading-progress-bar { + animation: progress-bar linear; + animation-timeline: scroll(root block); + animation-range: 0% 100%; + } } @keyframes progress-bar { + from { + transform: scaleX(0); + } - from { - transform: scaleX(0); - } - - to { - transform: scaleX(1); - } + to { + transform: scaleX(1); + } } /* Percentage Display */ .reading-progress-percentage { - position: absolute; - right: 16px; - top: 50%; - transform: translateY(-50%); - pointer-events: auto; - background: var(--progress-bar-color, #3858e9); - color: #fff; - padding: 4px 12px; - border-radius: 12px; - font-size: 12px; - font-weight: 600; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - opacity: 0; - transition: opacity 0.3s ease; + position: absolute; + right: 16px; + top: 50%; + transform: translateY(-50%); + pointer-events: auto; + background: var(--progress-bar-color, #3858e9); + color: #fff; + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + opacity: 0; + transition: opacity 0.3s ease; } .reading-progress-container:hover .reading-progress-percentage { - opacity: 1; + opacity: 1; } /* Animate percentage value using scroll timeline */ @supports (animation-timeline: scroll()) { - - .percentage-value::before { - content: "0"; - animation: percentage-counter linear; - animation-timeline: scroll(root block); - animation-range: 0% 100%; - counter-reset: percentage 0; - } - - @keyframes percentage-counter { - - to { - counter-increment: percentage 100; - content: counter(percentage); - } - } + .percentage-value::before { + content: '0'; + animation: percentage-counter linear; + animation-timeline: scroll(root block); + animation-range: 0% 100%; + counter-reset: percentage 0; + } + + @keyframes percentage-counter { + to { + counter-increment: percentage 100; + content: counter(percentage); + } + } } /* Fallback for browsers without scroll timeline support */ @supports not (animation-timeline: scroll()) { - - .reading-progress-bar { - transform: scaleX(0); - } - - .reading-progress-percentage { - display: none; - } - - body::after { - content: "⚠️ This browser does not support CSS Scroll Timelines. Please use Chrome 115+, Edge 115+, or Opera 101+ for the full experience."; - position: fixed; - bottom: 20px; - left: 50%; - transform: translateX(-50%); - background: #ff9800; - color: #fff; - padding: 12px 20px; - border-radius: 8px; - font-size: 14px; - z-index: 999999; - max-width: 90%; - text-align: center; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); - } + .reading-progress-bar { + transform: scaleX(0); + } + + .reading-progress-percentage { + display: none; + } + + body::after { + content: '⚠️ This browser does not support CSS Scroll Timelines. Please use Chrome 115+, Edge 115+, or Opera 101+ for the full experience.'; + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background: #ff9800; + color: #fff; + padding: 12px 20px; + border-radius: 8px; + font-size: 14px; + z-index: 999999; + max-width: 90%; + text-align: center; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + } } @keyframes fadeIn { + from { + opacity: 0; + } - from { - opacity: 0; - } - - to { - opacity: 1; - } + to { + opacity: 1; + } } /* Respect reduced motion preferences */ @media (prefers-reduced-motion: reduce) { - - .reading-progress-bar { - animation: none !important; - transition: none !important; - } - - .reading-progress-container { - animation: none !important; - } - - .reading-progress-percentage { - transition: none !important; - } + .reading-progress-bar { + animation: none !important; + transition: none !important; + } + + .reading-progress-container { + animation: none !important; + } + + .reading-progress-percentage { + transition: none !important; + } } /* Mobile optimizations */ @media (max-width: 768px) { - - .reading-progress-percentage { - right: 8px; - padding: 3px 8px; - font-size: 11px; - } + .reading-progress-percentage { + right: 8px; + padding: 3px 8px; + font-size: 11px; + } } diff --git a/src/style.css b/src/style.css index 832d1d2..523f117 100644 --- a/src/style.css +++ b/src/style.css @@ -7,466 +7,447 @@ /* Base defaults */ @property --anim-displacement-horizontal { - syntax: ""; - inherits: true; - initial-value: 10vw; + syntax: ''; + inherits: true; + initial-value: 10vw; } @property --anim-displacement-vertical { - syntax: ""; - inherits: true; - initial-value: 5vh; + syntax: ''; + inherits: true; + initial-value: 5vh; } @property --parallax-strength { - syntax: ""; - inherits: true; - initial-value: 50px; + syntax: ''; + inherits: true; + initial-value: 50px; } @supports (animation-timeline: view()) { - - /* Entry-only animations */ - .scroll-anim-fade-in, - .scroll-anim-slide-in-left, - .scroll-anim-slide-in-right, - .scroll-anim-slide-in-up, - .scroll-anim-slide-in-down, - .scroll-anim-scale-up, - .scroll-anim-rotate-in, - .scroll-anim-blur-in, - .scroll-anim-rotate-3d-in, - .scroll-anim-circle-reveal, - .scroll-anim-curtain-reveal { - animation-timeline: view(); - animation-range: entry 20% cover 100%; - } - - /* Quick timing preset */ - [data-anim-range="quick"].scroll-anim-fade-in, - [data-anim-range="quick"].scroll-anim-slide-in-left, - [data-anim-range="quick"].scroll-anim-slide-in-right, - [data-anim-range="quick"].scroll-anim-slide-in-up, - [data-anim-range="quick"].scroll-anim-slide-in-down, - [data-anim-range="quick"].scroll-anim-scale-up, - [data-anim-range="quick"].scroll-anim-rotate-in, - [data-anim-range="quick"].scroll-anim-blur-in, - [data-anim-range="quick"].scroll-anim-rotate-3d-in, - [data-anim-range="quick"].scroll-anim-circle-reveal, - [data-anim-range="quick"].scroll-anim-curtain-reveal { - animation-range: entry 0% cover 50%; - } - - /* Slow timing preset */ - [data-anim-range="slow"].scroll-anim-fade-in, - [data-anim-range="slow"].scroll-anim-slide-in-left, - [data-anim-range="slow"].scroll-anim-slide-in-right, - [data-anim-range="slow"].scroll-anim-slide-in-up, - [data-anim-range="slow"].scroll-anim-slide-in-down, - [data-anim-range="slow"].scroll-anim-scale-up, - [data-anim-range="slow"].scroll-anim-rotate-in, - [data-anim-range="slow"].scroll-anim-blur-in, - [data-anim-range="slow"].scroll-anim-rotate-3d-in, - [data-anim-range="slow"].scroll-anim-circle-reveal, - [data-anim-range="slow"].scroll-anim-curtain-reveal { - animation-range: entry 10% cover 100%; - } - - /* Late start timing preset */ - [data-anim-range="late"].scroll-anim-fade-in, - [data-anim-range="late"].scroll-anim-slide-in-left, - [data-anim-range="late"].scroll-anim-slide-in-right, - [data-anim-range="late"].scroll-anim-slide-in-up, - [data-anim-range="late"].scroll-anim-slide-in-down, - [data-anim-range="late"].scroll-anim-scale-up, - [data-anim-range="late"].scroll-anim-rotate-in, - [data-anim-range="late"].scroll-anim-blur-in, - [data-anim-range="late"].scroll-anim-rotate-3d-in, - [data-anim-range="late"].scroll-anim-circle-reveal, - [data-anim-range="late"].scroll-anim-curtain-reveal { - animation-range: entry 50% cover 100%; - } - - /* In-and-out animations */ - .scroll-anim-fade-in-out, - .scroll-anim-slide-up-in-out, - .scroll-anim-scale-in-out, - .scroll-anim-rotate-in-out, - .scroll-anim-rotate-3d-in-out { - animation-timeline: view(); - } - - /* Parallax Effect */ - [data-parallax="1"] { - animation-name: scrollParallax; - animation-timeline: scroll(root block); - animation-range: 0% 100%; - } + /* Entry-only animations */ + .scroll-anim-fade-in, + .scroll-anim-slide-in-left, + .scroll-anim-slide-in-right, + .scroll-anim-slide-in-up, + .scroll-anim-slide-in-down, + .scroll-anim-scale-up, + .scroll-anim-rotate-in, + .scroll-anim-blur-in, + .scroll-anim-rotate-3d-in, + .scroll-anim-circle-reveal, + .scroll-anim-curtain-reveal { + animation-timeline: view(); + animation-range: entry 20% cover 100%; + } + + /* Quick timing preset */ + [data-anim-range='quick'].scroll-anim-fade-in, + [data-anim-range='quick'].scroll-anim-slide-in-left, + [data-anim-range='quick'].scroll-anim-slide-in-right, + [data-anim-range='quick'].scroll-anim-slide-in-up, + [data-anim-range='quick'].scroll-anim-slide-in-down, + [data-anim-range='quick'].scroll-anim-scale-up, + [data-anim-range='quick'].scroll-anim-rotate-in, + [data-anim-range='quick'].scroll-anim-blur-in, + [data-anim-range='quick'].scroll-anim-rotate-3d-in, + [data-anim-range='quick'].scroll-anim-circle-reveal, + [data-anim-range='quick'].scroll-anim-curtain-reveal { + animation-range: entry 0% cover 50%; + } + + /* Slow timing preset */ + [data-anim-range='slow'].scroll-anim-fade-in, + [data-anim-range='slow'].scroll-anim-slide-in-left, + [data-anim-range='slow'].scroll-anim-slide-in-right, + [data-anim-range='slow'].scroll-anim-slide-in-up, + [data-anim-range='slow'].scroll-anim-slide-in-down, + [data-anim-range='slow'].scroll-anim-scale-up, + [data-anim-range='slow'].scroll-anim-rotate-in, + [data-anim-range='slow'].scroll-anim-blur-in, + [data-anim-range='slow'].scroll-anim-rotate-3d-in, + [data-anim-range='slow'].scroll-anim-circle-reveal, + [data-anim-range='slow'].scroll-anim-curtain-reveal { + animation-range: entry 10% cover 100%; + } + + /* Late start timing preset */ + [data-anim-range='late'].scroll-anim-fade-in, + [data-anim-range='late'].scroll-anim-slide-in-left, + [data-anim-range='late'].scroll-anim-slide-in-right, + [data-anim-range='late'].scroll-anim-slide-in-up, + [data-anim-range='late'].scroll-anim-slide-in-down, + [data-anim-range='late'].scroll-anim-scale-up, + [data-anim-range='late'].scroll-anim-rotate-in, + [data-anim-range='late'].scroll-anim-blur-in, + [data-anim-range='late'].scroll-anim-rotate-3d-in, + [data-anim-range='late'].scroll-anim-circle-reveal, + [data-anim-range='late'].scroll-anim-curtain-reveal { + animation-range: entry 50% cover 100%; + } + + /* In-and-out animations */ + .scroll-anim-fade-in-out, + .scroll-anim-slide-up-in-out, + .scroll-anim-scale-in-out, + .scroll-anim-rotate-in-out, + .scroll-anim-rotate-3d-in-out { + animation-timeline: view(); + } + + /* Parallax Effect */ + [data-parallax='1'] { + animation-name: scrollParallax; + animation-timeline: scroll(root block); + animation-range: 0% 100%; + } } /* Apply animations - Entry Only */ .scroll-anim-fade-in { - animation-name: scrollFadeIn; - opacity: 0; - transform: translateY(var(--anim-displacement-vertical)); + animation-name: scrollFadeIn; + opacity: 0; + transform: translateY(var(--anim-displacement-vertical)); } .scroll-anim-slide-in-left { - animation-name: scrollSlideInLeft; - opacity: 0; - transform: translateX(calc(-1 * var(--anim-displacement-horizontal))); + animation-name: scrollSlideInLeft; + opacity: 0; + transform: translateX(calc(-1 * var(--anim-displacement-horizontal))); } .scroll-anim-slide-in-right { - animation-name: scrollSlideInRight; - opacity: 0; - transform: translateX(var(--anim-displacement-horizontal)); + animation-name: scrollSlideInRight; + opacity: 0; + transform: translateX(var(--anim-displacement-horizontal)); } .scroll-anim-slide-in-up { - animation-name: scrollSlideInUp; - opacity: 0; - transform: translateY(var(--anim-displacement-vertical)); + animation-name: scrollSlideInUp; + opacity: 0; + transform: translateY(var(--anim-displacement-vertical)); } .scroll-anim-slide-in-down { - animation-name: scrollSlideInDown; - opacity: 0; - transform: translateY(calc(-1 * var(--anim-displacement-vertical))); + animation-name: scrollSlideInDown; + opacity: 0; + transform: translateY(calc(-1 * var(--anim-displacement-vertical))); } .scroll-anim-scale-up { - animation-name: scrollScaleUp; - opacity: 0; - transform: scale(0.3); + animation-name: scrollScaleUp; + opacity: 0; + transform: scale(0.3); } .scroll-anim-rotate-in { - animation-name: scrollRotateIn; - opacity: 0; - transform: rotate(25deg); + animation-name: scrollRotateIn; + opacity: 0; + transform: rotate(25deg); } .scroll-anim-blur-in { - animation-name: scrollBlurIn; - opacity: 0; - filter: blur(10px); + animation-name: scrollBlurIn; + opacity: 0; + filter: blur(10px); } .scroll-anim-rotate-3d-in { - animation-name: scrollRotate3DIn; - opacity: 0; - transform: perspective(1000px) rotateX(45deg); + animation-name: scrollRotate3DIn; + opacity: 0; + transform: perspective(1000px) rotateX(45deg); } .scroll-anim-circle-reveal { - animation-name: scrollCircleReveal; - clip-path: circle(0% at 50% 50%); + animation-name: scrollCircleReveal; + clip-path: circle(0% at 50% 50%); } .scroll-anim-curtain-reveal { - animation-name: scrollCurtainReveal; - clip-path: inset(0 50% 0 50%); + animation-name: scrollCurtainReveal; + clip-path: inset(0 50% 0 50%); } /* Apply animations - In and Out */ .scroll-anim-fade-in-out { - animation-name: scrollFadeInOut; + animation-name: scrollFadeInOut; } .scroll-anim-slide-up-in-out { - animation-name: scrollSlideUpInOut; + animation-name: scrollSlideUpInOut; } .scroll-anim-scale-in-out { - animation-name: scrollScaleInOut; + animation-name: scrollScaleInOut; } .scroll-anim-rotate-in-out { - animation-name: scrollRotateInOut; + animation-name: scrollRotateInOut; } .scroll-anim-rotate-3d-in-out { - animation-name: scrollRotate3DInOut; + animation-name: scrollRotate3DInOut; } /* CSS animations using keyframes - Entry Only */ @keyframes scrollFadeIn { + from { + opacity: 0; + transform: translateY(var(--anim-displacement-vertical)); + } - from { - opacity: 0; - transform: translateY(var(--anim-displacement-vertical)); - } - - to { - opacity: 1; - transform: translateY(0); - } + to { + opacity: 1; + transform: translateY(0); + } } @keyframes scrollSlideInLeft { + from { + opacity: 0; + transform: translateX(calc(-1 * var(--anim-displacement-horizontal))); + } - from { - opacity: 0; - transform: translateX(calc(-1 * var(--anim-displacement-horizontal))); - } - - to { - opacity: 1; - transform: translateX(0); - } + to { + opacity: 1; + transform: translateX(0); + } } @keyframes scrollSlideInRight { + from { + opacity: 0; + transform: translateX(var(--anim-displacement-horizontal)); + } - from { - opacity: 0; - transform: translateX(var(--anim-displacement-horizontal)); - } - - to { - opacity: 1; - transform: translateX(0); - } + to { + opacity: 1; + transform: translateX(0); + } } @keyframes scrollSlideInUp { + from { + opacity: 0; + transform: translateY(var(--anim-displacement-vertical)); + } - from { - opacity: 0; - transform: translateY(var(--anim-displacement-vertical)); - } - - to { - opacity: 1; - transform: translateY(0); - } + to { + opacity: 1; + transform: translateY(0); + } } @keyframes scrollSlideInDown { + from { + opacity: 0; + transform: translateY(calc(-1 * var(--anim-displacement-vertical))); + } - from { - opacity: 0; - transform: translateY(calc(-1 * var(--anim-displacement-vertical))); - } - - to { - opacity: 1; - transform: translateY(0); - } + to { + opacity: 1; + transform: translateY(0); + } } @keyframes scrollScaleUp { + from { + opacity: 0; + transform: scale(0.3); + } - from { - opacity: 0; - transform: scale(0.3); - } - - to { - opacity: 1; - transform: scale(1); - } + to { + opacity: 1; + transform: scale(1); + } } @keyframes scrollRotateIn { + from { + opacity: 0; + transform: rotate(25deg); + } - from { - opacity: 0; - transform: rotate(25deg); - } - - to { - opacity: 1; - transform: rotate(0); - } + to { + opacity: 1; + transform: rotate(0); + } } @keyframes scrollBlurIn { + from { + opacity: 0; + filter: blur(10px); + } - from { - opacity: 0; - filter: blur(10px); - } - - to { - opacity: 1; - filter: blur(0); - } + to { + opacity: 1; + filter: blur(0); + } } @keyframes scrollRotate3DIn { + from { + opacity: 0; + transform: perspective(1000px) rotateX(45deg); + } - from { - opacity: 0; - transform: perspective(1000px) rotateX(45deg); - } - - to { - opacity: 1; - transform: perspective(1000px) rotateX(0); - } + to { + opacity: 1; + transform: perspective(1000px) rotateX(0); + } } @keyframes scrollCircleReveal { + from { + clip-path: circle(0% at 50% 50%); + } - from { - clip-path: circle(0% at 50% 50%); - } - - to { - clip-path: circle(100% at 50% 50%); - } + to { + clip-path: circle(100% at 50% 50%); + } } @keyframes scrollCurtainReveal { + from { + clip-path: inset(0 50% 0 50%); + } - from { - clip-path: inset(0 50% 0 50%); - } - - to { - clip-path: inset(0 0 0 0); - } + to { + clip-path: inset(0 0 0 0); + } } /* CSS animations using keyframes - In and Out */ @keyframes scrollFadeInOut { + entry 0% { + opacity: 0; + transform: translateY(var(--anim-displacement-vertical)); + } - entry 0% { - opacity: 0; - transform: translateY(var(--anim-displacement-vertical)); - } - - entry 100% { - opacity: 1; - transform: translateY(0); - } + entry 100% { + opacity: 1; + transform: translateY(0); + } - exit 0% { - opacity: 1; - transform: translateY(0); - } + exit 0% { + opacity: 1; + transform: translateY(0); + } - exit 100% { - opacity: 0; - transform: translateY(calc(-1 * var(--anim-displacement-vertical))); - } + exit 100% { + opacity: 0; + transform: translateY(calc(-1 * var(--anim-displacement-vertical))); + } } @keyframes scrollSlideUpInOut { + entry 0% { + opacity: 0; + transform: translateY(var(--anim-displacement-vertical)); + } - entry 0% { - opacity: 0; - transform: translateY(var(--anim-displacement-vertical)); - } + entry 100% { + opacity: 1; + transform: translateY(0); + } - entry 100% { - opacity: 1; - transform: translateY(0); - } + exit 0% { + opacity: 1; + transform: translateY(0); + } - exit 0% { - opacity: 1; - transform: translateY(0); - } - - exit 100% { - opacity: 0; - transform: translateY(calc(-1 * var(--anim-displacement-vertical))); - } + exit 100% { + opacity: 0; + transform: translateY(calc(-1 * var(--anim-displacement-vertical))); + } } @keyframes scrollScaleInOut { + entry 0% { + opacity: 0; + transform: scale(0.3); + } - entry 0% { - opacity: 0; - transform: scale(0.3); - } - - entry 100% { - opacity: 1; - transform: scale(1); - } + entry 100% { + opacity: 1; + transform: scale(1); + } - exit 0% { - opacity: 1; - transform: scale(1); - } + exit 0% { + opacity: 1; + transform: scale(1); + } - exit 100% { - opacity: 0; - transform: scale(0.3); - } + exit 100% { + opacity: 0; + transform: scale(0.3); + } } @keyframes scrollRotateInOut { + entry 0% { + opacity: 0; + transform: rotate(-25deg); + } - entry 0% { - opacity: 0; - transform: rotate(-25deg); - } + entry 100% { + opacity: 1; + transform: rotate(0); + } - entry 100% { - opacity: 1; - transform: rotate(0); - } + exit 0% { + opacity: 1; + transform: rotate(0); + } - exit 0% { - opacity: 1; - transform: rotate(0); - } - - exit 100% { - opacity: 0; - transform: rotate(25deg); - } + exit 100% { + opacity: 0; + transform: rotate(25deg); + } } @keyframes scrollRotate3DInOut { + entry 0% { + opacity: 0; + transform: perspective(1000px) rotateX(-45deg); + } - entry 0% { - opacity: 0; - transform: perspective(1000px) rotateX(-45deg); - } - - entry 100% { - opacity: 1; - transform: perspective(1000px) rotateX(0); - } + entry 100% { + opacity: 1; + transform: perspective(1000px) rotateX(0); + } - exit 0% { - opacity: 1; - transform: perspective(1000px) rotateX(0); - } + exit 0% { + opacity: 1; + transform: perspective(1000px) rotateX(0); + } - exit 100% { - opacity: 0; - transform: perspective(1000px) rotateX(45deg); - } + exit 100% { + opacity: 0; + transform: perspective(1000px) rotateX(45deg); + } } @keyframes scrollParallax { + from { + transform: translateY(0); + } - from { - transform: translateY(0); - } - - to { - transform: translateY(var(--parallax-strength)); - } + to { + transform: translateY(var(--parallax-strength)); + } } /* Respect reduced motion */ @media (prefers-reduced-motion: reduce) { - - [data-scroll-anim], - [data-parallax] { - animation: none !important; - transition: none !important; - opacity: 1 !important; - transform: none !important; - filter: none !important; - clip-path: none !important; - } + [data-scroll-anim], + [data-parallax] { + animation: none !important; + transition: none !important; + opacity: 1 !important; + transform: none !important; + filter: none !important; + clip-path: none !important; + } } diff --git a/tests/global-setup.ts b/tests/global-setup.ts index d89a20a..1368eea 100644 --- a/tests/global-setup.ts +++ b/tests/global-setup.ts @@ -2,40 +2,40 @@ import { runCLI } from '@wp-playground/cli'; import * as path from 'path'; async function globalSetup(): Promise { - // eslint-disable-next-line no-console - console.log('Starting WordPress Playground server...'); + // eslint-disable-next-line no-console + console.log('Starting WordPress Playground server...'); - // Use process.cwd() and navigate to plugin directory - const pluginPath = path.join(process.cwd()); + // Use process.cwd() and navigate to plugin directory + const pluginPath = path.join(process.cwd()); - const cliServer = await runCLI({ - command: 'server', - php: '8.3', - wp: 'latest', - login: true, - port: 9400, - mount: [ - { - hostPath: pluginPath, - vfsPath: '/wordpress/wp-content/plugins/my-scroll-block', - }, - ], - blueprint: { - steps: [ - { - step: 'setSiteOptions', - options: { - blogname: 'WordPress Scroll-driven block', - blogdescription: 'Created by Fellyph', - }, - }, - { - step: 'activatePlugin', - pluginPath: '/wordpress/wp-content/plugins/my-scroll-block/my-scroll-block.php', - }, - { - step: 'runPHP', - code: ` 'Demo Scroll Animations Post', @@ -44,19 +44,19 @@ async function globalSetup(): Promise { 'post_status' => 'publish' )); ?>`, - }, - ], - }, - }); + }, + ], + }, + }); - // Store the server instance globally for teardown - (global as any).cliServer = cliServer; + // Store the server instance globally for teardown + (global as any).cliServer = cliServer; - // eslint-disable-next-line no-console - console.log('WordPress Playground server started on http://127.0.0.1:9400'); + // eslint-disable-next-line no-console + console.log('WordPress Playground server started on http://127.0.0.1:9400'); - // Wait a bit for the server to be fully ready - await new Promise((resolve) => setTimeout(resolve, 2000)); + // Wait a bit for the server to be fully ready + await new Promise((resolve) => setTimeout(resolve, 2000)); } export default globalSetup; diff --git a/tests/global-teardown.ts b/tests/global-teardown.ts index 51ac1fe..a3d4bf6 100644 --- a/tests/global-teardown.ts +++ b/tests/global-teardown.ts @@ -1,15 +1,15 @@ async function globalTeardown(): Promise { - // eslint-disable-next-line no-console - console.log('Stopping WordPress Playground server...'); + // eslint-disable-next-line no-console + console.log('Stopping WordPress Playground server...'); - const cliServer = (global as any).cliServer; + const cliServer = (global as any).cliServer; - if (cliServer && typeof cliServer.exit === 'function') { - await cliServer.exit(); - } + if (cliServer && typeof cliServer.exit === 'function') { + await cliServer.exit(); + } - // eslint-disable-next-line no-console - console.log('WordPress Playground server stopped.'); + // eslint-disable-next-line no-console + console.log('WordPress Playground server stopped.'); } export default globalTeardown; diff --git a/tests/reduced-motion.spec.ts b/tests/reduced-motion.spec.ts index 2f4b30c..04afe61 100644 --- a/tests/reduced-motion.spec.ts +++ b/tests/reduced-motion.spec.ts @@ -1,367 +1,399 @@ import { test, expect, type Page } from '@playwright/test'; +/** + * Helper function to dismiss the WordPress welcome modal and wait for the editor to be ready + */ +async function setupEditor(page: Page): Promise { + await page.waitForLoadState('domcontentloaded'); + + // Wait for the editor to initialize + await page.waitForTimeout(1000); + + // Dismiss the welcome modal if present - try multiple possible buttons + const welcomeModalDismissButtons = [ + page.getByRole('button', { name: 'Get started' }), + page.getByRole('button', { name: 'Close', exact: true }), + page.locator('.components-modal__screen-overlay button[aria-label="Close"]'), + page.locator('.components-modal__header button'), + ]; + + for (const button of welcomeModalDismissButtons) { + try { + if (await button.isVisible({ timeout: 2000 })) { + await button.click(); + await page.waitForTimeout(500); + break; + } + } catch { + // Button not found or not clickable, try next one + } + } + + // Wait for any modal to disappear + try { + await page.waitForSelector('.components-modal__screen-overlay', { + state: 'hidden', + timeout: 5000, + }); + } catch { + // Modal may not exist + } +} + +/** + * Helper to add a paragraph block and ensure it's selected + */ +async function addParagraphBlock(page: Page, text: string): Promise { + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + + // Wait for the editor canvas to be ready + await editorFrame.locator('body').waitFor({ state: 'visible', timeout: 30000 }); + + // Click on the add block button or empty paragraph + const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); + const emptyParagraph = editorFrame.locator('p.block-editor-default-block-appender__content'); + + try { + if (await addBlockButton.isVisible({ timeout: 5000 })) { + await addBlockButton.click(); + } else if (await emptyParagraph.isVisible({ timeout: 2000 })) { + await emptyParagraph.click(); + } + } catch { + // Try clicking on the editor body + await editorFrame.locator('.wp-block-post-content').click(); + } + + // Wait for the block to be ready and type + await page.waitForTimeout(500); + await page.keyboard.type(text); + await page.waitForTimeout(500); +} + test.describe('Reduced Motion Support', () => { - test('should disable animations when prefers-reduced-motion is set', async ({ - page, - }: { - page: Page; - }) => { - // Set the prefers-reduced-motion preference - await page.emulateMedia({ reducedMotion: 'reduce' }); - - // Create a post with scroll animation - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Create post with animation - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill('Reduced Motion Test', { timeout: 15000 }); - - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('This paragraph should not animate with reduced motion', { - timeout: 15000, - }); - - const animationTypeSelect = page.getByLabel('Animation Type'); - await animationTypeSelect.selectOption('Fade In'); - - // Publish the post - await page.getByRole('button', { name: 'Publish', exact: true }).click(); - const publishPanelButton = page - .getByLabel('Editor publish') - .getByRole('button', { name: 'Publish', exact: true }); - await publishPanelButton.click(); - - // Wait for post to be published - await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); - - // Get the post URL - const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); - const postUrl = await viewPostLink.getAttribute('href'); - - if (!postUrl) { - throw new Error('Could not get post URL'); - } - - // Visit the frontend post with reduced motion preference - await page.goto(postUrl); - await page.waitForLoadState('domcontentloaded'); - - // Verify the paragraph has the animation class but animations should be disabled via CSS - const animatedParagraph = page.locator('p.scroll-anim-fade-in[data-scroll-anim="1"]'); - await expect(animatedParagraph).toBeVisible(); - - // Check computed styles - with prefers-reduced-motion, the element should have: - // - opacity: 1 (not 0) - // - transform: none - // - animation: none - const computedStyles = await animatedParagraph.evaluate((el) => { - const styles = window.getComputedStyle(el); - return { - opacity: styles.opacity, - transform: styles.transform, - animation: styles.animation, - }; - }); - - // With reduced motion, the CSS should override the animation styles - expect(parseFloat(computedStyles.opacity)).toBe(1); - expect(computedStyles.transform).toBe('none'); - expect(computedStyles.animation).toContain('none'); - }); - - test('should disable parallax effect when prefers-reduced-motion is set', async ({ - page, - }: { - page: Page; - }) => { - // Set the prefers-reduced-motion preference - await page.emulateMedia({ reducedMotion: 'reduce' }); - - // Create a post with parallax - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Create post - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill('Parallax Reduced Motion Test', { timeout: 15000 }); - - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('This paragraph has parallax disabled with reduced motion', { - timeout: 15000, - }); - - // Enable parallax - const parallaxToggle = page.getByLabel('Enable Parallax Effect'); - await parallaxToggle.click(); - - // Publish the post - await page.getByRole('button', { name: 'Publish', exact: true }).click(); - const publishPanelButton = page - .getByLabel('Editor publish') - .getByRole('button', { name: 'Publish', exact: true }); - await publishPanelButton.click(); - - await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); - - // Get the post URL - const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); - const postUrl = await viewPostLink.getAttribute('href'); - - if (!postUrl) { - throw new Error('Could not get post URL'); - } - - // Visit the frontend post - await page.goto(postUrl); - await page.waitForLoadState('domcontentloaded'); - - // Verify the paragraph has parallax data attribute but effect is disabled - const parallaxParagraph = page.locator('p[data-parallax="1"]'); - await expect(parallaxParagraph).toBeVisible(); - - // Check that animations are disabled - const computedStyles = await parallaxParagraph.evaluate((el) => { - const styles = window.getComputedStyle(el); - return { - animation: styles.animation, - transform: styles.transform, - }; - }); - - expect(computedStyles.animation).toContain('none'); - expect(computedStyles.transform).toBe('none'); - }); - - test('should disable clip-path animations when prefers-reduced-motion is set', async ({ - page, - }: { - page: Page; - }) => { - // Set the prefers-reduced-motion preference - await page.emulateMedia({ reducedMotion: 'reduce' }); - - // Create a post with circle reveal animation - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Create post - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill('Circle Reveal Reduced Motion Test', { timeout: 15000 }); - - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('Circle reveal should be disabled with reduced motion', { - timeout: 15000, - }); - - const animationTypeSelect = page.getByLabel('Animation Type'); - await animationTypeSelect.selectOption('Circle Reveal'); - - // Publish the post - await page.getByRole('button', { name: 'Publish', exact: true }).click(); - const publishPanelButton = page - .getByLabel('Editor publish') - .getByRole('button', { name: 'Publish', exact: true }); - await publishPanelButton.click(); - - await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); - - // Get the post URL - const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); - const postUrl = await viewPostLink.getAttribute('href'); - - if (!postUrl) { - throw new Error('Could not get post URL'); - } - - // Visit the frontend post - await page.goto(postUrl); - await page.waitForLoadState('domcontentloaded'); - - // Verify clip-path is disabled - const revealParagraph = page.locator('p.scroll-anim-circle-reveal[data-scroll-anim="1"]'); - await expect(revealParagraph).toBeVisible(); - - const computedStyles = await revealParagraph.evaluate((el) => { - const styles = window.getComputedStyle(el); - return { - clipPath: styles.clipPath, - animation: styles.animation, - }; - }); - - expect(computedStyles.clipPath).toBe('none'); - expect(computedStyles.animation).toContain('none'); - }); - - test('should disable blur filter when prefers-reduced-motion is set', async ({ - page, - }: { - page: Page; - }) => { - // Set the prefers-reduced-motion preference - await page.emulateMedia({ reducedMotion: 'reduce' }); - - // Create a post with blur-in animation - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Create post - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill('Blur In Reduced Motion Test', { timeout: 15000 }); - - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('Blur effect should be disabled with reduced motion', { - timeout: 15000, - }); - - const animationTypeSelect = page.getByLabel('Animation Type'); - await animationTypeSelect.selectOption('Blur In'); - - // Publish the post - await page.getByRole('button', { name: 'Publish', exact: true }).click(); - const publishPanelButton = page - .getByLabel('Editor publish') - .getByRole('button', { name: 'Publish', exact: true }); - await publishPanelButton.click(); - - await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); - - // Get the post URL - const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); - const postUrl = await viewPostLink.getAttribute('href'); - - if (!postUrl) { - throw new Error('Could not get post URL'); - } - - // Visit the frontend post - await page.goto(postUrl); - await page.waitForLoadState('domcontentloaded'); - - // Verify filter is disabled - const blurParagraph = page.locator('p.scroll-anim-blur-in[data-scroll-anim="1"]'); - await expect(blurParagraph).toBeVisible(); - - const computedStyles = await blurParagraph.evaluate((el) => { - const styles = window.getComputedStyle(el); - return { - filter: styles.filter, - opacity: styles.opacity, - animation: styles.animation, - }; - }); - - expect(computedStyles.filter).toBe('none'); - expect(parseFloat(computedStyles.opacity)).toBe(1); - expect(computedStyles.animation).toContain('none'); - }); - - test('should allow animations when prefers-reduced-motion is not set', async ({ - page, - }: { - page: Page; - }) => { - // Do NOT set reduced motion preference (default is 'no-preference') - await page.emulateMedia({ reducedMotion: 'no-preference' }); - - // Create a post with animation - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Create post - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill('Normal Animation Test', { timeout: 15000 }); - - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('This paragraph should animate normally', { timeout: 15000 }); - - const animationTypeSelect = page.getByLabel('Animation Type'); - await animationTypeSelect.selectOption('Fade In'); - - // Publish the post - await page.getByRole('button', { name: 'Publish', exact: true }).click(); - const publishPanelButton = page - .getByLabel('Editor publish') - .getByRole('button', { name: 'Publish', exact: true }); - await publishPanelButton.click(); - - await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); - - // Get the post URL - const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); - const postUrl = await viewPostLink.getAttribute('href'); - - if (!postUrl) { - throw new Error('Could not get post URL'); - } - - // Visit the frontend post - await page.goto(postUrl); - await page.waitForLoadState('domcontentloaded'); - - // Verify the paragraph has the animation classes - const animatedParagraph = page.locator('p.scroll-anim-fade-in[data-scroll-anim="1"]'); - await expect(animatedParagraph).toBeVisible(); - - // Animation name should be defined (not 'none') - const animationName = await animatedParagraph.evaluate((el) => { - const styles = window.getComputedStyle(el); - return styles.animationName; - }); - - // Should have the scrollFadeIn animation name - expect(animationName).not.toBe('none'); - expect(animationName).toContain('scrollFadeIn'); - }); + test('should disable animations when prefers-reduced-motion is set', async ({ + page, + }: { + page: Page; + }) => { + // Set the prefers-reduced-motion preference + await page.emulateMedia({ reducedMotion: 'reduce' }); + + // Create a post with scroll animation + await page.goto('/wp-admin/post-new.php'); + await setupEditor(page); + + // Create post with animation + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill('Reduced Motion Test', { timeout: 15000 }); + + await addParagraphBlock(page, 'This paragraph should not animate with reduced motion'); + + // Wait for sidebar to be ready + await page.waitForTimeout(1000); + + const animationTypeSelect = page.getByLabel('Animation Type'); + await expect(animationTypeSelect).toBeVisible({ timeout: 10000 }); + await animationTypeSelect.selectOption('Fade In'); + + // Publish the post + await page.getByRole('button', { name: 'Publish', exact: true }).click(); + const publishPanelButton = page + .getByLabel('Editor publish') + .getByRole('button', { name: 'Publish', exact: true }); + await publishPanelButton.click(); + + // Wait for post to be published + await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); + + // Get the post URL + const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); + const postUrl = await viewPostLink.getAttribute('href'); + + if (!postUrl) { + throw new Error('Could not get post URL'); + } + + // Visit the frontend post with reduced motion preference + await page.goto(postUrl); + await page.waitForLoadState('domcontentloaded'); + + // Verify the paragraph has the animation class but animations should be disabled via CSS + const animatedParagraph = page.locator('p.scroll-anim-fade-in[data-scroll-anim="1"]'); + await expect(animatedParagraph).toBeVisible(); + + // Check computed styles - with prefers-reduced-motion, the element should have: + // - opacity: 1 (not 0) + // - transform: none + // - animation: none + const computedStyles = await animatedParagraph.evaluate((el) => { + const styles = window.getComputedStyle(el); + return { + opacity: styles.opacity, + transform: styles.transform, + animation: styles.animation, + }; + }); + + // With reduced motion, the CSS should override the animation styles + expect(parseFloat(computedStyles.opacity)).toBe(1); + expect(computedStyles.transform).toBe('none'); + expect(computedStyles.animation).toContain('none'); + }); + + test('should disable parallax effect when prefers-reduced-motion is set', async ({ + page, + }: { + page: Page; + }) => { + // Set the prefers-reduced-motion preference + await page.emulateMedia({ reducedMotion: 'reduce' }); + + // Create a post with parallax + await page.goto('/wp-admin/post-new.php'); + await setupEditor(page); + + // Create post + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill('Parallax Reduced Motion Test', { timeout: 15000 }); + + await addParagraphBlock(page, 'This paragraph has parallax disabled with reduced motion'); + + // Wait for sidebar to be ready + await page.waitForTimeout(1000); + + // Enable parallax + const parallaxToggle = page.getByLabel('Enable Parallax Effect'); + await expect(parallaxToggle).toBeVisible({ timeout: 10000 }); + await parallaxToggle.click(); + + // Publish the post + await page.getByRole('button', { name: 'Publish', exact: true }).click(); + const publishPanelButton = page + .getByLabel('Editor publish') + .getByRole('button', { name: 'Publish', exact: true }); + await publishPanelButton.click(); + + await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); + + // Get the post URL + const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); + const postUrl = await viewPostLink.getAttribute('href'); + + if (!postUrl) { + throw new Error('Could not get post URL'); + } + + // Visit the frontend post + await page.goto(postUrl); + await page.waitForLoadState('domcontentloaded'); + + // Verify the paragraph has parallax data attribute but effect is disabled + const parallaxParagraph = page.locator('p[data-parallax="1"]'); + await expect(parallaxParagraph).toBeVisible(); + + // Check that animations are disabled + const computedStyles = await parallaxParagraph.evaluate((el) => { + const styles = window.getComputedStyle(el); + return { + animation: styles.animation, + transform: styles.transform, + }; + }); + + expect(computedStyles.animation).toContain('none'); + expect(computedStyles.transform).toBe('none'); + }); + + test('should disable clip-path animations when prefers-reduced-motion is set', async ({ + page, + }: { + page: Page; + }) => { + // Set the prefers-reduced-motion preference + await page.emulateMedia({ reducedMotion: 'reduce' }); + + // Create a post with circle reveal animation + await page.goto('/wp-admin/post-new.php'); + await setupEditor(page); + + // Create post + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill('Circle Reveal Reduced Motion Test', { timeout: 15000 }); + + await addParagraphBlock(page, 'Circle reveal should be disabled with reduced motion'); + + // Wait for sidebar to be ready + await page.waitForTimeout(1000); + + const animationTypeSelect = page.getByLabel('Animation Type'); + await expect(animationTypeSelect).toBeVisible({ timeout: 10000 }); + await animationTypeSelect.selectOption('Circle Reveal'); + + // Publish the post + await page.getByRole('button', { name: 'Publish', exact: true }).click(); + const publishPanelButton = page + .getByLabel('Editor publish') + .getByRole('button', { name: 'Publish', exact: true }); + await publishPanelButton.click(); + + await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); + + // Get the post URL + const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); + const postUrl = await viewPostLink.getAttribute('href'); + + if (!postUrl) { + throw new Error('Could not get post URL'); + } + + // Visit the frontend post + await page.goto(postUrl); + await page.waitForLoadState('domcontentloaded'); + + // Verify clip-path is disabled + const revealParagraph = page.locator('p.scroll-anim-circle-reveal[data-scroll-anim="1"]'); + await expect(revealParagraph).toBeVisible(); + + const computedStyles = await revealParagraph.evaluate((el) => { + const styles = window.getComputedStyle(el); + return { + clipPath: styles.clipPath, + animation: styles.animation, + }; + }); + + expect(computedStyles.clipPath).toBe('none'); + expect(computedStyles.animation).toContain('none'); + }); + + test('should disable blur filter when prefers-reduced-motion is set', async ({ + page, + }: { + page: Page; + }) => { + // Set the prefers-reduced-motion preference + await page.emulateMedia({ reducedMotion: 'reduce' }); + + // Create a post with blur-in animation + await page.goto('/wp-admin/post-new.php'); + await setupEditor(page); + + // Create post + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill('Blur In Reduced Motion Test', { timeout: 15000 }); + + await addParagraphBlock(page, 'Blur effect should be disabled with reduced motion'); + + // Wait for sidebar to be ready + await page.waitForTimeout(1000); + + const animationTypeSelect = page.getByLabel('Animation Type'); + await expect(animationTypeSelect).toBeVisible({ timeout: 10000 }); + await animationTypeSelect.selectOption('Blur In'); + + // Publish the post + await page.getByRole('button', { name: 'Publish', exact: true }).click(); + const publishPanelButton = page + .getByLabel('Editor publish') + .getByRole('button', { name: 'Publish', exact: true }); + await publishPanelButton.click(); + + await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); + + // Get the post URL + const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); + const postUrl = await viewPostLink.getAttribute('href'); + + if (!postUrl) { + throw new Error('Could not get post URL'); + } + + // Visit the frontend post + await page.goto(postUrl); + await page.waitForLoadState('domcontentloaded'); + + // Verify filter is disabled + const blurParagraph = page.locator('p.scroll-anim-blur-in[data-scroll-anim="1"]'); + await expect(blurParagraph).toBeVisible(); + + const computedStyles = await blurParagraph.evaluate((el) => { + const styles = window.getComputedStyle(el); + return { + filter: styles.filter, + opacity: styles.opacity, + animation: styles.animation, + }; + }); + + expect(computedStyles.filter).toBe('none'); + expect(parseFloat(computedStyles.opacity)).toBe(1); + expect(computedStyles.animation).toContain('none'); + }); + + test('should allow animations when prefers-reduced-motion is not set', async ({ + page, + }: { + page: Page; + }) => { + // Do NOT set reduced motion preference (default is 'no-preference') + await page.emulateMedia({ reducedMotion: 'no-preference' }); + + // Create a post with animation + await page.goto('/wp-admin/post-new.php'); + await setupEditor(page); + + // Create post + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill('Normal Animation Test', { timeout: 15000 }); + + await addParagraphBlock(page, 'This paragraph should animate normally'); + + // Wait for sidebar to be ready + await page.waitForTimeout(1000); + + const animationTypeSelect = page.getByLabel('Animation Type'); + await expect(animationTypeSelect).toBeVisible({ timeout: 10000 }); + await animationTypeSelect.selectOption('Fade In'); + + // Publish the post + await page.getByRole('button', { name: 'Publish', exact: true }).click(); + const publishPanelButton = page + .getByLabel('Editor publish') + .getByRole('button', { name: 'Publish', exact: true }); + await publishPanelButton.click(); + + await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); + + // Get the post URL + const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); + const postUrl = await viewPostLink.getAttribute('href'); + + if (!postUrl) { + throw new Error('Could not get post URL'); + } + + // Visit the frontend post + await page.goto(postUrl); + await page.waitForLoadState('domcontentloaded'); + + // Verify the paragraph has the animation classes + const animatedParagraph = page.locator('p.scroll-anim-fade-in[data-scroll-anim="1"]'); + await expect(animatedParagraph).toBeVisible(); + + // Animation name should be defined (not 'none') + const animationName = await animatedParagraph.evaluate((el) => { + const styles = window.getComputedStyle(el); + return styles.animationName; + }); + + // Should have the scrollFadeIn animation name + expect(animationName).not.toBe('none'); + expect(animationName).toContain('scrollFadeIn'); + }); }); diff --git a/tests/scroll-block.spec.ts b/tests/scroll-block.spec.ts index 79796f9..c156165 100644 --- a/tests/scroll-block.spec.ts +++ b/tests/scroll-block.spec.ts @@ -1,332 +1,370 @@ import { test, expect, type Page } from '@playwright/test'; +/** + * Helper function to dismiss the WordPress welcome modal and wait for the editor to be ready + */ +async function setupEditor(page: Page): Promise { + await page.waitForLoadState('domcontentloaded'); + + // Wait for the editor to initialize + await page.waitForTimeout(1000); + + // Dismiss the welcome modal if present - try multiple possible buttons + const welcomeModalDismissButtons = [ + page.getByRole('button', { name: 'Get started' }), + page.getByRole('button', { name: 'Close', exact: true }), + page.locator('.components-modal__screen-overlay button[aria-label="Close"]'), + page.locator('.components-modal__header button'), + ]; + + for (const button of welcomeModalDismissButtons) { + try { + if (await button.isVisible({ timeout: 2000 })) { + await button.click(); + await page.waitForTimeout(500); + break; + } + } catch { + // Button not found or not clickable, try next one + } + } + + // Wait for any modal to disappear + try { + await page.waitForSelector('.components-modal__screen-overlay', { + state: 'hidden', + timeout: 5000, + }); + } catch { + // Modal may not exist + } +} + +/** + * Helper to add a paragraph block and ensure it's selected + */ +async function addParagraphBlock(page: Page, text: string): Promise { + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + + // Wait for the editor canvas to be ready + await editorFrame.locator('body').waitFor({ state: 'visible', timeout: 30000 }); + + // Click on the add block button or empty paragraph + const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); + const emptyParagraph = editorFrame.locator('p.block-editor-default-block-appender__content'); + + try { + if (await addBlockButton.isVisible({ timeout: 5000 })) { + await addBlockButton.click(); + } else if (await emptyParagraph.isVisible({ timeout: 2000 })) { + await emptyParagraph.click(); + } + } catch { + // Try clicking on the editor body + await editorFrame.locator('.wp-block-post-content').click(); + } + + // Wait for the block to be ready and type + await page.waitForTimeout(500); + await page.keyboard.type(text); + await page.waitForTimeout(500); +} + test.describe('WordPress Playground Setup', () => { - test('should load WordPress homepage', async ({ page }: { page: Page }) => { - await page.goto('/'); + test('should load WordPress homepage', async ({ page }: { page: Page }) => { + await page.goto('/'); - // Wait for the page to be fully loaded - await page.waitForLoadState('networkidle'); + // Wait for the page to be fully loaded + await page.waitForLoadState('networkidle'); - // Check that WordPress loaded successfully - await expect(page).toHaveTitle(/WordPress Scroll-driven block/); + // Check that WordPress loaded successfully + await expect(page).toHaveTitle(/WordPress Scroll-driven block/); - // Verify we can see WordPress content - const body = await page.locator('body'); - await expect(body).toBeVisible(); - }); + // Verify we can see WordPress content + const body = await page.locator('body'); + await expect(body).toBeVisible(); + }); - test('should have My Scroll Block plugin activated', async ({ page }: { page: Page }) => { - await page.goto('/wp-admin/plugins.php'); - await page.waitForLoadState('networkidle'); + test('should have My Scroll Block plugin activated', async ({ page }: { page: Page }) => { + await page.goto('/wp-admin/plugins.php'); + await page.waitForLoadState('networkidle'); - // Look for the plugin row - const pluginRow = page.locator('tr').filter({ hasText: 'My Scroll Block' }); - await expect(pluginRow).toBeVisible(); + // Look for the plugin row + const pluginRow = page.locator('tr').filter({ hasText: 'My Scroll Block' }); + await expect(pluginRow).toBeVisible(); - // Verify the plugin is active (shows "Deactivate" link) - const deactivateLink = pluginRow.getByRole('link', { name: 'Deactivate My Scroll Block' }); - await expect(deactivateLink).toBeVisible(); + // Verify the plugin is active (shows "Deactivate" link) + const deactivateLink = pluginRow.getByRole('link', { name: 'Deactivate My Scroll Block' }); + await expect(deactivateLink).toBeVisible(); - // Verify plugin description - await expect(pluginRow).toContainText( - 'Adds a Scroll Animation panel to supported core blocks.' - ); - }); + // Verify plugin description + await expect(pluginRow).toContainText( + 'Adds a Scroll Animation panel to supported core blocks.' + ); + }); }); test.describe('Block Editor - Scroll Animation Panel', () => { - test('should show Scroll Animation panel for paragraph block', async ({ - page, - }: { - page: Page; - }) => { - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Add a paragraph block - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - // Type some text - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('Test paragraph with scroll animation', { timeout: 15000 }); - - // Check for Scroll Animation panel in the sidebar - const scrollAnimationHeading = page.getByRole('heading', { name: 'Scroll Animation' }); - await expect(scrollAnimationHeading).toBeVisible(); - - // Verify Animation Type dropdown exists - const animationTypeSelect = page.getByLabel('Animation Type'); - await expect(animationTypeSelect).toBeVisible(); - }); - - test('should apply Fade In animation to paragraph', async ({ page }: { page: Page }) => { - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Add title - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill('Test Animation Post'); - - // Add paragraph - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click(); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('This paragraph will fade in'); - - // Select Fade In animation - const animationTypeSelect = page.getByLabel('Animation Type'); - await animationTypeSelect.selectOption('Fade In'); - - // Verify the animation was applied (look for the indicator button in the frame) - const animationIndicator = editorFrame.getByRole('button', { - name: /Scroll Animation Applied/, - }); - await expect(animationIndicator).toBeVisible(); - }); - - test('should have all animation type options available', async ({ page }: { page: Page }) => { - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Add a paragraph block - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - // Check animation options - const animationTypeSelect = page.getByLabel('Animation Type'); - - // Verify all expected animation types are available - const expectedOptions: string[] = [ - 'None', - 'Fade In', - 'Slide In Left', - 'Slide In Right', - 'Slide In Up', - 'Slide In Down', - 'Scale Up', - 'Rotate In', - ]; - - for (const option of expectedOptions) { - const optionElement = animationTypeSelect.locator(`option:has-text("${option}")`); - await expect(optionElement).toBeAttached(); - } - }); + test('should show Scroll Animation panel for paragraph block', async ({ + page, + }: { + page: Page; + }) => { + await page.goto('/wp-admin/post-new.php'); + await setupEditor(page); + + // Add a paragraph block + await addParagraphBlock(page, 'Test paragraph with scroll animation'); + + // Wait for the sidebar to update + await page.waitForTimeout(1000); + + // Check for Scroll Animation panel in the sidebar + const scrollAnimationHeading = page.getByRole('heading', { name: 'Scroll Animation' }); + await expect(scrollAnimationHeading).toBeVisible({ timeout: 10000 }); + + // Verify Animation Type dropdown exists + const animationTypeSelect = page.getByLabel('Animation Type'); + await expect(animationTypeSelect).toBeVisible(); + }); + + test('should apply Fade In animation to paragraph', async ({ page }: { page: Page }) => { + await page.goto('/wp-admin/post-new.php'); + await setupEditor(page); + + // Add title + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill('Test Animation Post'); + + // Add paragraph + await addParagraphBlock(page, 'This paragraph will fade in'); + + // Wait for sidebar to be ready + await page.waitForTimeout(1000); + + // Select Fade In animation + const animationTypeSelect = page.getByLabel('Animation Type'); + await expect(animationTypeSelect).toBeVisible({ timeout: 10000 }); + await animationTypeSelect.selectOption('Fade In'); + + // Verify the animation was applied (look for the indicator button in the frame) + const animationIndicator = editorFrame.getByRole('button', { + name: /Scroll Animation Applied/, + }); + await expect(animationIndicator).toBeVisible({ timeout: 10000 }); + }); + + test('should have all animation type options available', async ({ page }: { page: Page }) => { + await page.goto('/wp-admin/post-new.php'); + await setupEditor(page); + + // Add a paragraph block + await addParagraphBlock(page, 'Test paragraph'); + + // Wait for sidebar to be ready + await page.waitForTimeout(1000); + + // Check animation options + const animationTypeSelect = page.getByLabel('Animation Type'); + await expect(animationTypeSelect).toBeVisible({ timeout: 10000 }); + + // Verify all expected animation types are available + const expectedOptions: string[] = [ + 'None', + 'Fade In', + 'Slide In Left', + 'Slide In Right', + 'Slide In Up', + 'Slide In Down', + 'Scale Up', + 'Rotate In', + ]; + + for (const option of expectedOptions) { + const optionElement = animationTypeSelect.locator(`option:has-text("${option}")`); + await expect(optionElement).toBeAttached(); + } + }); }); test.describe('Frontend - Scroll Animation Rendering', () => { - test('should render published post with scroll animation classes', async ({ - page, - }: { - page: Page; - }) => { - // First create and publish a post with animation - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - // Close welcome dialog if present - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - // Create post with animation - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill('Frontend Animation Test', { timeout: 15000 }); - - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('This paragraph has a fade in animation', { timeout: 15000 }); - - const animationTypeSelect = page.getByLabel('Animation Type'); - await animationTypeSelect.selectOption('Fade In'); - - // Publish the post - await page.getByRole('button', { name: 'Publish', exact: true }).click(); - const publishPanelButton = page - .getByLabel('Editor publish') - .getByRole('button', { name: 'Publish', exact: true }); - await publishPanelButton.click(); - - // Wait for post to be published - await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); - - // Get the post URL - const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); - const postUrl = await viewPostLink.getAttribute('href'); - - if (!postUrl) { - throw new Error('Could not get post URL'); - } - - // Visit the frontend post - await page.goto(postUrl); - await page.waitForLoadState('domcontentloaded'); - - // Verify the paragraph has correct scroll animation classes and attributes - const animatedParagraph = page.locator( - 'p.scroll-anim-block.scroll-anim-fade-in[data-scroll-anim="1"]' - ); - await expect(animatedParagraph).toBeVisible(); - await expect(animatedParagraph).toContainText('This paragraph has a fade in animation'); - }); - - test('should apply different animation types correctly on frontend', async ({ - page, - }: { - page: Page; - }) => { - interface AnimationType { - type: string; - class: string; - } - - const animations: AnimationType[] = [ - { type: 'Slide In Left', class: 'scroll-anim-slide-in-left' }, - { type: 'Scale Up', class: 'scroll-anim-scale-up' }, - ]; - - for (const animation of animations) { - // Create post - await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); - - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await closeButton.click(); - } - - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); - await titleBox.fill(`Test ${animation.type}`, { timeout: 15000 }); - - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); - - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill(`Testing ${animation.type}`, { timeout: 15000 }); - - const animationTypeSelect = page.getByLabel('Animation Type'); - await animationTypeSelect.selectOption(animation.type); - - // Publish - await page.getByRole('button', { name: 'Publish', exact: true }).click(); - const publishPanelButton = page - .getByLabel('Editor publish') - .getByRole('button', { name: 'Publish', exact: true }); - await publishPanelButton.click(); - - await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); - - // Visit frontend - const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); - const postUrl = await viewPostLink.getAttribute('href'); - - if (!postUrl) { - throw new Error('Could not get post URL'); - } - - await page.goto(postUrl); - await page.waitForLoadState('domcontentloaded'); - - // Verify animation class - look for any paragraph with the animation class - const animatedElement = page.locator(`p.${animation.class}`); - const isVisible = await animatedElement.isVisible({ timeout: 5000 }).catch(() => false); - - // If not found, check if post content exists at all - if (!isVisible) { - const anyParagraph = page.locator('p').first(); - await expect(anyParagraph).toBeVisible(); - } else { - await expect(animatedElement).toBeVisible(); - } - } - }); - - test('should load scroll animation CSS styles', async ({ page }: { page: Page }) => { - // First go to homepage to get a valid URL - await page.goto('/'); - await page.waitForLoadState('domcontentloaded'); - - // Try to find the first post link - const postLink = page.locator('a[rel="bookmark"]').first(); - const firstPostUrl = await postLink.getAttribute('href').catch(() => null); - - if (!firstPostUrl) { - // Skip this test if there are no posts - return; - } - - await page.goto(firstPostUrl); - await page.waitForLoadState('domcontentloaded'); - - // Check if the style-index.css is loaded - const stylesheets = await page.evaluate((): string[] => { - return Array.from(document.styleSheets) - .map((sheet) => sheet.href) - .filter( - (href): href is string => href !== null && href.includes('style-index.css') - ); - }); - - expect(stylesheets.length).toBeGreaterThanOrEqual(0); // CSS may not be loaded if no animations are present - }); + test('should render published post with scroll animation classes', async ({ + page, + }: { + page: Page; + }) => { + // First create and publish a post with animation + await page.goto('/wp-admin/post-new.php'); + await setupEditor(page); + + // Create post with animation + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill('Frontend Animation Test', { timeout: 15000 }); + + await addParagraphBlock(page, 'This paragraph has a fade in animation'); + + // Wait for sidebar to be ready + await page.waitForTimeout(1000); + + const animationTypeSelect = page.getByLabel('Animation Type'); + await expect(animationTypeSelect).toBeVisible({ timeout: 10000 }); + await animationTypeSelect.selectOption('Fade In'); + + // Publish the post + await page.getByRole('button', { name: 'Publish', exact: true }).click(); + const publishPanelButton = page + .getByLabel('Editor publish') + .getByRole('button', { name: 'Publish', exact: true }); + await publishPanelButton.click(); + + // Wait for post to be published + await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); + + // Get the post URL + const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); + const postUrl = await viewPostLink.getAttribute('href'); + + if (!postUrl) { + throw new Error('Could not get post URL'); + } + + // Visit the frontend post + await page.goto(postUrl); + await page.waitForLoadState('domcontentloaded'); + + // Verify the paragraph has correct scroll animation classes and attributes + const animatedParagraph = page.locator( + 'p.scroll-anim-block.scroll-anim-fade-in[data-scroll-anim="1"]' + ); + await expect(animatedParagraph).toBeVisible(); + await expect(animatedParagraph).toContainText('This paragraph has a fade in animation'); + }); + + test('should apply different animation types correctly on frontend', async ({ + page, + }: { + page: Page; + }) => { + interface AnimationType { + type: string; + class: string; + } + + const animations: AnimationType[] = [ + { type: 'Slide In Left', class: 'scroll-anim-slide-in-left' }, + { type: 'Scale Up', class: 'scroll-anim-scale-up' }, + ]; + + for (const animation of animations) { + // Create post + await page.goto('/wp-admin/post-new.php'); + await setupEditor(page); + + const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); + const titleBox = editorFrame.getByRole('textbox', { name: 'Add title' }); + await titleBox.fill(`Test ${animation.type}`, { timeout: 15000 }); + + await addParagraphBlock(page, `Testing ${animation.type}`); + + // Wait for sidebar to be ready + await page.waitForTimeout(1000); + + const animationTypeSelect = page.getByLabel('Animation Type'); + await expect(animationTypeSelect).toBeVisible({ timeout: 10000 }); + await animationTypeSelect.selectOption(animation.type); + + // Publish + await page.getByRole('button', { name: 'Publish', exact: true }).click(); + const publishPanelButton = page + .getByLabel('Editor publish') + .getByRole('button', { name: 'Publish', exact: true }); + await publishPanelButton.click(); + + await page.waitForSelector('text=is now live', { timeout: 10000 }).catch(() => null); + + // Visit frontend + const viewPostLink = page.getByRole('link', { name: 'View Post' }).first(); + const postUrl = await viewPostLink.getAttribute('href'); + + if (!postUrl) { + throw new Error('Could not get post URL'); + } + + await page.goto(postUrl); + await page.waitForLoadState('domcontentloaded'); + + // Verify animation class - look for any paragraph with the animation class + const animatedElement = page.locator(`p.${animation.class}`); + const isVisible = await animatedElement.isVisible({ timeout: 5000 }).catch(() => false); + + // If not found, check if post content exists at all + if (!isVisible) { + const anyParagraph = page.locator('p').first(); + await expect(anyParagraph).toBeVisible(); + } else { + await expect(animatedElement).toBeVisible(); + } + } + }); + + test('should load scroll animation CSS styles', async ({ page }: { page: Page }) => { + // First go to homepage to get a valid URL + await page.goto('/'); + await page.waitForLoadState('domcontentloaded'); + + // Try to find the first post link + const postLink = page.locator('a[rel="bookmark"]').first(); + const firstPostUrl = await postLink.getAttribute('href').catch(() => null); + + if (!firstPostUrl) { + // Skip this test if there are no posts + return; + } + + await page.goto(firstPostUrl); + await page.waitForLoadState('domcontentloaded'); + + // Check if the style-index.css is loaded + const stylesheets = await page.evaluate((): string[] => { + return Array.from(document.styleSheets) + .map((sheet) => sheet.href) + .filter((href): href is string => href !== null && href.includes('style-index.css')); + }); + + expect(stylesheets.length).toBeGreaterThanOrEqual(0); // CSS may not be loaded if no animations are present + }); }); test.describe('Plugin Compatibility', () => { - test('should work with WordPress 6.8+', async ({ page }: { page: Page }) => { - await page.goto('/wp-admin'); - await page.waitForLoadState('domcontentloaded'); - - // Get WordPress version from the page HTML - const version = await page.evaluate((): string => { - const versionElement = Array.from(document.querySelectorAll('*')).find((el) => - el.textContent?.match(/Version \d+\.\d+/) - ); - return versionElement?.textContent || ''; - }); - - // If we can't find version text, at least verify the admin page loaded - const adminPageBody = page.locator('.wp-admin'); - await expect(adminPageBody).toBeVisible(); - - if (version) { - // Extract version like "Version 6.8.3" or just "6.8" - const match = version.match(/Version?\s*(\d+)\.(\d+)/); - if (match) { - const major = parseInt(match[1], 10); - const minor = parseInt(match[2], 10); - expect(major).toBeGreaterThanOrEqual(6); - if (major === 6) { - expect(minor).toBeGreaterThanOrEqual(7); - } - } - } - }); + test('should work with WordPress 6.8+', async ({ page }: { page: Page }) => { + await page.goto('/wp-admin'); + await page.waitForLoadState('domcontentloaded'); + + // Get WordPress version from the page HTML + const version = await page.evaluate((): string => { + const versionElement = Array.from(document.querySelectorAll('*')).find((el) => + el.textContent?.match(/Version \d+\.\d+/) + ); + return versionElement?.textContent || ''; + }); + + // If we can't find version text, at least verify the admin page loaded + const adminPageBody = page.locator('.wp-admin'); + await expect(adminPageBody).toBeVisible(); + + if (version) { + // Extract version like "Version 6.8.3" or just "6.8" + const match = version.match(/Version?\s*(\d+)\.(\d+)/); + if (match) { + const major = parseInt(match[1], 10); + const minor = parseInt(match[2], 10); + expect(major).toBeGreaterThanOrEqual(6); + if (major === 6) { + expect(minor).toBeGreaterThanOrEqual(7); + } + } + } + }); }); diff --git a/webpack.config.js b/webpack.config.js index 5e3d726..6924cd7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,19 +2,19 @@ const defaultConfig = require('@wordpress/scripts/config/webpack.config'); // If defaultConfig is an array, extend each config if (Array.isArray(defaultConfig)) { - module.exports = defaultConfig.map((config) => ({ - ...config, - entry: { - index: './src/index.js', - ...(typeof config.entry === 'object' ? config.entry : {}), - }, - })); + module.exports = defaultConfig.map((config) => ({ + ...config, + entry: { + index: './src/index.js', + ...(typeof config.entry === 'object' ? config.entry : {}), + }, + })); } else { - module.exports = { - ...defaultConfig, - entry: { - index: './src/index.js', - ...(typeof defaultConfig.entry === 'object' ? defaultConfig.entry : {}), - }, - }; + module.exports = { + ...defaultConfig, + entry: { + index: './src/index.js', + ...(typeof defaultConfig.entry === 'object' ? defaultConfig.entry : {}), + }, + }; }