@@ -1468,6 +1468,196 @@ describe("Backward Compatibility - V1 Templates", () => {
14681468 expect ( findTransformationDeep ( tree , "deep" ) ?. name ) . toBe ( "Deep" )
14691469 expect ( findTransformationDeep ( tree , "missing" ) ) . toBeUndefined ( )
14701470 } )
1471+
1472+ it ( "non-layer child (ai-removedotbg) is appended as a chained step inside the parent layer" , async ( ) => {
1473+ const { buildSrc } = await import ( "@imagekit/javascript" )
1474+ const { convertTransformationToIK } = await import (
1475+ "./transformationConverter"
1476+ )
1477+ // Parent has multiple own-params (image url + width + trim) so the SDK
1478+ // serializes the child with an explicit `:` chain separator. With a
1479+ // single-param parent the SDK collapses to a `,` joiner — equivalent
1480+ // ImageKit syntax, but less obvious that the child is a chained step.
1481+ const parent : Transformation = {
1482+ id : "p" ,
1483+ key : "layers-image" ,
1484+ name : "Image Layer" ,
1485+ type : "transformation" ,
1486+ value : {
1487+ imageUrl : "photo.jpg" ,
1488+ width : "13" ,
1489+ trimEnabled : true ,
1490+ trimThreshold : 10 ,
1491+ } ,
1492+ children : [
1493+ {
1494+ id : "c" ,
1495+ key : "ai-removedotbg" ,
1496+ name : "Remove Background" ,
1497+ type : "transformation" ,
1498+ value : { removedotbg : true } ,
1499+ } ,
1500+ ] ,
1501+ }
1502+ const url = buildSrc ( {
1503+ urlEndpoint : "https://ik.imagekit.io/demo" ,
1504+ src : "/base.jpg" ,
1505+ transformation : [ convertTransformationToIK ( parent ) ] ,
1506+ } )
1507+ expect ( url ) . toBe (
1508+ "https://ik.imagekit.io/demo/base.jpg?tr=l-image,i-photo.jpg,w-13,t-10:e-removedotbg,l-end" ,
1509+ )
1510+ } )
1511+
1512+ it ( "mixes non-layer and nested-layer children in declaration order" , async ( ) => {
1513+ const { buildSrc } = await import ( "@imagekit/javascript" )
1514+ const { convertTransformationToIK } = await import (
1515+ "./transformationConverter"
1516+ )
1517+ const parent : Transformation = {
1518+ id : "p" ,
1519+ key : "layers-image" ,
1520+ name : "Image Layer" ,
1521+ type : "transformation" ,
1522+ value : { imageUrl : "photo.jpg" } ,
1523+ children : [
1524+ {
1525+ id : "c1" ,
1526+ key : "adjust-blur" ,
1527+ name : "Blur" ,
1528+ type : "transformation" ,
1529+ value : { blur : 5 } ,
1530+ } ,
1531+ {
1532+ id : "c2" ,
1533+ key : "layers-text" ,
1534+ name : "Caption" ,
1535+ type : "transformation" ,
1536+ value : { text : "Sale" , radius : 0 } ,
1537+ } ,
1538+ ] ,
1539+ }
1540+ const url = buildSrc ( {
1541+ urlEndpoint : "https://ik.imagekit.io/demo" ,
1542+ src : "/base.jpg" ,
1543+ transformation : [ convertTransformationToIK ( parent ) ] ,
1544+ } )
1545+ expect ( url ) . toBe (
1546+ "https://ik.imagekit.io/demo/base.jpg?tr=l-image,i-photo.jpg,bl-5:l-text,i-Sale,r-0,l-end,l-end" ,
1547+ )
1548+ } )
1549+
1550+ it ( "hidden non-layer child is skipped from the URL" , async ( ) => {
1551+ const { buildSrc } = await import ( "@imagekit/javascript" )
1552+ const { convertTransformationToIK } = await import (
1553+ "./transformationConverter"
1554+ )
1555+ const parent : Transformation = {
1556+ id : "p" ,
1557+ key : "layers-image" ,
1558+ name : "Image Layer" ,
1559+ type : "transformation" ,
1560+ value : { imageUrl : "photo.jpg" } ,
1561+ children : [
1562+ {
1563+ id : "c1" ,
1564+ key : "adjust-blur" ,
1565+ name : "Blur" ,
1566+ type : "transformation" ,
1567+ value : { blur : 5 } ,
1568+ enabled : false ,
1569+ } ,
1570+ ] ,
1571+ }
1572+ const url = buildSrc ( {
1573+ urlEndpoint : "https://ik.imagekit.io/demo" ,
1574+ src : "/base.jpg" ,
1575+ transformation : [ convertTransformationToIK ( parent ) ] ,
1576+ } )
1577+ expect ( url ) . toBe (
1578+ "https://ik.imagekit.io/demo/base.jpg?tr=l-image,i-photo.jpg,l-end" ,
1579+ )
1580+ } )
1581+
1582+ it ( "isAllowedChildKey enforces per-parent allow lists" , async ( ) => {
1583+ const { isAllowedChildKey } = await import ( "./store" )
1584+
1585+ // Image layer: liberal allow list including AI + adjust + nested layers.
1586+ expect ( isAllowedChildKey ( "layers-image" , "ai-removedotbg" ) ) . toBe ( true )
1587+ expect ( isAllowedChildKey ( "layers-image" , "adjust-blur" ) ) . toBe ( true )
1588+ expect ( isAllowedChildKey ( "layers-image" , "layers-text" ) ) . toBe ( true )
1589+ // Delivery transforms are output-only; never valid inside a layer block.
1590+ expect ( isAllowedChildKey ( "layers-image" , "delivery-format" ) ) . toBe ( false )
1591+
1592+ // Canvas layer: tighter list (no blur/AI), but layers still allowed.
1593+ expect ( isAllowedChildKey ( "layers-canvas" , "adjust-radius" ) ) . toBe ( true )
1594+ expect ( isAllowedChildKey ( "layers-canvas" , "adjust-blur" ) ) . toBe ( false )
1595+ expect ( isAllowedChildKey ( "layers-canvas" , "ai-removedotbg" ) ) . toBe ( false )
1596+ expect ( isAllowedChildKey ( "layers-canvas" , "layers-image" ) ) . toBe ( true )
1597+
1598+ // Text layers are leaves: nothing is allowed, including other layers.
1599+ expect ( isAllowedChildKey ( "layers-text" , "adjust-blur" ) ) . toBe ( false )
1600+ expect ( isAllowedChildKey ( "layers-text" , "adjust-shadow" ) ) . toBe ( false )
1601+ expect ( isAllowedChildKey ( "layers-text" , "layers-image" ) ) . toBe ( true )
1602+ // ^ Note: the layer-keys short-circuit returns true here. The picker
1603+ // additionally gates on canHostLayerChildren, which excludes text.
1604+ } )
1605+
1606+ it ( "canHostLayerChildren only lets image/canvas host children" , async ( ) => {
1607+ const { canHostLayerChildren } = await import ( "./store" )
1608+ expect ( canHostLayerChildren ( "layers-image" ) ) . toBe ( true )
1609+ expect ( canHostLayerChildren ( "layers-canvas" ) ) . toBe ( true )
1610+ expect ( canHostLayerChildren ( "layers-text" ) ) . toBe ( false )
1611+ expect ( canHostLayerChildren ( "adjust-blur" ) ) . toBe ( false )
1612+ } )
1613+
1614+ it ( "getLayerDepth counts only layer ancestors, not non-layer ones" , async ( ) => {
1615+ const { getLayerDepth } = await import ( "./store" )
1616+ const tree : Transformation [ ] = [
1617+ {
1618+ id : "root" ,
1619+ key : "layers-image" ,
1620+ name : "Root" ,
1621+ type : "transformation" ,
1622+ value : { imageUrl : "a.png" } ,
1623+ children : [
1624+ {
1625+ // Non-layer child of root layer — itself at depth 0 (it has
1626+ // zero *layer* ancestors above its parent slot).
1627+ id : "blur" ,
1628+ key : "adjust-blur" ,
1629+ name : "Blur" ,
1630+ type : "transformation" ,
1631+ value : { blur : 4 } ,
1632+ } ,
1633+ {
1634+ // Nested layer — depth 1.
1635+ id : "child" ,
1636+ key : "layers-image" ,
1637+ name : "Child" ,
1638+ type : "transformation" ,
1639+ value : { imageUrl : "b.png" } ,
1640+ children : [
1641+ {
1642+ id : "grand" ,
1643+ key : "layers-text" ,
1644+ name : "Grand" ,
1645+ type : "transformation" ,
1646+ value : { text : "hi" , radius : 0 } ,
1647+ } ,
1648+ ] ,
1649+ } ,
1650+ ] ,
1651+ } ,
1652+ ]
1653+ expect ( getLayerDepth ( tree , "root" ) ) . toBe ( 0 )
1654+ // Non-layer children inherit the parent's depth (they don't open a
1655+ // new l-...,l-end scope).
1656+ expect ( getLayerDepth ( tree , "blur" ) ) . toBe ( 1 )
1657+ expect ( getLayerDepth ( tree , "child" ) ) . toBe ( 1 )
1658+ expect ( getLayerDepth ( tree , "grand" ) ) . toBe ( 2 )
1659+ expect ( getLayerDepth ( tree , "missing" ) ) . toBeUndefined ( )
1660+ } )
14711661 } )
14721662
14731663 describe ( "Resize & Crop Complex Validations" , ( ) => {
0 commit comments