Skip to content

Add E2E tests for SVG styles on mount#3654

Open
mattgperry wants to merge 1 commit intomainfrom
worktree-fix-issue-2949
Open

Add E2E tests for SVG styles on mount#3654
mattgperry wants to merge 1 commit intomainfrom
worktree-fix-issue-2949

Conversation

@mattgperry
Copy link
Collaborator

Summary

  • Adds Cypress E2E tests verifying SVG styles apply correctly on initial mount when using useTransform
  • Tests cover: transform/transformBox/transformOrigin, pathLength/stroke-dasharray/stroke-dashoffset, opacity, and fill on SVG elements
  • All tests pass on both React 18 and React 19

Background

The bug reported in #2949 — SVG transform-origin jumping on initial mount because dimensions aren't measured yet — was already fixed in PR #3154 (merged April 2025). That PR ensured transformBox: "fill-box" and transformOrigin: "50% 50%" are set on SVG elements before dimensions are measured. The fix was preserved during the subsequent motion-dom refactoring.

However, the issue was never auto-closed because PR #3154 used ref: #2949 instead of Fixes #2949. This PR adds targeted regression tests and closes the issue.

Fixes #2949

Test plan

  • Cypress tests pass on React 18
  • Cypress tests pass on React 19
  • All 776 existing unit tests pass
  • Build succeeds

🤖 Generated with Claude Code

The bug (SVG transform-origin jumping on initial mount) was fixed in PR
#3154, which ensured transformBox: "fill-box" and transformOrigin: "50%
50%" are set before dimensions are measured. The fix was preserved during
the motion-dom refactoring. This commit adds targeted E2E tests to
prevent regression and closes the issue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR adds two new files — a React test fixture and a Cypress E2E suite — to provide targeted regression coverage for issue #2949 (SVG transform-origin jumping on initial mount). The underlying bug was already fixed in PR #3154; these tests ensure it doesn't regress.

Key observations:

  • The test fixture (svg-style-on-mount.tsx) is clean and correctly exercises useTransform-derived pathLength, opacity, fill, and static rotate across motion.path, motion.circle, and motion.rect.
  • All five Cypress it blocks use .then() for their assertions instead of .should(), which is the established pattern throughout the rest of the integration suite. .should() provides Cypress's built-in retry-ability; .then() does not, making these tests more susceptible to timing-related false negatives.
  • The opacity test falls back to window.getComputedStyle($path).opacity, but window in a Cypress spec file is the spec-runner window, not the application window. If the SVG attribute path is ever absent, this fallback will query the wrong window.

Confidence Score: 4/5

  • Safe to merge — test-only changes with no production code impact; minor test-quality issues should be addressed before the suite grows.
  • No production code is modified; the fixture and test files are self-contained. The .then() vs .should() issue doesn't break anything today (styles are likely synchronous on mount) but is inconsistent with the codebase pattern and reduces resilience. The window reference in the opacity test is a latent bug that only fires if getAttribute("opacity") returns null.
  • packages/framer-motion/cypress/integration/svg-style-on-mount.ts — all five it blocks use .then() instead of .should(), and the opacity test uses the wrong window reference.

Important Files Changed

Filename Overview
packages/framer-motion/cypress/integration/svg-style-on-mount.ts New Cypress E2E suite for SVG-on-mount regression (#2949); uses .then() instead of the codebase's standard .should() in all five test cases (removing retry-ability), and references the spec-runner window instead of the app window in the opacity test.
dev/react/src/tests/svg-style-on-mount.tsx New React test fixture providing path, circle, and rect SVG elements with useTransform-derived styles; straightforward and correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["cy.visit('?test=svg-style-on-mount')"] --> B["React mounts svg-style-on-mount.tsx"]
    B --> C["useMotionValue(50)\nuseTransform → pathLength / opacity / fill"]
    C --> D["motion.path\n(pathLength, opacity, x:10, y:10)"]
    C --> E["motion.circle\n(fill)"]
    C --> F["motion.rect\n(rotate:45)"]
    D --> G["Test 1: transform / transformBox / transformOrigin"]
    D --> H["Test 2: pathLength attr / stroke-dasharray / stroke-dashoffset"]
    D --> I["Test 3: opacity attr (⚠ window.getComputedStyle fallback)"]
    E --> J["Test 4: fill attr non-null"]
    F --> K["Test 5: transform / transformBox / transformOrigin"]
    style I fill:#ffe0b2,stroke:#e65100
Loading

Last reviewed commit: ebccbf8

cy.visit("?test=svg-style-on-mount")
.get("#path")
.then(([$path]: any) => {
// Transform should be applied immediately on mount
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Use .should() instead of .then() for assertions

All five test cases use .then() for their assertions, but the rest of the codebase (e.g. svg.ts, animate-style.ts) consistently uses .should(). This is a meaningful difference in Cypress: .should() retries the callback until it passes (subject to the command timeout), giving Cypress's async retry-ability for style assertions that may not be synchronously available on the first tick. .then() runs the callback exactly once with no retry, making these tests potentially flaky if styles are applied asynchronously.

The same issue applies to all five it blocks (lines 18, 35, 49, 62, and 73).

Suggested change
// Transform should be applied immediately on mount
.should(([$path]: any) => {

Comment on lines +52 to +55
.get("#path")
.then(([$path]: any) => {
// opacity should be 0.5 (useTransform(50, [0,100], [0,1]))
const opacity =
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 window refers to spec-runner window, not the app window

Inside a Cypress .then() (or .should()) callback, the plain window global resolves to the Cypress spec-runner window, not the tested application's window. Calling window.getComputedStyle($path) on an element from the app iframe may return incorrect results or throw a cross-origin error.

The canonical Cypress pattern is to use cy.window() to obtain the application window:

it("Applies useTransform-derived opacity on SVG path on mount", () => {
    cy.visit("?test=svg-style-on-mount")
        .get("#path")
        .should(([$path]: any) => {
            const opacity = $path.getAttribute("opacity")
            // framer-motion sets opacity as a native SVG attribute,
            // so getAttribute should always return a value here.
            expect(parseFloat(opacity)).to.equal(0.5)
        })
})

If a fallback to computed style is genuinely needed, restructure with cy.window():

cy.window().then((win) => {
    const opacity =
        $path.getAttribute("opacity") ??
        win.getComputedStyle($path).opacity
    expect(parseFloat(opacity)).to.equal(0.5)
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] SVG styles not applying on mount

1 participant