@elementor/editor-canvas 4.2.0-911 → 4.2.0-912

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,7 +36,7 @@ describe( '<GridOutline />', () => {
36
36
  expect( svg ).toHaveAttribute( 'height', '200' );
37
37
  } );
38
38
 
39
- it( 'draws N+M boundary lines for an N×M grid (no gaps)', () => {
39
+ it( 'draws one rect per cell for an N×M grid', () => {
40
40
  const { container } = renderWithTheme(
41
41
  <GridOutline
42
42
  tracks={ makeTracks( { columns: [ 100, 100, 100 ], rows: [ 80, 80 ] } ) }
@@ -45,10 +45,10 @@ describe( '<GridOutline />', () => {
45
45
  />
46
46
  );
47
47
 
48
- expect( container.querySelectorAll( 'line' ) ).toHaveLength( 4 + 3 );
48
+ expect( container.querySelectorAll( 'rect' ) ).toHaveLength( 3 * 2 );
49
49
  } );
50
50
 
51
- it( 'emits both edges of every gap between tracks', () => {
51
+ it( 'keeps the cell count stable with a gap and offsets cells past the gap', () => {
52
52
  const { container } = renderWithTheme(
53
53
  <GridOutline
54
54
  tracks={ makeTracks( {
@@ -62,14 +62,21 @@ describe( '<GridOutline />', () => {
62
62
  />
63
63
  );
64
64
 
65
- expect( container.querySelectorAll( 'line' ) ).toHaveLength( 6 + 4 );
65
+ const rects = Array.from( container.querySelectorAll( 'rect' ) );
66
+ expect( rects ).toHaveLength( 3 * 2 );
67
+
68
+ const xs = rects.map( ( rect ) => rect.getAttribute( 'x' ) );
69
+ expect( xs ).toContain( '0.5' );
70
+ expect( xs ).toContain( '110.5' );
71
+ expect( xs ).toContain( '220.5' );
66
72
  } );
67
73
 
68
- it( 'snaps vertical line coordinates to half pixels for crisp 1px strokes', () => {
74
+ it( 'snaps cell coordinates to half pixels for crisp 1px strokes', () => {
69
75
  const { container } = renderWithTheme(
70
76
  <GridOutline
71
77
  tracks={ makeTracks( {
72
78
  columns: [ 100, 100 ],
79
+ rows: [ 80 ],
73
80
  padding: { top: 10, right: 10, bottom: 10, left: 10 },
74
81
  } ) }
75
82
  width={ 220 }
@@ -77,14 +84,11 @@ describe( '<GridOutline />', () => {
77
84
  />
78
85
  );
79
86
 
80
- const verticals = Array.from( container.querySelectorAll( 'line' ) ).filter(
81
- ( line ) => line.getAttribute( 'x1' ) === line.getAttribute( 'x2' )
82
- );
83
-
84
- expect( verticals.map( ( line ) => line.getAttribute( 'x1' ) ) ).toEqual( [ '10.5', '110.5', '210.5' ] );
87
+ const xs = Array.from( container.querySelectorAll( 'rect' ) ).map( ( rect ) => rect.getAttribute( 'x' ) );
88
+ expect( xs ).toEqual( [ '10.5', '110.5' ] );
85
89
  } );
86
90
 
87
- it( 'spans horizontal lines across the padded content rect', () => {
91
+ it( 'spans a single full-width cell per row when only rows are defined', () => {
88
92
  const { container } = renderWithTheme(
89
93
  <GridOutline
90
94
  tracks={ makeTracks( {
@@ -96,36 +100,33 @@ describe( '<GridOutline />', () => {
96
100
  />
97
101
  );
98
102
 
99
- const horizontals = Array.from( container.querySelectorAll( 'line' ) ).filter(
100
- ( line ) => line.getAttribute( 'y1' ) === line.getAttribute( 'y2' )
101
- );
102
-
103
- expect( horizontals.length ).toBeGreaterThan( 0 );
104
- for ( const line of horizontals ) {
105
- expect( line ).toHaveAttribute( 'x1', '4' );
106
- expect( line ).toHaveAttribute( 'x2', '288' );
103
+ const rects = Array.from( container.querySelectorAll( 'rect' ) );
104
+ expect( rects ).toHaveLength( 2 );
105
+ for ( const rect of rects ) {
106
+ expect( rect ).toHaveAttribute( 'x', '4.5' );
107
+ expect( rect ).toHaveAttribute( 'width', '284' );
107
108
  }
108
109
  } );
109
110
 
110
- it( 'passes the resolved iframe border color through to each line', () => {
111
+ it( 'passes the resolved iframe border color through to each cell', () => {
111
112
  const { container } = renderWithTheme(
112
113
  <GridOutline
113
- tracks={ makeTracks( { columns: [ 100 ], borderColor: '#abcdef' } ) }
114
+ tracks={ makeTracks( { columns: [ 100 ], rows: [ 100 ], borderColor: '#abcdef' } ) }
114
115
  width={ 100 }
115
116
  height={ 100 }
116
117
  />
117
118
  );
118
119
 
119
- const lines = container.querySelectorAll( 'line' );
120
- expect( lines.length ).toBeGreaterThan( 0 );
121
- lines.forEach( ( line ) => {
122
- expect( line ).toHaveAttribute( 'stroke', '#abcdef' );
120
+ const rects = container.querySelectorAll( 'rect' );
121
+ expect( rects.length ).toBeGreaterThan( 0 );
122
+ rects.forEach( ( rect ) => {
123
+ expect( rect ).toHaveAttribute( 'stroke', '#abcdef' );
123
124
  } );
124
125
  } );
125
126
 
126
- it( 'renders no lines when there are no tracks on either axis', () => {
127
+ it( 'renders no cells when there are no tracks on either axis', () => {
127
128
  const { container } = renderWithTheme( <GridOutline tracks={ makeTracks() } width={ 100 } height={ 100 } /> );
128
129
 
129
- expect( container.querySelectorAll( 'line' ) ).toHaveLength( 0 );
130
+ expect( container.querySelectorAll( 'rect' ) ).toHaveLength( 0 );
130
131
  } );
131
132
  } );
@@ -4,20 +4,21 @@ const FALLBACK_COLOR = 'rgba(0, 0, 0, 0.12)';
4
4
  export const DASH = '2 2';
5
5
 
6
6
  type Props = {
7
- x1: number;
8
- x2: number;
9
- y1: number;
10
- y2: number;
7
+ x: number;
8
+ y: number;
9
+ width: number;
10
+ height: number;
11
11
  color?: string;
12
12
  };
13
13
 
14
- export function GridOutlineLine( { x1, x2, y1, y2, color }: Props ) {
14
+ export function GridOutlineCell( { x, y, width, height, color }: Props ) {
15
15
  return (
16
- <line
17
- x1={ x1 }
18
- x2={ x2 }
19
- y1={ y1 }
20
- y2={ y2 }
16
+ <rect
17
+ x={ x }
18
+ y={ y }
19
+ width={ width }
20
+ height={ height }
21
+ fill="none"
21
22
  stroke={ color || FALLBACK_COLOR }
22
23
  strokeWidth={ 1 }
23
24
  strokeDasharray={ DASH }
@@ -1,8 +1,8 @@
1
1
  import * as React from 'react';
2
2
 
3
3
  import { type GridTracks } from '../../hooks/use-grid-tracks';
4
- import { computeOutlineGeometry, snapToHalfPixel } from '../../utils/grid-outline-utils';
5
- import { GridOutlineLine } from './grid-outline-line';
4
+ import { computeCellRects, snapToHalfPixel } from '../../utils/grid-outline-utils';
5
+ import { GridOutlineCell } from './grid-outline-cell';
6
6
 
7
7
  type Props = {
8
8
  tracks: GridTracks;
@@ -11,7 +11,7 @@ type Props = {
11
11
  };
12
12
 
13
13
  export function GridOutline( { tracks, width, height }: Props ) {
14
- const { vertical, horizontal, top, bottom, left, right } = computeOutlineGeometry( tracks, width, height );
14
+ const cells = computeCellRects( tracks, width, height );
15
15
 
16
16
  return (
17
17
  <svg
@@ -20,23 +20,13 @@ export function GridOutline( { tracks, width, height }: Props ) {
20
20
  style={ { position: 'absolute', inset: 0, overflow: 'visible' } }
21
21
  xmlns="http://www.w3.org/2000/svg"
22
22
  >
23
- { vertical.map( ( x, i ) => (
24
- <GridOutlineLine
25
- key={ `v-${ i }` }
26
- x1={ snapToHalfPixel( x ) }
27
- x2={ snapToHalfPixel( x ) }
28
- y1={ top }
29
- y2={ bottom }
30
- color={ tracks.borderColor }
31
- />
32
- ) ) }
33
- { horizontal.map( ( y, i ) => (
34
- <GridOutlineLine
35
- key={ `h-${ i }` }
36
- x1={ left }
37
- x2={ right }
38
- y1={ snapToHalfPixel( y ) }
39
- y2={ snapToHalfPixel( y ) }
23
+ { cells.map( ( cell, i ) => (
24
+ <GridOutlineCell
25
+ key={ i }
26
+ x={ snapToHalfPixel( cell.x ) }
27
+ y={ snapToHalfPixel( cell.y ) }
28
+ width={ Math.round( cell.width ) }
29
+ height={ Math.round( cell.height ) }
40
30
  color={ tracks.borderColor }
41
31
  />
42
32
  ) ) }
@@ -1,4 +1,6 @@
1
- import { renderHook } from '@testing-library/react';
1
+ import { dispatchWindowEvent } from 'test-utils';
2
+ import { ELEMENT_STYLE_CHANGE_EVENT } from '@elementor/editor-elements';
3
+ import { act, renderHook } from '@testing-library/react';
2
4
 
3
5
  import { useGridTracks } from '../use-grid-tracks';
4
6
 
@@ -26,9 +28,12 @@ const DEFAULT_STYLE: Style = {
26
28
  '--e-a-border-color-bold': '',
27
29
  };
28
30
 
29
- function mockElement( style: Partial< Style > = {} ): HTMLElement {
31
+ const DEVICE_MODE_CHANGE_EVENT = 'elementor/device-mode/change';
32
+
33
+ function mockElement( style: Partial< Style > = {} ) {
30
34
  const resolved: Style = { ...DEFAULT_STYLE, ...style };
31
- const getComputedStyle = jest.fn().mockReturnValue( {
35
+
36
+ const getComputedStyle = jest.fn().mockImplementation( () => ( {
32
37
  gridTemplateColumns: resolved.gridTemplateColumns,
33
38
  gridTemplateRows: resolved.gridTemplateRows,
34
39
  columnGap: resolved.columnGap,
@@ -38,13 +43,22 @@ function mockElement( style: Partial< Style > = {} ): HTMLElement {
38
43
  paddingBottom: resolved.paddingBottom,
39
44
  paddingLeft: resolved.paddingLeft,
40
45
  getPropertyValue: ( name: string ) => ( name === '--e-a-border-color-bold' ? resolved[ name ] : '' ),
41
- } );
46
+ } ) );
42
47
 
43
- return {
48
+ const element = {
44
49
  ownerDocument: {
45
- defaultView: { getComputedStyle } as unknown as Window,
50
+ defaultView: {
51
+ getComputedStyle,
52
+ requestAnimationFrame: ( cb: FrameRequestCallback ) => {
53
+ cb( 0 );
54
+ return 1;
55
+ },
56
+ cancelAnimationFrame: jest.fn(),
57
+ } as unknown as Window,
46
58
  },
47
59
  } as unknown as HTMLElement;
60
+
61
+ return { element, resolved, getComputedStyle };
48
62
  }
49
63
 
50
64
  const RECT = new DOMRect( 0, 0, 320, 200 );
@@ -73,7 +87,7 @@ describe( 'useGridTracks', () => {
73
87
  } );
74
88
 
75
89
  it( 'parses resolved track lists, gaps, and padding from computed style', () => {
76
- const element = mockElement( {
90
+ const { element } = mockElement( {
77
91
  gridTemplateColumns: '100px 100px 100px',
78
92
  gridTemplateRows: '80px 80px',
79
93
  columnGap: '10px',
@@ -96,33 +110,8 @@ describe( 'useGridTracks', () => {
96
110
  } );
97
111
  } );
98
112
 
99
- it( 'reads the --e-a-border-color-bold CSS variable from the iframe', () => {
100
- const element = mockElement( {
101
- gridTemplateColumns: '100px',
102
- '--e-a-border-color-bold': ' #d5d8dc ',
103
- } );
104
-
105
- const { result } = renderHook( () => useGridTracks( element, RECT ) );
106
-
107
- expect( result.current.borderColor ).toBe( '#d5d8dc' );
108
- } );
109
-
110
- it( 'reports gap as 0 when computed style returns "normal"', () => {
111
- const element = mockElement( {
112
- gridTemplateColumns: '100px 100px',
113
- columnGap: 'normal',
114
- rowGap: 'normal',
115
- } );
116
-
117
- const { result } = renderHook( () => useGridTracks( element, RECT ) );
118
-
119
- expect( result.current.columnGap ).toBe( 0 );
120
- expect( result.current.rowGap ).toBe( 0 );
121
- } );
122
-
123
113
  it( 'recomputes when the rect dimensions change', () => {
124
- const element = mockElement( { gridTemplateColumns: '100px' } );
125
- const getComputedStyle = element.ownerDocument?.defaultView?.getComputedStyle as jest.Mock;
114
+ const { element, getComputedStyle } = mockElement( { gridTemplateColumns: '100px' } );
126
115
 
127
116
  const { rerender } = renderHook( ( { rect } ) => useGridTracks( element, rect ), {
128
117
  initialProps: { rect: new DOMRect( 0, 0, 320, 200 ) },
@@ -136,8 +125,7 @@ describe( 'useGridTracks', () => {
136
125
  } );
137
126
 
138
127
  it( 'does not recompute when the same rect dimensions are passed again', () => {
139
- const element = mockElement( { gridTemplateColumns: '100px' } );
140
- const getComputedStyle = element.ownerDocument?.defaultView?.getComputedStyle as jest.Mock;
128
+ const { element, getComputedStyle } = mockElement( { gridTemplateColumns: '100px' } );
141
129
 
142
130
  const { rerender } = renderHook( ( { rect } ) => useGridTracks( element, rect ), {
143
131
  initialProps: { rect: new DOMRect( 0, 0, 320, 200 ) },
@@ -149,4 +137,38 @@ describe( 'useGridTracks', () => {
149
137
 
150
138
  expect( getComputedStyle.mock.calls.length ).toBe( callsBefore );
151
139
  } );
140
+
141
+ it( 'recomputes when a grid style change event fires', () => {
142
+ const mock = mockElement( { gridTemplateColumns: '100px 100px' } );
143
+
144
+ const { result } = renderHook( () => useGridTracks( mock.element, RECT ) );
145
+
146
+ expect( result.current.columns ).toEqual( [ 100, 100 ] );
147
+
148
+ mock.resolved.gridTemplateColumns = '100px 100px 100px 100px';
149
+
150
+ act( () => {
151
+ dispatchWindowEvent( ELEMENT_STYLE_CHANGE_EVENT );
152
+ } );
153
+
154
+ expect( result.current.columns ).toEqual( [ 100, 100, 100, 100 ] );
155
+ } );
156
+
157
+ it( 'recomputes when the device mode changes', () => {
158
+ const mock = mockElement( { gridTemplateRows: '80px', rowGap: '8px' } );
159
+
160
+ const { result } = renderHook( () => useGridTracks( mock.element, RECT ) );
161
+
162
+ expect( result.current.rows ).toEqual( [ 80 ] );
163
+
164
+ mock.resolved.gridTemplateRows = '80px 80px';
165
+ mock.resolved.rowGap = '12px';
166
+
167
+ act( () => {
168
+ dispatchWindowEvent( DEVICE_MODE_CHANGE_EVENT );
169
+ } );
170
+
171
+ expect( result.current.rows ).toEqual( [ 80, 80 ] );
172
+ expect( result.current.rowGap ).toBe( 12 );
173
+ } );
152
174
  } );
@@ -1,6 +1,8 @@
1
- import { useMemo } from 'react';
1
+ import { useEffect, useState } from 'react';
2
+ import { ELEMENT_STYLE_CHANGE_EVENT } from '@elementor/editor-elements';
3
+ import { __privateUseListenTo as useListenTo, windowEvent } from '@elementor/editor-v1-adapters';
2
4
 
3
- import { parseTrackList, toPx } from '../utils/grid-outline-utils';
5
+ import { toGridTracks } from '../utils/grid-outline-utils';
4
6
 
5
7
  export type GridTracks = {
6
8
  columns: number[];
@@ -20,33 +22,32 @@ const EMPTY: GridTracks = {
20
22
  borderColor: '',
21
23
  };
22
24
 
25
+ const DEVICE_MODE_CHANGE_EVENT = 'elementor/device-mode/change';
26
+
23
27
  export function useGridTracks( element: HTMLElement | null, rect: DOMRect ): GridTracks {
24
- return useMemo( () => {
25
- if ( ! element ) {
26
- return EMPTY;
27
- }
28
+ const [ tracks, setTracks ] = useState< GridTracks >( EMPTY );
29
+
30
+ const trigger = useListenTo(
31
+ [ windowEvent( ELEMENT_STYLE_CHANGE_EVENT ), windowEvent( DEVICE_MODE_CHANGE_EVENT ) ],
32
+ () => ( {} )
33
+ );
28
34
 
29
- const previewWindow = element.ownerDocument?.defaultView;
35
+ useEffect( () => {
36
+ const previewWindow = element?.ownerDocument?.defaultView;
30
37
 
31
- if ( ! previewWindow ) {
32
- return EMPTY;
38
+ if ( ! element || ! previewWindow ) {
39
+ setTracks( EMPTY );
40
+ return;
33
41
  }
34
42
 
35
- const computedStyle = previewWindow.getComputedStyle( element );
36
-
37
- return {
38
- columns: parseTrackList( computedStyle.gridTemplateColumns ),
39
- rows: parseTrackList( computedStyle.gridTemplateRows ),
40
- columnGap: toPx( computedStyle.columnGap ),
41
- rowGap: toPx( computedStyle.rowGap ),
42
- padding: {
43
- top: toPx( computedStyle.paddingTop ),
44
- right: toPx( computedStyle.paddingRight ),
45
- bottom: toPx( computedStyle.paddingBottom ),
46
- left: toPx( computedStyle.paddingLeft ),
47
- },
48
- borderColor: computedStyle.getPropertyValue( '--e-a-border-color-bold' ).trim(),
43
+ const frame = previewWindow.requestAnimationFrame( () => {
44
+ setTracks( toGridTracks( previewWindow.getComputedStyle( element ) ) );
45
+ } );
46
+
47
+ return () => {
48
+ previewWindow.cancelAnimationFrame( frame );
49
49
  };
50
- // eslint-disable-next-line react-hooks/exhaustive-deps
51
- }, [ element, rect.width, rect.height ] );
50
+ }, [ element, rect.width, rect.height, trigger ] );
51
+
52
+ return tracks;
52
53
  }
@@ -1,5 +1,5 @@
1
1
  import { type GridTracks } from '../../hooks/use-grid-tracks';
2
- import { computeOutlineGeometry, parseTrackList, snapToHalfPixel, toPx } from '../grid-outline-utils';
2
+ import { computeCellRects, parseTrackList, snapToHalfPixel, toGridTracks, toPx } from '../grid-outline-utils';
3
3
 
4
4
  function makeTracks( partial: Partial< GridTracks > = {} ): GridTracks {
5
5
  return {
@@ -13,74 +13,77 @@ function makeTracks( partial: Partial< GridTracks > = {} ): GridTracks {
13
13
  };
14
14
  }
15
15
 
16
- describe( 'computeOutlineGeometry', () => {
17
- it( 'returns empty boundary lists when there are no tracks', () => {
18
- const geometry = computeOutlineGeometry( makeTracks(), 100, 100 );
19
-
20
- expect( geometry.vertical ).toEqual( [] );
21
- expect( geometry.horizontal ).toEqual( [] );
16
+ describe( 'computeCellRects', () => {
17
+ it( 'returns no cells when there are no tracks on either axis', () => {
18
+ expect( computeCellRects( makeTracks(), 100, 100 ) ).toEqual( [] );
22
19
  } );
23
20
 
24
- it( 'shifts boundaries by the padding offset on each axis', () => {
21
+ it( 'produces one rect per cell offset by the padding', () => {
25
22
  const tracks = makeTracks( {
26
23
  columns: [ 100, 100 ],
27
24
  rows: [ 80, 80 ],
28
25
  padding: { top: 5, right: 0, bottom: 0, left: 20 },
29
26
  } );
30
27
 
31
- const geometry = computeOutlineGeometry( tracks, 300, 300 );
32
-
33
- expect( geometry.vertical ).toEqual( [ 20, 120, 220 ] );
34
- expect( geometry.horizontal ).toEqual( [ 5, 85, 165 ] );
28
+ expect( computeCellRects( tracks, 300, 300 ) ).toEqual( [
29
+ { x: 20, y: 5, width: 100, height: 80 },
30
+ { x: 120, y: 5, width: 100, height: 80 },
31
+ { x: 20, y: 85, width: 100, height: 80 },
32
+ { x: 120, y: 85, width: 100, height: 80 },
33
+ ] );
35
34
  } );
36
35
 
37
- it( 'emits both edges of every gap between tracks', () => {
36
+ it( 'separates cells by the gap so the gap stays empty', () => {
38
37
  const tracks = makeTracks( {
39
38
  columns: [ 100, 100, 100 ],
39
+ rows: [ 80 ],
40
40
  columnGap: 10,
41
41
  } );
42
42
 
43
- const geometry = computeOutlineGeometry( tracks, 400, 100 );
44
-
45
- // 0 — 100 [gap 10] 110 210 [gap 10] 220 320
46
- expect( geometry.vertical ).toEqual( [ 0, 100, 110, 210, 220, 320 ] );
47
- } );
48
-
49
- it( 'collapses gap boundaries when the gap is zero', () => {
50
- const tracks = makeTracks( {
51
- rows: [ 50, 50, 50 ],
52
- rowGap: 0,
53
- } );
54
-
55
- const geometry = computeOutlineGeometry( tracks, 100, 200 );
56
-
57
- expect( geometry.horizontal ).toEqual( [ 0, 50, 100, 150 ] );
43
+ expect( computeCellRects( tracks, 400, 100 ) ).toEqual( [
44
+ { x: 0, y: 0, width: 100, height: 80 },
45
+ { x: 110, y: 0, width: 100, height: 80 },
46
+ { x: 220, y: 0, width: 100, height: 80 },
47
+ ] );
58
48
  } );
59
49
 
60
50
  it( 'handles uneven track sizes', () => {
61
51
  const tracks = makeTracks( {
62
52
  columns: [ 100, 200, 100 ],
53
+ rows: [ 50 ],
63
54
  columnGap: 5,
64
55
  padding: { top: 0, right: 0, bottom: 0, left: 10 },
65
56
  } );
66
57
 
67
- const geometry = computeOutlineGeometry( tracks, 500, 100 );
58
+ expect( computeCellRects( tracks, 500, 100 ) ).toEqual( [
59
+ { x: 10, y: 0, width: 100, height: 50 },
60
+ { x: 115, y: 0, width: 200, height: 50 },
61
+ { x: 320, y: 0, width: 100, height: 50 },
62
+ ] );
63
+ } );
68
64
 
69
- // 10 110 [5] 115 315 [5] 320 420
70
- expect( geometry.vertical ).toEqual( [ 10, 110, 115, 315, 320, 420 ] );
65
+ it( 'spans a single full-width cell per row when only rows are defined', () => {
66
+ const tracks = makeTracks( {
67
+ rows: [ 50, 50 ],
68
+ padding: { top: 8, right: 12, bottom: 6, left: 4 },
69
+ } );
70
+
71
+ expect( computeCellRects( tracks, 300, 120 ) ).toEqual( [
72
+ { x: 4, y: 8, width: 284, height: 50 },
73
+ { x: 4, y: 58, width: 284, height: 50 },
74
+ ] );
71
75
  } );
72
76
 
73
- it( 'derives the content rect from element size and padding', () => {
77
+ it( 'spans a single full-height cell per column when only columns are defined', () => {
74
78
  const tracks = makeTracks( {
79
+ columns: [ 100, 100 ],
75
80
  padding: { top: 5, right: 8, bottom: 12, left: 20 },
76
81
  } );
77
82
 
78
- const geometry = computeOutlineGeometry( tracks, 300, 200 );
79
-
80
- expect( geometry.top ).toBe( 5 );
81
- expect( geometry.left ).toBe( 20 );
82
- expect( geometry.right ).toBe( 292 );
83
- expect( geometry.bottom ).toBe( 188 );
83
+ expect( computeCellRects( tracks, 300, 200 ) ).toEqual( [
84
+ { x: 20, y: 5, width: 100, height: 183 },
85
+ { x: 120, y: 5, width: 100, height: 183 },
86
+ ] );
84
87
  } );
85
88
  } );
86
89
 
@@ -140,3 +143,90 @@ describe( 'toPx', () => {
140
143
  expect( toPx( input ) ).toBe( expected );
141
144
  } );
142
145
  } );
146
+
147
+ type ComputedStyleParts = {
148
+ gridTemplateColumns: string;
149
+ gridTemplateRows: string;
150
+ columnGap: string;
151
+ rowGap: string;
152
+ paddingTop: string;
153
+ paddingRight: string;
154
+ paddingBottom: string;
155
+ paddingLeft: string;
156
+ borderColor: string;
157
+ };
158
+
159
+ function mockComputedStyle( parts: Partial< ComputedStyleParts > = {} ): CSSStyleDeclaration {
160
+ const resolved: ComputedStyleParts = {
161
+ gridTemplateColumns: 'none',
162
+ gridTemplateRows: 'none',
163
+ columnGap: 'normal',
164
+ rowGap: 'normal',
165
+ paddingTop: '0px',
166
+ paddingRight: '0px',
167
+ paddingBottom: '0px',
168
+ paddingLeft: '0px',
169
+ borderColor: '',
170
+ ...parts,
171
+ };
172
+
173
+ return {
174
+ gridTemplateColumns: resolved.gridTemplateColumns,
175
+ gridTemplateRows: resolved.gridTemplateRows,
176
+ columnGap: resolved.columnGap,
177
+ rowGap: resolved.rowGap,
178
+ paddingTop: resolved.paddingTop,
179
+ paddingRight: resolved.paddingRight,
180
+ paddingBottom: resolved.paddingBottom,
181
+ paddingLeft: resolved.paddingLeft,
182
+ getPropertyValue: ( name: string ) => ( name === '--e-a-border-color-bold' ? resolved.borderColor : '' ),
183
+ } as unknown as CSSStyleDeclaration;
184
+ }
185
+
186
+ describe( 'toGridTracks', () => {
187
+ it( 'parses resolved track lists, gaps, and padding from computed style', () => {
188
+ const tracks = toGridTracks(
189
+ mockComputedStyle( {
190
+ gridTemplateColumns: '100px 100px 100px',
191
+ gridTemplateRows: '80px 80px',
192
+ columnGap: '10px',
193
+ rowGap: '8px',
194
+ paddingTop: '5px',
195
+ paddingRight: '6px',
196
+ paddingBottom: '7px',
197
+ paddingLeft: '8px',
198
+ } )
199
+ );
200
+
201
+ expect( tracks ).toEqual( {
202
+ columns: [ 100, 100, 100 ],
203
+ rows: [ 80, 80 ],
204
+ columnGap: 10,
205
+ rowGap: 8,
206
+ padding: { top: 5, right: 6, bottom: 7, left: 8 },
207
+ borderColor: '',
208
+ } );
209
+ } );
210
+
211
+ it( 'returns empty track lists when the template is "none"', () => {
212
+ const tracks = toGridTracks( mockComputedStyle() );
213
+
214
+ expect( tracks.columns ).toEqual( [] );
215
+ expect( tracks.rows ).toEqual( [] );
216
+ } );
217
+
218
+ it( 'reports gap as 0 when computed style returns "normal"', () => {
219
+ const tracks = toGridTracks(
220
+ mockComputedStyle( { gridTemplateColumns: '100px 100px', columnGap: 'normal', rowGap: 'normal' } )
221
+ );
222
+
223
+ expect( tracks.columnGap ).toBe( 0 );
224
+ expect( tracks.rowGap ).toBe( 0 );
225
+ } );
226
+
227
+ it( 'trims the --e-a-border-color-bold CSS variable', () => {
228
+ const tracks = toGridTracks( mockComputedStyle( { borderColor: ' #d5d8dc ' } ) );
229
+
230
+ expect( tracks.borderColor ).toBe( '#d5d8dc' );
231
+ } );
232
+ } );