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/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 ) ); } } diff --git a/tests/reduced-motion.spec.ts b/tests/reduced-motion.spec.ts index f0b22d3..04afe61 100644 --- a/tests/reduced-motion.spec.ts +++ b/tests/reduced-motion.spec.ts @@ -1,5 +1,75 @@ 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, @@ -11,28 +81,20 @@ test.describe('Reduced Motion Support', () => { // 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(); - } + 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 }); - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); + await addParagraphBlock(page, 'This paragraph should not animate with reduced motion'); - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('This paragraph should not animate with reduced motion', { - timeout: 15000, - }); + // 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 @@ -90,29 +152,21 @@ test.describe('Reduced Motion Support', () => { // 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(); - } + 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 }); - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); + await addParagraphBlock(page, 'This paragraph has parallax disabled with reduced motion'); - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('This paragraph has parallax disabled with reduced motion', { - timeout: 15000, - }); + // 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 @@ -163,28 +217,20 @@ test.describe('Reduced Motion Support', () => { // 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(); - } + 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 }); - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); + await addParagraphBlock(page, 'Circle reveal should be disabled with reduced motion'); - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('Circle reveal should be disabled with reduced motion', { - timeout: 15000, - }); + // 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 @@ -234,28 +280,20 @@ test.describe('Reduced Motion Support', () => { // 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(); - } + 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 }); - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); + await addParagraphBlock(page, 'Blur effect should be disabled with reduced motion'); - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('Blur effect should be disabled with reduced motion', { - timeout: 15000, - }); + // 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 @@ -307,26 +345,20 @@ test.describe('Reduced Motion Support', () => { // 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(); - } + 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 }); - const addBlockButton = editorFrame.getByRole('button', { name: 'Add default block' }); - await addBlockButton.click({ timeout: 15000 }); + await addParagraphBlock(page, 'This paragraph should animate normally'); - const blockEditor = editorFrame.getByRole('document', { name: /Empty block/ }); - await blockEditor.fill('This paragraph should animate normally', { timeout: 15000 }); + // 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 diff --git a/tests/scroll-block.spec.ts b/tests/scroll-block.spec.ts index 60222bd..8bca29e 100644 --- a/tests/scroll-block.spec.ts +++ b/tests/scroll-block.spec.ts @@ -1,6 +1,110 @@ import { test, expect, type Page } from '@playwright/test'; -test.describe('Scroll Block Plugin Basic Check', () => { +/** + * 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('/'); + + // 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/); + + // 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'); + + // 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 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, }: { @@ -8,36 +112,15 @@ test.describe('Scroll Block Plugin Basic Check', () => { }) => { // 1. Go directly to the new post editor (as per updated blueprint) await page.goto('/wp-admin/post-new.php'); - await page.waitForLoadState('domcontentloaded'); + await setupEditor(page); - // 2. Handle any welcome/onboarding dialogs - const closeButton = page.getByRole('button', { name: 'Close' }); - if (await closeButton.isVisible({ timeout: 3000 }).catch(() => false)) { - await closeButton.click(); - } + // Add a paragraph block + await addParagraphBlock(page, 'Test paragraph with scroll animation'); - // 3. Add a Paragraph block (compatible with plugin) - // We look for the main editor canvas frame - const editorFrame = page.frameLocator('iframe[name="editor-canvas"]'); - - // In a fresh editor, there's often an empty paragraph block ready, or we click "Add block" - // We try to type into the default empty block first - const defaultBlock = editorFrame.getByRole('document', { name: /Empty block/ }); - - if (await defaultBlock.isVisible({ timeout: 5000 }).catch(() => false)) { - await defaultBlock.click(); - await defaultBlock.fill('Testing Scroll Block Plugin'); - } else { - // Fallback: explicitly add a paragraph - const addBlockBtn = editorFrame.getByRole('button', { name: 'Add default block' }); - if (await addBlockBtn.isVisible()) { - await addBlockBtn.click(); - await defaultBlock.fill('Testing Scroll Block Plugin'); - } - } - - // 4. Verify the "Scroll Animation" panel appears in the inspector sidebar - // This confirms the plugin is active and the JS logic for `SUPPORTED_BLOCKS` is working + // 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 }); @@ -46,4 +129,244 @@ test.describe('Scroll Block Plugin Basic Check', () => { await expect(animationTypeSelect).toBeVisible(); await expect(animationTypeSelect).toBeEnabled(); }); -}); \ No newline at end of file + + 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 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); + } + } + } + }); +});