Skip to content

Commit 5c6fa62

Browse files
authored
feat: builder template regression detection (baserow#5348)
* Adding a builder template regression detection system. See docs/testing/builder-template-regression.md for more information. * documentation update * Address initial feedback * Addressing Jrmi's feedback.
1 parent 409721d commit 5c6fa62

163 files changed

Lines changed: 35643 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ dev = [
152152
"ipdb",
153153
"build",
154154
"rust-just>=1.46.0",
155+
"syrupy==5.1.0",
155156
]
156157
changelog = [
157158
"typer==0.24.1",

backend/src/baserow/contrib/builder/application_types.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,3 +579,33 @@ def fetch_pages_to_serialize(
579579
else:
580580
instance = self.enhance_queryset(base_queryset).first()
581581
return instance and list(instance.page_set.all()) or []
582+
583+
def serialize_for_regression_testing(self, builder: Builder) -> dict:
584+
"""
585+
Serializes each page's element tree as a list of ``{type, children}`` nodes,
586+
keyed by page name. Used by snapshot tests to detect element-hierarchy
587+
regressions across template changes.
588+
589+
:param builder: The builder application instance to serialize.
590+
:return: A dict mapping page names to their element-tree representation.
591+
"""
592+
from baserow.contrib.builder.elements.handler import ElementHandler
593+
from baserow.contrib.builder.pages.handler import PageHandler
594+
595+
result = {}
596+
for page in PageHandler().get_pages(builder):
597+
elements = list(ElementHandler().get_elements(page))
598+
# Elements are already ordered by (order, id) via Element.Meta.ordering.
599+
by_parent: dict[int | None, list] = {}
600+
for el in elements:
601+
by_parent.setdefault(el.parent_element_id, []).append(el)
602+
603+
def build_tree(parent_id):
604+
return [
605+
{"type": el.get_type().type, "children": build_tree(el.id)}
606+
for el in by_parent.get(parent_id, [])
607+
]
608+
609+
result[page.name] = build_tree(None)
610+
611+
return result

backend/src/baserow/core/registries.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,21 @@ def get_application_id_for_url(cls, url: str) -> int | None:
627627

628628
return None
629629

630+
def serialize_for_regression_testing(
631+
self, application: "Application"
632+
) -> dict | None:
633+
"""
634+
Optionally serialize the application state for regression snapshot testing.
635+
636+
Override in subclasses to capture a human-readable, ID-free representation of
637+
the application structure. Return None to opt this application type out.
638+
639+
:param application: The specific application instance.
640+
:return: A serializable dict, or None to skip this application type.
641+
"""
642+
643+
return None
644+
630645

631646
ApplicationSubClassInstance = TypeVar(
632647
"ApplicationSubClassInstance", bound="Application"

backend/tests/baserow/core/templates/__init__.py

Whitespace-only changes.
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
# serializer version: 1
2+
# name: test_template_snapshot[ab-testing]
3+
dict({
4+
'builder': dict({
5+
'Homepage': list([
6+
dict({
7+
'children': list([
8+
]),
9+
'type': 'heading',
10+
}),
11+
dict({
12+
'children': list([
13+
]),
14+
'type': 'table',
15+
}),
16+
]),
17+
'Variant details': list([
18+
dict({
19+
'children': list([
20+
]),
21+
'type': 'link',
22+
}),
23+
dict({
24+
'children': list([
25+
]),
26+
'type': 'heading',
27+
}),
28+
dict({
29+
'children': list([
30+
dict({
31+
'children': list([
32+
]),
33+
'type': 'text',
34+
}),
35+
dict({
36+
'children': list([
37+
]),
38+
'type': 'text',
39+
}),
40+
dict({
41+
'children': list([
42+
]),
43+
'type': 'text',
44+
}),
45+
dict({
46+
'children': list([
47+
]),
48+
'type': 'text',
49+
}),
50+
dict({
51+
'children': list([
52+
]),
53+
'type': 'text',
54+
}),
55+
dict({
56+
'children': list([
57+
]),
58+
'type': 'link',
59+
}),
60+
dict({
61+
'children': list([
62+
]),
63+
'type': 'text',
64+
}),
65+
dict({
66+
'children': list([
67+
]),
68+
'type': 'text',
69+
}),
70+
dict({
71+
'children': list([
72+
]),
73+
'type': 'text',
74+
}),
75+
dict({
76+
'children': list([
77+
]),
78+
'type': 'text',
79+
}),
80+
dict({
81+
'children': list([
82+
]),
83+
'type': 'text',
84+
}),
85+
dict({
86+
'children': list([
87+
]),
88+
'type': 'text',
89+
}),
90+
dict({
91+
'children': list([
92+
]),
93+
'type': 'text',
94+
}),
95+
dict({
96+
'children': list([
97+
]),
98+
'type': 'text',
99+
}),
100+
dict({
101+
'children': list([
102+
]),
103+
'type': 'text',
104+
}),
105+
dict({
106+
'children': list([
107+
]),
108+
'type': 'text',
109+
}),
110+
dict({
111+
'children': list([
112+
]),
113+
'type': 'text',
114+
}),
115+
dict({
116+
'children': list([
117+
]),
118+
'type': 'text',
119+
}),
120+
dict({
121+
'children': list([
122+
]),
123+
'type': 'text',
124+
}),
125+
dict({
126+
'children': list([
127+
]),
128+
'type': 'text',
129+
}),
130+
]),
131+
'type': 'column',
132+
}),
133+
dict({
134+
'children': list([
135+
]),
136+
'type': 'heading',
137+
}),
138+
dict({
139+
'children': list([
140+
dict({
141+
'children': list([
142+
]),
143+
'type': 'image',
144+
}),
145+
]),
146+
'type': 'repeat',
147+
}),
148+
dict({
149+
'children': list([
150+
]),
151+
'type': 'heading',
152+
}),
153+
dict({
154+
'children': list([
155+
dict({
156+
'children': list([
157+
]),
158+
'type': 'rating_input',
159+
}),
160+
dict({
161+
'children': list([
162+
]),
163+
'type': 'rating_input',
164+
}),
165+
dict({
166+
'children': list([
167+
]),
168+
'type': 'choice',
169+
}),
170+
dict({
171+
'children': list([
172+
]),
173+
'type': 'choice',
174+
}),
175+
dict({
176+
'children': list([
177+
]),
178+
'type': 'choice',
179+
}),
180+
dict({
181+
'children': list([
182+
]),
183+
'type': 'choice',
184+
}),
185+
]),
186+
'type': 'form_container',
187+
}),
188+
dict({
189+
'children': list([
190+
]),
191+
'type': 'heading',
192+
}),
193+
dict({
194+
'children': list([
195+
]),
196+
'type': 'table',
197+
}),
198+
]),
199+
'Variant overview': list([
200+
dict({
201+
'children': list([
202+
]),
203+
'type': 'heading',
204+
}),
205+
dict({
206+
'children': list([
207+
dict({
208+
'children': list([
209+
]),
210+
'type': 'text',
211+
}),
212+
dict({
213+
'children': list([
214+
]),
215+
'type': 'text',
216+
}),
217+
dict({
218+
'children': list([
219+
]),
220+
'type': 'text',
221+
}),
222+
dict({
223+
'children': list([
224+
]),
225+
'type': 'text',
226+
}),
227+
dict({
228+
'children': list([
229+
]),
230+
'type': 'text',
231+
}),
232+
dict({
233+
'children': list([
234+
]),
235+
'type': 'text',
236+
}),
237+
dict({
238+
'children': list([
239+
]),
240+
'type': 'text',
241+
}),
242+
dict({
243+
'children': list([
244+
]),
245+
'type': 'text',
246+
}),
247+
dict({
248+
'children': list([
249+
]),
250+
'type': 'text',
251+
}),
252+
dict({
253+
'children': list([
254+
]),
255+
'type': 'text',
256+
}),
257+
dict({
258+
'children': list([
259+
]),
260+
'type': 'text',
261+
}),
262+
dict({
263+
'children': list([
264+
]),
265+
'type': 'text',
266+
}),
267+
dict({
268+
'children': list([
269+
]),
270+
'type': 'text',
271+
}),
272+
dict({
273+
'children': list([
274+
]),
275+
'type': 'text',
276+
}),
277+
dict({
278+
'children': list([
279+
]),
280+
'type': 'text',
281+
}),
282+
dict({
283+
'children': list([
284+
]),
285+
'type': 'text',
286+
}),
287+
dict({
288+
'children': list([
289+
]),
290+
'type': 'text',
291+
}),
292+
dict({
293+
'children': list([
294+
]),
295+
'type': 'text',
296+
}),
297+
]),
298+
'type': 'column',
299+
}),
300+
dict({
301+
'children': list([
302+
]),
303+
'type': 'heading',
304+
}),
305+
dict({
306+
'children': list([
307+
]),
308+
'type': 'table',
309+
}),
310+
]),
311+
'__shared__': list([
312+
dict({
313+
'children': list([
314+
dict({
315+
'children': list([
316+
]),
317+
'type': 'image',
318+
}),
319+
dict({
320+
'children': list([
321+
]),
322+
'type': 'menu',
323+
}),
324+
]),
325+
'type': 'header',
326+
}),
327+
]),
328+
}),
329+
})
330+
# ---

0 commit comments

Comments
 (0)