PluginProbe
Tableberg – Simple Gutenberg Table Block / 1.1.5
Tableberg – Simple Gutenberg Table Block v1.1.5
1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 trunk 0.0.2 0.2.1 0.3.2 0.3.3 0.4.1 0.5.0 0.5.1 0.5.2 0.5.3 0.5.4 0.5.5 0.5.6 0.5.7 All 42 releases
tableberg / src / blocks / table / grid-model.test.ts

grid-model.test.ts in Tableberg – Simple Gutenberg Table Block 1.1.5, at src/blocks/table/grid-model.test.ts

248 lines 7.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import {
2 GridRow,
3 buildOccupancy,
4 cellColumn,
5 planDeleteColumn,
6 planDeleteRow,
7 planInsertColumn,
8 planInsertRow,
9 planMerge,
10 planSplit,
11 } from "./grid-model";
12
13 const cell = (id: string, rowSpan = 1, colSpan = 1) => ({
14 id,
15 rowSpan,
16 colSpan,
17 });
18
19 /**
20 * 3x3 with a 2x2 merged block "M" anchored at (0,0):
21 * M M a
22 * M M b
23 * c d e
24 */
25 const spanned: GridRow[] = [
26 [cell("M", 2, 2), cell("a")],
27 [cell("b")],
28 [cell("c"), cell("d"), cell("e")],
29 ];
30
31 const plain: GridRow[] = [
32 [cell("a1"), cell("a2"), cell("a3")],
33 [cell("b1"), cell("b2"), cell("b3")],
34 ];
35
36 describe("buildOccupancy", () => {
37 it("maps anchors and covered positions", () => {
38 const { matrix, anchors, cols } = buildOccupancy(spanned);
39
40 expect(cols).toBe(3);
41 expect(matrix[0]).toEqual(["M", "M", "a"]);
42 expect(matrix[1]).toEqual(["M", "M", "b"]);
43 expect(matrix[2]).toEqual(["c", "d", "e"]);
44 expect(anchors.get("b")).toEqual({ row: 1, col: 2 });
45 expect(anchors.get("d")).toEqual({ row: 2, col: 1 });
46 });
47
48 it("computes cellColumn through spans", () => {
49 expect(cellColumn(spanned, "b")).toBe(2);
50 expect(cellColumn(spanned, "e")).toBe(2);
51 });
52 });
53
54 describe("planInsertRow", () => {
55 it("plain: full-width new row", () => {
56 expect(planInsertRow(plain, 1)).toEqual({
57 growSpans: [],
58 newCellCount: 3,
59 });
60 });
61
62 it("grows spans crossing the line and shrinks the new row", () => {
63 // Between row0 and row1: M (rows 0-1) crosses.
64 expect(planInsertRow(spanned, 1)).toEqual({
65 growSpans: ["M"],
66 newCellCount: 1,
67 });
68 });
69
70 it("no crossing at outer edges", () => {
71 expect(planInsertRow(spanned, 0).growSpans).toEqual([]);
72 expect(planInsertRow(spanned, 3).growSpans).toEqual([]);
73 expect(planInsertRow(spanned, 0).newCellCount).toBe(3);
74 });
75 });
76
77 describe("planDeleteRow", () => {
78 it("plain: nothing special", () => {
79 expect(planDeleteRow(plain, 0)).toEqual({
80 shrinkSpans: [],
81 reanchor: [],
82 });
83 });
84
85 it("shrinks spans crossing the deleted row", () => {
86 // Deleting row 1: M crosses (anchored row 0, extends into row 1).
87 expect(planDeleteRow(spanned, 1)).toEqual({
88 shrinkSpans: ["M"],
89 reanchor: [],
90 });
91 });
92
93 it("re-anchors spans anchored in the deleted row", () => {
94 // Deleting row 0: M is anchored there with rowSpan 2 -> it must be
95 // recreated in row 1 (as first cell) with rowSpan 1.
96 expect(planDeleteRow(spanned, 0)).toEqual({
97 shrinkSpans: [],
98 reanchor: [
99 { id: "M", insertIndex: 0, rowSpan: 1, colSpan: 2 },
100 ],
101 });
102 });
103 });
104
105 describe("planInsertColumn", () => {
106 it("plain: inserts at the right array position in each row", () => {
107 expect(planInsertColumn(plain, 1)).toEqual([
108 { type: "insert", rowIndex: 0, insertIndex: 1 },
109 { type: "insert", rowIndex: 1, insertIndex: 1 },
110 ]);
111 });
112
113 it("grows a span crossed by the line, once, at its anchor row", () => {
114 // Line between col0 and col1 runs through M (cols 0-1).
115 expect(planInsertColumn(spanned, 1)).toEqual([
116 { type: "grow", rowIndex: 0, id: "M" },
117 { type: "skip", rowIndex: 1 },
118 { type: "insert", rowIndex: 2, insertIndex: 1 },
119 ]);
120 });
121
122 it("edge inserts never split spans", () => {
123 expect(planInsertColumn(spanned, 0)).toEqual([
124 { type: "insert", rowIndex: 0, insertIndex: 0 },
125 { type: "insert", rowIndex: 1, insertIndex: 0 },
126 { type: "insert", rowIndex: 2, insertIndex: 0 },
127 ]);
128 expect(planInsertColumn(spanned, 3)).toEqual([
129 { type: "insert", rowIndex: 0, insertIndex: 2 },
130 { type: "insert", rowIndex: 1, insertIndex: 1 },
131 { type: "insert", rowIndex: 2, insertIndex: 3 },
132 ]);
133 });
134 });
135
136 describe("planDeleteColumn", () => {
137 it("plain: removes one cell per row", () => {
138 expect(planDeleteColumn(plain, 1)).toEqual([
139 { type: "remove", rowIndex: 0, id: "a2" },
140 { type: "remove", rowIndex: 1, id: "b2" },
141 ]);
142 });
143
144 it("shrinks spans covering the column", () => {
145 expect(planDeleteColumn(spanned, 0)).toEqual([
146 { type: "shrink", rowIndex: 0, id: "M" },
147 { type: "skip", rowIndex: 1 },
148 { type: "remove", rowIndex: 2, id: "c" },
149 ]);
150 });
151 });
152
153 describe("planMerge", () => {
154 it("merges a clean rectangle", () => {
155 expect(planMerge(plain, ["a1", "a2"])).toEqual({
156 anchorId: "a1",
157 rowSpan: 1,
158 colSpan: 2,
159 absorbedIds: ["a2"],
160 });
161 });
162
163 it("merges vertically across rows", () => {
164 expect(planMerge(plain, ["a1", "b1"])).toEqual({
165 anchorId: "a1",
166 rowSpan: 2,
167 colSpan: 1,
168 absorbedIds: ["b1"],
169 });
170 });
171
172 it("merges an already-merged cell with neighbours tiling a rect", () => {
173 // M (2x2) + a + b = full 2x3 rect.
174 expect(planMerge(spanned, ["M", "a", "b"])).toEqual({
175 anchorId: "M",
176 rowSpan: 2,
177 colSpan: 3,
178 absorbedIds: ["a", "b"],
179 });
180 });
181
182 it("rejects non-rectangular selections", () => {
183 // L-shape: a1 + a2 + b1
184 expect(planMerge(plain, ["a1", "a2", "b1"])).toBeNull();
185 // M + c does not tile a rectangle.
186 expect(planMerge(spanned, ["M", "c"])).toBeNull();
187 });
188
189 it("rejects fewer than two cells", () => {
190 expect(planMerge(plain, ["a1"])).toBeNull();
191 });
192 });
193
194 describe("planSplit", () => {
195 it("splits a merged cell into 1x1 positions", () => {
196 expect(planSplit(spanned, "M")).toEqual({
197 inserts: [
198 { rowIndex: 0, insertIndex: 1, count: 1 },
199 { rowIndex: 1, insertIndex: 0, count: 2 },
200 ],
201 });
202 });
203
204 it("returns null for 1x1 cells", () => {
205 expect(planSplit(plain, "a1")).toBeNull();
206 });
207 });
208
209 describe("round-trips", () => {
210 it("insert row then occupancy stays consistent", () => {
211 const plan = planInsertRow(spanned, 1);
212 // Simulate: M grows to rowSpan 3, new row with 1 cell inserted at 1.
213 const grown: GridRow[] = [
214 [cell("M", 3, 2), cell("a")],
215 [cell("new1")],
216 [cell("b")],
217 [cell("c"), cell("d"), cell("e")],
218 ];
219 expect(plan.newCellCount).toBe(1);
220 const { matrix, cols } = buildOccupancy(grown);
221 expect(cols).toBe(3);
222 expect(matrix[1]).toEqual(["M", "M", "new1"]);
223 expect(matrix[2]).toEqual(["M", "M", "b"]);
224 });
225
226 it("merge then split restores full occupancy", () => {
227 const merge = planMerge(plain, ["a1", "a2", "b1", "b2"]);
228 expect(merge).toEqual({
229 anchorId: "a1",
230 rowSpan: 2,
231 colSpan: 2,
232 absorbedIds: ["a2", "b1", "b2"],
233 });
234
235 const merged: GridRow[] = [
236 [cell("a1", 2, 2), cell("a3")],
237 [cell("b3")],
238 ];
239 const split = planSplit(merged, "a1");
240 expect(split).toEqual({
241 inserts: [
242 { rowIndex: 0, insertIndex: 1, count: 1 },
243 { rowIndex: 1, insertIndex: 0, count: 2 },
244 ],
245 });
246 });
247 });
248