@@ -16,6 +16,7 @@ import {
1616 updateKeyframeInScript ,
1717 convertToKeyframesInScript ,
1818 removeAllKeyframesFromScript ,
19+ addAnimationWithKeyframesToScript ,
1920} from "./gsapParser.js" ;
2021import type { GsapAnimation } from "./gsapParser.js" ;
2122import type { Keyframe } from "../core.types" ;
@@ -1504,3 +1505,202 @@ describe("keyframe mutations", () => {
15041505 expect ( anim . properties . opacity ) . toBe ( 1 ) ;
15051506 } ) ;
15061507} ) ;
1508+
1509+ describe ( "motionPath parsing" , ( ) => {
1510+ it ( "parses motionPath with waypoint array and curviness" , ( ) => {
1511+ const script = `
1512+ const tl = gsap.timeline({ paused: true });
1513+ tl.to("#el", {
1514+ motionPath: {
1515+ path: [{x: 0, y: 0}, {x: 200, y: -100}, {x: 400, y: 50}],
1516+ curviness: 1.5
1517+ },
1518+ duration: 2
1519+ }, 0);
1520+ ` ;
1521+ const result = parseGsapScript ( script ) ;
1522+ expect ( result . animations ) . toHaveLength ( 1 ) ;
1523+ const anim = result . animations [ 0 ] ;
1524+
1525+ expect ( anim . arcPath ) . toBeDefined ( ) ;
1526+ expect ( anim . arcPath ! . enabled ) . toBe ( true ) ;
1527+ expect ( anim . arcPath ! . segments ) . toHaveLength ( 2 ) ;
1528+ expect ( anim . arcPath ! . segments [ 0 ] . curviness ) . toBe ( 1.5 ) ;
1529+ expect ( anim . arcPath ! . segments [ 1 ] . curviness ) . toBe ( 1.5 ) ;
1530+
1531+ expect ( anim . keyframes ) . toBeDefined ( ) ;
1532+ expect ( anim . keyframes ! . keyframes ) . toHaveLength ( 3 ) ;
1533+ expect ( anim . keyframes ! . keyframes [ 0 ] . properties . x ) . toBe ( 0 ) ;
1534+ expect ( anim . keyframes ! . keyframes [ 0 ] . properties . y ) . toBe ( 0 ) ;
1535+ expect ( anim . keyframes ! . keyframes [ 1 ] . properties . x ) . toBe ( 200 ) ;
1536+ expect ( anim . keyframes ! . keyframes [ 1 ] . properties . y ) . toBe ( - 100 ) ;
1537+ expect ( anim . keyframes ! . keyframes [ 2 ] . properties . x ) . toBe ( 400 ) ;
1538+ expect ( anim . keyframes ! . keyframes [ 2 ] . properties . y ) . toBe ( 50 ) ;
1539+ } ) ;
1540+
1541+ it ( "parses motionPath with type cubic and explicit control points" , ( ) => {
1542+ const script = `
1543+ const tl = gsap.timeline({ paused: true });
1544+ tl.to("#el", {
1545+ motionPath: {
1546+ path: [
1547+ {x: 0, y: 0},
1548+ {x: 50, y: -80}, {x: 150, y: -120},
1549+ {x: 200, y: -100},
1550+ {x: 250, y: -80}, {x: 350, y: 30},
1551+ {x: 400, y: 50}
1552+ ],
1553+ type: "cubic"
1554+ },
1555+ duration: 2
1556+ }, 0);
1557+ ` ;
1558+ const result = parseGsapScript ( script ) ;
1559+ const anim = result . animations [ 0 ] ;
1560+
1561+ expect ( anim . arcPath ) . toBeDefined ( ) ;
1562+ expect ( anim . arcPath ! . segments ) . toHaveLength ( 2 ) ;
1563+
1564+ expect ( anim . arcPath ! . segments [ 0 ] . cp1 ) . toEqual ( { x : 50 , y : - 80 } ) ;
1565+ expect ( anim . arcPath ! . segments [ 0 ] . cp2 ) . toEqual ( { x : 150 , y : - 120 } ) ;
1566+
1567+ expect ( anim . arcPath ! . segments [ 1 ] . cp1 ) . toEqual ( { x : 250 , y : - 80 } ) ;
1568+ expect ( anim . arcPath ! . segments [ 1 ] . cp2 ) . toEqual ( { x : 350 , y : 30 } ) ;
1569+
1570+ expect ( anim . keyframes ! . keyframes ) . toHaveLength ( 3 ) ;
1571+ expect ( anim . keyframes ! . keyframes [ 0 ] . properties ) . toEqual ( { x : 0 , y : 0 } ) ;
1572+ expect ( anim . keyframes ! . keyframes [ 1 ] . properties ) . toEqual ( { x : 200 , y : - 100 } ) ;
1573+ expect ( anim . keyframes ! . keyframes [ 2 ] . properties ) . toEqual ( { x : 400 , y : 50 } ) ;
1574+ } ) ;
1575+
1576+ it ( "parses motionPath with autoRotate" , ( ) => {
1577+ const script = `
1578+ const tl = gsap.timeline({ paused: true });
1579+ tl.to("#el", {
1580+ motionPath: {
1581+ path: [{x: 0, y: 0}, {x: 200, y: 100}],
1582+ autoRotate: true
1583+ },
1584+ duration: 1
1585+ }, 0);
1586+ ` ;
1587+ const result = parseGsapScript ( script ) ;
1588+ const anim = result . animations [ 0 ] ;
1589+ expect ( anim . arcPath ! . autoRotate ) . toBe ( true ) ;
1590+ } ) ;
1591+
1592+ it ( "merges motionPath waypoints into existing keyframes" , ( ) => {
1593+ const script = `
1594+ const tl = gsap.timeline({ paused: true });
1595+ tl.to("#el", {
1596+ motionPath: {
1597+ path: [{x: 0, y: 0}, {x: 200, y: 100}],
1598+ curviness: 2
1599+ },
1600+ keyframes: {
1601+ "0%": { opacity: 1 },
1602+ "100%": { opacity: 0 }
1603+ },
1604+ duration: 2
1605+ }, 0);
1606+ ` ;
1607+ const result = parseGsapScript ( script ) ;
1608+ const anim = result . animations [ 0 ] ;
1609+
1610+ expect ( anim . arcPath ) . toBeDefined ( ) ;
1611+ expect ( anim . arcPath ! . segments ) . toHaveLength ( 1 ) ;
1612+ expect ( anim . arcPath ! . segments [ 0 ] . curviness ) . toBe ( 2 ) ;
1613+
1614+ expect ( anim . keyframes ! . keyframes ) . toHaveLength ( 2 ) ;
1615+ expect ( anim . keyframes ! . keyframes [ 0 ] . properties ) . toEqual ( { opacity : 1 , x : 0 , y : 0 } ) ;
1616+ expect ( anim . keyframes ! . keyframes [ 1 ] . properties ) . toEqual ( { opacity : 0 , x : 200 , y : 100 } ) ;
1617+ } ) ;
1618+
1619+ it ( "skips motionPath with fewer than 2 waypoints" , ( ) => {
1620+ const script = `
1621+ const tl = gsap.timeline({ paused: true });
1622+ tl.to("#el", {
1623+ motionPath: { path: [{x: 0, y: 0}] },
1624+ duration: 1
1625+ }, 0);
1626+ ` ;
1627+ const result = parseGsapScript ( script ) ;
1628+ expect ( result . animations [ 0 ] . arcPath ) . toBeUndefined ( ) ;
1629+ } ) ;
1630+
1631+ it ( "tween without motionPath parses identically to before" , ( ) => {
1632+ const script = `
1633+ const tl = gsap.timeline({ paused: true });
1634+ tl.to("#el", { x: 100, y: 200, duration: 1 }, 0);
1635+ ` ;
1636+ const result = parseGsapScript ( script ) ;
1637+ const anim = result . animations [ 0 ] ;
1638+ expect ( anim . arcPath ) . toBeUndefined ( ) ;
1639+ expect ( anim . properties . x ) . toBe ( 100 ) ;
1640+ expect ( anim . properties . y ) . toBe ( 200 ) ;
1641+ } ) ;
1642+ } ) ;
1643+
1644+ // ── addAnimationWithKeyframesToScript ──────────────────────────────────────
1645+
1646+ describe ( "addAnimationWithKeyframesToScript" , ( ) => {
1647+ const BASE = `
1648+ const tl = gsap.timeline({ paused: true });
1649+ tl.to("#title", { x: 100, duration: 0.5 }, 0);
1650+ ` . trim ( ) ;
1651+
1652+ it ( "adds a new tween with keyframes after existing tweens" , ( ) => {
1653+ const { script, id } = addAnimationWithKeyframesToScript ( BASE , "#box" , 3 , 0.5 , [
1654+ { percentage : 0 , properties : { x : 0 } } ,
1655+ { percentage : 100 , properties : { x : 200 } } ,
1656+ ] ) ;
1657+ expect ( script ) . toContain ( "#box" ) ;
1658+ expect ( script ) . toContain ( "keyframes" ) ;
1659+ expect ( script ) . toContain ( '"0%"' ) ;
1660+ expect ( script ) . toContain ( '"100%"' ) ;
1661+ expect ( id ) . toBeTruthy ( ) ;
1662+
1663+ const parsed = parseGsapScript ( script ) ;
1664+ expect ( parsed . animations . length ) . toBe ( 2 ) ;
1665+ const newAnim = parsed . animations [ 1 ] ;
1666+ expect ( newAnim . targetSelector ) . toBe ( "#box" ) ;
1667+ expect ( newAnim . keyframes ) . toBeDefined ( ) ;
1668+ expect ( newAnim . keyframes ! . keyframes . length ) . toBe ( 2 ) ;
1669+ } ) ;
1670+
1671+ it ( "preserves existing tween code" , ( ) => {
1672+ const { script } = addAnimationWithKeyframesToScript ( BASE , "#new" , 2 , 1 , [
1673+ { percentage : 0 , properties : { opacity : 0 } } ,
1674+ { percentage : 100 , properties : { opacity : 1 } } ,
1675+ ] ) ;
1676+ expect ( script ) . toContain ( "#title" ) ;
1677+ expect ( script ) . toContain ( "x: 100" ) ;
1678+ } ) ;
1679+
1680+ it ( "produces a stable ID for the new animation" , ( ) => {
1681+ const { script, id } = addAnimationWithKeyframesToScript ( BASE , "#el" , 1 , 1 , [
1682+ { percentage : 0 , properties : { y : 0 } } ,
1683+ { percentage : 100 , properties : { y : 100 } } ,
1684+ ] ) ;
1685+ expect ( id ) . toContain ( "#el" ) ;
1686+ const parsed = parseGsapScript ( script ) ;
1687+ const match = parsed . animations . find ( ( a ) => a . id === id ) ;
1688+ expect ( match ) . toBeDefined ( ) ;
1689+ } ) ;
1690+
1691+ it ( "includes per-keyframe ease when provided" , ( ) => {
1692+ const { script } = addAnimationWithKeyframesToScript ( BASE , "#el" , 0 , 1 , [
1693+ { percentage : 0 , properties : { x : 0 } , ease : "power2.out" } ,
1694+ { percentage : 100 , properties : { x : 100 } } ,
1695+ ] ) ;
1696+ expect ( script ) . toContain ( "power2.out" ) ;
1697+ } ) ;
1698+
1699+ it ( "returns original script on parse failure" , ( ) => {
1700+ const { script, id } = addAnimationWithKeyframesToScript ( "not valid js {{" , "#el" , 0 , 1 , [
1701+ { percentage : 0 , properties : { x : 0 } } ,
1702+ ] ) ;
1703+ expect ( script ) . toBe ( "not valid js {{" ) ;
1704+ expect ( id ) . toBe ( "" ) ;
1705+ } ) ;
1706+ } ) ;
0 commit comments