1 /*
2 * Copyright (c) 2021-2022 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15 #include "gtest/gtest.h"
16 #include "ui_model.h"
17
18 using namespace OHOS::uitest;
19 using namespace std;
20
21 static constexpr auto ATTR_TEXT = "text";
22 static constexpr auto ATTR_ID = "id";
23
TEST(RectTest,testRectBase)24 TEST(RectTest, testRectBase)
25 {
26 Rect rect(100, 200, 300, 400);
27 ASSERT_EQ(100, rect.left_);
28 ASSERT_EQ(200, rect.right_);
29 ASSERT_EQ(300, rect.top_);
30 ASSERT_EQ(400, rect.bottom_);
31
32 ASSERT_EQ(rect.GetCenterX(), (100 + 200) / 2);
33 ASSERT_EQ(rect.GetCenterY(), (300 + 400) / 2);
34 ASSERT_EQ(rect.GetHeight(), 400 - 300);
35 ASSERT_EQ(rect.GetWidth(), 200 - 100);
36 }
37
TEST(WidgetTest,testAttributes)38 TEST(WidgetTest, testAttributes)
39 {
40 Widget widget("hierarchy");
41 // get not-exist attribute, should return default value
42 ASSERT_EQ("none", widget.GetAttr(ATTR_TEXT, "none"));
43 // get exist attribute, should return actual value
44 widget.SetAttr(ATTR_TEXT, "wyz");
45 ASSERT_EQ("wyz", widget.GetAttr(ATTR_TEXT, "none"));
46 }
47
48 /** NOTE:: Widget need to be movable since we need to move a constructed widget to
49 * its hosting tree. We must ensure that the move-created widget be same as the
50 * moved one (No any attribute/filed should be lost during moving).
51 * */
TEST(WidgetTest,testSafeMovable)52 TEST(WidgetTest, testSafeMovable)
53 {
54 Widget widget("hierarchy");
55 widget.SetAttr(ATTR_TEXT, "wyz");
56 widget.SetAttr(ATTR_ID, "100");
57 widget.SetHostTreeId("tree-10086");
58 widget.SetBounds(Rect(1, 2, 3, 4));
59
60 auto newWidget = move(widget);
61 ASSERT_EQ("hierarchy", newWidget.GetHierarchy());
62 ASSERT_EQ("tree-10086", newWidget.GetHostTreeId());
63 ASSERT_EQ("wyz", newWidget.GetAttr(ATTR_TEXT, ""));
64 ASSERT_EQ("100", newWidget.GetAttr(ATTR_ID, ""));
65 auto bounds = newWidget.GetBounds();
66 ASSERT_EQ(1, bounds.left_);
67 ASSERT_EQ(2, bounds.right_);
68 ASSERT_EQ(3, bounds.top_);
69 ASSERT_EQ(4, bounds.bottom_);
70 }
71
TEST(WidgetTest,testToStr)72 TEST(WidgetTest, testToStr)
73 {
74 Widget widget("hierarchy");
75
76 // get exist attribute, should return default value
77 widget.SetAttr(ATTR_TEXT, "wyz");
78 widget.SetAttr(ATTR_ID, "100");
79
80 auto str0 = widget.ToStr();
81 ASSERT_TRUE(str0.find(ATTR_TEXT) != string::npos);
82 ASSERT_TRUE(str0.find("wyz") != string::npos);
83 ASSERT_TRUE(str0.find(ATTR_ID) != string::npos);
84 ASSERT_TRUE(str0.find("100") != string::npos);
85
86 // change attribute
87 widget.SetAttr(ATTR_ID, "211");
88 str0 = widget.ToStr();
89 ASSERT_TRUE(str0.find(ATTR_TEXT) != string::npos);
90 ASSERT_TRUE(str0.find("wyz") != string::npos);
91 ASSERT_TRUE(str0.find(ATTR_ID) != string::npos);
92 ASSERT_TRUE(str0.find("211") != string::npos);
93 ASSERT_TRUE(str0.find("100") == string::npos);
94 }
95
96 // define a widget visitor to visit the specified attribute
97 class WidgetAttrVisitor : public WidgetVisitor {
98 public:
WidgetAttrVisitor(string_view attr)99 explicit WidgetAttrVisitor(string_view attr) : attr_(attr) {}
100
~WidgetAttrVisitor()101 ~WidgetAttrVisitor() {}
102
Visit(const Widget & widget)103 void Visit(const Widget &widget) override
104 {
105 if (!widget.IsVisible()) {
106 firstWidget_ = false;
107 return;
108 }
109 if (!firstWidget_) {
110 attrValueSequence_ << ",";
111 } else {
112 firstWidget_ = false;
113 }
114 attrValueSequence_ << widget.GetAttr(attr_, "");
115 }
116
117 stringstream attrValueSequence_;
118
119 private:
120 const string attr_;
121 volatile bool firstWidget_ = true;
122 };
123
124 static constexpr string_view DOM_TEXT = R"({
125 "attributes": {"resource-id": "id0"},
126 "children": [
127 {
128 "attributes": {"resource-id": "id00"},
129 "children": [
130 {
131 "attributes": {"resource-id": "id000"},
132 "children": [
133 {
134 "attributes": {"resource-id": "id0000"},
135 "children": []
136 }]}]},
137 {
138 "attributes": {"resource-id": "id01"},
139 "children": [
140 {
141 "attributes": {"resource-id": "id010"},
142 "children": []
143 }]}]})";
144
TEST(WidgetTreeTest,testConstructWidgetsFromDomCheckOrder)145 TEST(WidgetTreeTest, testConstructWidgetsFromDomCheckOrder)
146 {
147 auto dom = nlohmann::json::parse(DOM_TEXT);
148 WidgetTree tree("tree");
149 tree.ConstructFromDom(dom, false);
150
151 // visited the widget tree and check the 'resource-id' attribute
152 WidgetAttrVisitor visitor("resource-id");
153 tree.DfsTraverse(visitor);
154 // should collect correct attribute value sequence (DFS)
155 ASSERT_EQ("id0,id00,id000,id0000,id01,id010", visitor.attrValueSequence_.str()) << "Incorrect node order";
156 }
157
158 class BoundsVisitor : public WidgetVisitor {
159 public:
Visit(const Widget & widget)160 void Visit(const Widget &widget) override
161 {
162 if (!widget.IsVisible()) {
163 return;
164 }
165 boundsList_.emplace_back(widget.GetBounds());
166 }
167
168 vector<Rect> boundsList_;
169 };
170
TEST(WidgetTreeTest,testConstructWidgetsFromDomCheckBounds)171 TEST(WidgetTreeTest, testConstructWidgetsFromDomCheckBounds)
172 {
173 auto dom = nlohmann::json::parse(R"({"attributes":{"bounds":"[0,-50][100,200]"}, "children":[]})");
174 WidgetTree tree("tree");
175 tree.ConstructFromDom(dom, false);
176 BoundsVisitor visitor;
177 tree.DfsTraverse(visitor);
178 auto &bounds = visitor.boundsList_.at(0);
179 ASSERT_EQ(0, bounds.left_);
180 ASSERT_EQ(100, bounds.right_); // check converting negative number
181 ASSERT_EQ(-50, bounds.top_);
182 ASSERT_EQ(200, bounds.bottom_);
183 }
184
TEST(WidgetTreeTest,testGetRelativeNode)185 TEST(WidgetTreeTest, testGetRelativeNode)
186 {
187 auto dom = nlohmann::json::parse(DOM_TEXT);
188 WidgetTree tree("tree");
189 tree.ConstructFromDom(dom, false);
190 BoundsVisitor visitor;
191 tree.DfsTraverse(visitor);
192
193 auto rootPtr = tree.GetRootWidget();
194 ASSERT_TRUE(rootPtr != nullptr) << "Failed to get root node";
195 ASSERT_EQ(nullptr, tree.GetParentWidget(*rootPtr)) << "Root node should have no parent";
196 ASSERT_EQ("id0", rootPtr->GetAttr("resource-id", "")) << "Incorrect root node attribute";
197
198 auto child1Ptr = tree.GetChildWidget(*rootPtr, 1);
199 ASSERT_TRUE(child1Ptr != nullptr) << "Failed to get child widget of root node at index 1";
200 ASSERT_EQ("id01", child1Ptr->GetAttr("resource-id", "")) << "Incorrect child node attribute";
201
202 ASSERT_TRUE(tree.GetChildWidget(*rootPtr, 2) == nullptr) << "Unexpected child not";
203 }
204
TEST(WidgetTreeTest,testVisitNodesInGivenRoot)205 TEST(WidgetTreeTest, testVisitNodesInGivenRoot)
206 {
207 auto dom = nlohmann::json::parse(DOM_TEXT);
208 WidgetTree tree("tree");
209 tree.ConstructFromDom(dom, false);
210
211 auto rootPtr = tree.GetRootWidget();
212 ASSERT_TRUE(rootPtr != nullptr) << "Failed to get root node";
213 ASSERT_EQ(nullptr, tree.GetParentWidget(*rootPtr)) << "Root node should have no parent";
214
215 auto child0Ptr = tree.GetChildWidget(*rootPtr, 0);
216 ASSERT_TRUE(child0Ptr != nullptr) << "Failed to get child widget of root node at index 0";
217
218 WidgetAttrVisitor attrVisitor("resource-id");
219 tree.DfsTraverseDescendants(attrVisitor, *child0Ptr);
220 auto visitedTextSequence = attrVisitor.attrValueSequence_.str();
221 ASSERT_EQ("id00,id000,id0000", visitedTextSequence) << "Incorrect text sequence of node descendants";
222 }
223
TEST(WidgetTreeTest,testVisitFrontNodes)224 TEST(WidgetTreeTest, testVisitFrontNodes)
225 {
226 auto dom = nlohmann::json::parse(DOM_TEXT);
227 WidgetTree tree("tree");
228 tree.ConstructFromDom(dom, false);
229
230 auto rootPtr = tree.GetRootWidget();
231 ASSERT_TRUE(rootPtr != nullptr) << "Failed to get root node";
232 ASSERT_EQ(nullptr, tree.GetParentWidget(*rootPtr)) << "Root node should have no parent";
233
234 auto child1Ptr = tree.GetChildWidget(*rootPtr, 1);
235 ASSERT_TRUE(child1Ptr != nullptr) << "Failed to get child widget of root node at index 0";
236
237 WidgetAttrVisitor attrVisitor("resource-id");
238 tree.DfsTraverseFronts(attrVisitor, *child1Ptr);
239 auto visitedTextSequence = attrVisitor.attrValueSequence_.str();
240 ASSERT_EQ("id0,id00,id000,id0000", visitedTextSequence) << "Incorrect text sequence of front nodes";
241 }
242
TEST(WidgetTreeTest,testVisitTearNodes)243 TEST(WidgetTreeTest, testVisitTearNodes)
244 {
245 auto dom = nlohmann::json::parse(DOM_TEXT);
246 WidgetTree tree("tree");
247 tree.ConstructFromDom(dom, false);
248
249 auto rootPtr = tree.GetRootWidget();
250 ASSERT_TRUE(rootPtr != nullptr) << "Failed to get root node";
251 ASSERT_EQ(nullptr, tree.GetParentWidget(*rootPtr)) << "Root node should have no parent";
252
253 auto child0Ptr = tree.GetChildWidget(*rootPtr, 0);
254 ASSERT_TRUE(child0Ptr != nullptr) << "Failed to get child widget of root node at index 0";
255
256 WidgetAttrVisitor attrVisitor("resource-id");
257 tree.DfsTraverseRears(attrVisitor, *child0Ptr);
258 auto visitedTextSequence = attrVisitor.attrValueSequence_.str();
259 ASSERT_EQ("id000,id0000,id01,id010", visitedTextSequence) << "Incorrect text sequence of tear nodes";
260 }
261
TEST(WidgetTreeTest,testBoundsAndVisibilityCorrection)262 TEST(WidgetTreeTest, testBoundsAndVisibilityCorrection)
263 {
264 constexpr string_view domText = R"(
265 {"attributes" : {"resource-id" : "id0","bounds" : "[0,0][100,100]"},
266 "children": [
267 {"attributes" : { "resource-id" : "id00","bounds" : "[0,20][100,80]" },
268 "children": [ {"attributes": {"resource-id": "id000","bounds": "[0,10][100,90]"}, "children": []} ]
269 },
270 {"attributes": {"resource-id": "id01","bounds": "[0,-20][100,100]"},
271 "children": [ {"attributes": {"resource-id": "id010","bounds": "[0,0][100,0]"}, "children": []},
272 {"attributes": {"resource-id": "id011","bounds": "[0,-20][100,20]"}, "children": []}]
273 }
274 ]
275 })";
276 // id01 id010 id011
277 // | | |
278 // | | |
279 // id0 | | |
280 // | id000 | |
281 // | id00 | | |
282 // | | | |
283 // | | | |
284 // | | | |
285 // | | |
286 // | |
287 auto dom = nlohmann::json::parse(domText);
288 WidgetTree tree("tree");
289 tree.ConstructFromDom(dom, true); // enable bounds amending
290 WidgetAttrVisitor attrVisitor("resource-id");
291 tree.DfsTraverse(attrVisitor);
292 // id010 should be discarded dut to totaly invisible
293 ASSERT_EQ("id0,id00,id000,id01,id011", attrVisitor.attrValueSequence_.str());
294 BoundsVisitor boundsVisitor;
295 tree.DfsTraverse(boundsVisitor);
296 // check revised bounds
297 vector<Rect> expectedBounds = {Rect(0, 100, 0, 100), Rect(0, 100, 20, 80), Rect(0, 100, 10, 90),
298 Rect(0, 100, -20, 100), Rect(0, 100, -20, 20)};
299 ASSERT_EQ(expectedBounds.size(), boundsVisitor.boundsList_.size());
300 for (size_t index = 0; index < expectedBounds.size(); index++) {
301 auto &expectedBound = expectedBounds.at(index);
302 auto &actualBound = boundsVisitor.boundsList_.at(index);
303 ASSERT_EQ(expectedBound.left_, actualBound.left_);
304 ASSERT_EQ(expectedBound.right_, actualBound.right_);
305 ASSERT_EQ(expectedBound.top_, actualBound.top_);
306 ASSERT_EQ(expectedBound.bottom_, actualBound.bottom_);
307 }
308 }
309
TEST(WidgetTreeTest,testBoundsAndVisibilityCorrectionInList)310 TEST(WidgetTreeTest, testBoundsAndVisibilityCorrectionInList)
311 {
312 constexpr string_view domText = R"(
313 {"attributes" : {"resource-id" : "id0","bounds" : "[0,0][100,100]","type" : "List"},
314 "children": [
315 {"attributes" : { "resource-id" : "id00","bounds" : "[0,20][100,120]" },
316 "children": [ {"attributes": {"resource-id": "id000","bounds": "[0,0][100,20]"}, "children": []},
317 {"attributes": {"resource-id": "id001","bounds": "[0,100][100,120]"}, "children": []}]
318 },
319 {"attributes": {"resource-id": "id01","bounds": "[110,110][120,120]"},
320 "children": [ {"attributes": {"resource-id": "id010","bounds": "[0,0][100,80]"}, "children": []},
321 {"attributes": {"resource-id": "id011","bounds": "[0,20][100,80]","type" : "List"},
322 "children": [ {"attributes": {"resource-id": "id0110","bounds": "[0,0][100,20]"}, "children": []} ]}]
323 }
324 ]
325 })";
326 // id0 List0.
327 // id00 Child of List0, is within List0 partially, set it visible and amend bounds.
328 // id000 Child of id00, is not within id00, but is within List0, set it visible.
329 // id001 Child of id00, is within id00, but is not within List0, set it invisible.
330 // id01 Child of List0, is not within List0, but it has visible child id010, set it visible and Rect(0,0,0,0).
331 // id010 Child of id01, is not within id01, but is within List0, set it visible.
332 // id011 List, Child of List0, is within List0, set it visible.
333 // id0110 Child of List1, is within List0, but is not within List1, set it invisible.
334 auto dom = nlohmann::json::parse(domText);
335 WidgetTree tree("tree");
336 tree.ConstructFromDom(dom, true); // enable bounds amending
337 WidgetAttrVisitor attrVisitor("resource-id");
338 tree.DfsTraverse(attrVisitor);
339 // id010 should be discarded dut to totaly invisible
340 ASSERT_EQ("id0,id00,id000,id01,id010,id011", attrVisitor.attrValueSequence_.str());
341 BoundsVisitor boundsVisitor;
342 tree.DfsTraverse(boundsVisitor);
343 // check revised bounds
344 vector<Rect> expectedBounds = {Rect(0, 100, 0, 100), Rect(0, 100, 20, 100), Rect(0, 100, 0, 20),
345 Rect(0, 0, 0, 0), Rect(0, 100, 0, 80), Rect(0, 100, 20, 80)};
346 ASSERT_EQ(expectedBounds.size(), boundsVisitor.boundsList_.size());
347 for (size_t index = 0; index < expectedBounds.size(); index++) {
348 auto &expectedBound = expectedBounds.at(index);
349 auto &actualBound = boundsVisitor.boundsList_.at(index);
350 ASSERT_EQ(expectedBound.left_, actualBound.left_);
351 ASSERT_EQ(expectedBound.right_, actualBound.right_);
352 ASSERT_EQ(expectedBound.top_, actualBound.top_);
353 ASSERT_EQ(expectedBound.bottom_, actualBound.bottom_);
354 }
355 }
356
TEST(WidgetTreeTest,testMarshalIntoDom)357 TEST(WidgetTreeTest, testMarshalIntoDom)
358 {
359 auto dom = nlohmann::json::parse(DOM_TEXT);
360 WidgetTree tree("tree");
361 tree.ConstructFromDom(dom, false);
362 auto dom1 = nlohmann::json();
363 tree.MarshalIntoDom(dom1);
364 ASSERT_FALSE(dom1.empty());
365 }
366
367 /** Make merged tree from doms and collects the target attribute dfs sequence.*/
CheckMergedTree(const array<string_view,3> & doms,map<string,string> & attrCollector)368 static void CheckMergedTree(const array<string_view, 3> &doms, map<string, string> &attrCollector)
369 {
370 WidgetTree tree("");
371 vector<unique_ptr<WidgetTree>> subTrees;
372 for (auto &dom : doms) {
373 auto tempTree = make_unique<WidgetTree>("");
374 tempTree->ConstructFromDom(nlohmann::json::parse(dom), true);
375 subTrees.push_back(move(tempTree));
376 }
377 vector<int32_t> mergedOrders;
378 WidgetTree::MergeTrees(subTrees, tree, mergedOrders);
379 for (auto &[name, value] : attrCollector) {
380 if (name == "bounds") {
381 BoundsVisitor visitor;
382 tree.DfsTraverse(visitor);
383 stringstream stream;
384 for (auto &rect : visitor.boundsList_) {
385 stream << "[" << rect.left_ << "," << rect.top_ << "][" << rect.right_ << "," << rect.bottom_ << "]";
386 stream << ",";
387 }
388 attrCollector[name] = stream.str();
389 } else {
390 WidgetAttrVisitor visitor(name);
391 tree.DfsTraverse(visitor);
392 attrCollector[name] = visitor.attrValueSequence_.str();
393 }
394 }
395 }
396
TEST(WidgetTreeTest,testMergeTreesNoIntersection)397 TEST(WidgetTreeTest, testMergeTreesNoIntersection)
398 {
399 // 3 tree vertical/horizontal arranged without intersection
400 constexpr string_view domText0 = R"(
401 {
402 "attributes": {"id": "t0-id0", "bounds": "[0,0][2,2]"},
403 "children": [{"attributes": {"id": "t0-id00", "bounds": "[0,0][2,1]"}, "children": []}]
404 })";
405 constexpr string_view domText1 = R"(
406 {
407 "attributes": {"id": "t1-id0", "bounds": "[0,2][2,4]"},
408 "children": [{"attributes": {"id": "t1-id00", "bounds": "[0,2][2,3]"}, "children": []}]
409 })";
410 constexpr string_view domText2 = R"(
411 {
412 "attributes": {"id": "t2-id0", "bounds": "[2,0][4,4]"},
413 "children": [{"attributes": {"id": "t2-id00", "bounds": "[2,0][4,3]"}, "children": []}]
414 })";
415 map<string, string> attrs;
416 attrs["id"] = "";
417 attrs["bounds"] = "";
418 CheckMergedTree( {domText0, domText1, domText2}, attrs);
419 // all widgets should be available (leading ',': separator of virtual-root node attr-value)
420 ASSERT_EQ(",t0-id0,t0-id00,t1-id0,t1-id00,t2-id0,t2-id00", attrs["id"]);
421 // bounds should not be revised (leading '[0,0][4,4]': auto-computed virtual-root node bounds)
422 ASSERT_EQ("[0,0][4,4],[0,0][2,2],[0,0][2,1],[0,2][2,4],[0,2][2,3],[2,0][4,4],[2,0][4,3],", attrs["bounds"]);
423 }
424
TEST(WidgetTreeTest,testMergeTreesWithFullyCovered)425 TEST(WidgetTreeTest, testMergeTreesWithFullyCovered)
426 {
427 // dom2 is fully covered by dom0 and dom1
428 constexpr string_view domText0 = R"(
429 {
430 "attributes": {"id": "t0-id0", "bounds": "[0,0][2,2]"},
431 "children": [{"attributes": {"id": "t0-id00", "bounds": "[0,0][2,1]"}, "children": []}]
432 })";
433 constexpr string_view domText1 = R"(
434 {
435 "attributes": {"id": "t1-id0", "bounds": "[0,2][2,4]"},
436 "children": [{"attributes": {"id": "t1-id00", "bounds": "[0,2][2,3]"}, "children": []}]
437 })";
438 constexpr string_view domText2 = R"(
439 {
440 "attributes": {"id": "t2-id0", "bounds": "[0,0][2,4]"},
441 "children": [{"attributes": {"id": "t2-id00", "bounds": "[2,0][4,3]"}, "children": []}]
442 })";
443 map<string, string> attrs;
444 attrs["id"] = "";
445 attrs["bounds"] = "";
446 CheckMergedTree( {domText0, domText1, domText2}, attrs);
447 // tree2 widgets should be discarded (leading ',': separator of virtual-root node attr-value)
448 ASSERT_EQ(",t0-id0,t0-id00,t1-id0,t1-id00", attrs["id"]);
449 // bounds should not be revised (leading '[0,0][2,4]': auto-computed virtual-root node bounds)
450 ASSERT_EQ("[0,0][2,4],[0,0][2,2],[0,0][2,1],[0,2][2,4],[0,2][2,3],", attrs["bounds"]);
451 }
452
TEST(WidgetTreeTest,testMergeTreesWithPartialCovered)453 TEST(WidgetTreeTest, testMergeTreesWithPartialCovered)
454 {
455 constexpr string_view domText0 = R"(
456 {
457 "attributes": {"id": "t0-id0", "bounds": "[0,0][4,4]"},
458 "children": [{"attributes": {"id": "t0-id00", "bounds": "[0,0][4,2]"}, "children": []}]
459 })";
460 // t1-id0 is partial covered by tree0, t1-id00 is fully covered
461 constexpr string_view domText1 = R"(
462 {
463 "attributes": {"id": "t1-id0", "bounds": "[0,2][4,6]"},
464 "children": [{"attributes": {"id": "t1-id00", "bounds": "[0,2][4,4]"}, "children": []}]
465 })";
466 // t2-id0 is partial covered by tree0/tree1, t2-id00 is fully covered by tree0/tree1
467 constexpr string_view domText2 = R"(
468 {
469 "attributes": {"id": "t2-id0", "bounds": "[0,0][4,8]"},
470 "children": [{"attributes": {"id": "t2-id00", "bounds": "[0,0][4,5]"}, "children": []}]
471 })";
472 map<string, string> attrs;
473 attrs["id"] = "";
474 attrs["bounds"] = "";
475 CheckMergedTree( {domText0, domText1, domText2}, attrs);
476 // check visible widgets (leading ',': separator of virtual-root node attr-value)
477 ASSERT_EQ(",t0-id0,t0-id00,t1-id0,t2-id0", attrs["id"]);
478 // bounds should not be revised (leading '[0,0][2,4]': auto-computed virtual-root node bounds)
479 // t1-id0: [0,4][4,6]; t2-id0: [0,6][4,8]
480 ASSERT_EQ("[0,0][4,8],[0,0][4,4],[0,0][4,2],[0,4][4,6],[0,6][4,8],", attrs["bounds"]);
481 }