1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
4 *
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 */
9
10 #include <config_folders.h>
11
12 #include <vcl/uitest/logger.hxx>
13
14 #include <rtl/bootstrap.hxx>
15 #include <osl/file.hxx>
16 #include <vcl/event.hxx>
17 #include <vcl/uitest/uiobject.hxx>
18 #include <vcl/uitest/eventdescription.hxx>
19 #include <svdata.hxx>
20 #include <com/sun/star/beans/PropertyValue.hpp>
21 #include <memory>
22
23 namespace
24 {
isDialogWindow(vcl::Window const * pWindow)25 bool isDialogWindow(vcl::Window const* pWindow)
26 {
27 WindowType nType = pWindow->GetType();
28 // DIALOG to MODALDIALOG
29 if (nType >= WindowType::DIALOG && nType <= WindowType::MODALDIALOG)
30 return true;
31
32 // MESSBOX, INFOBOX, WARNINGBOX, ERRORBOX, QUERYBOX
33 if (nType >= WindowType::MESSBOX && nType <= WindowType::QUERYBOX)
34 return true;
35
36 if (nType == WindowType::TABDIALOG)
37 return true;
38
39 return false;
40 }
41
isTopWindow(vcl::Window const * pWindow)42 bool isTopWindow(vcl::Window const* pWindow)
43 {
44 WindowType eType = pWindow->GetType();
45 if (eType == WindowType::FLOATINGWINDOW)
46 {
47 return pWindow->GetStyle() & WB_SYSTEMFLOATWIN;
48 }
49 return false;
50 }
51
get_top_parent(vcl::Window * pWindow)52 vcl::Window* get_top_parent(vcl::Window* pWindow)
53 {
54 if (isDialogWindow(pWindow) || isTopWindow(pWindow))
55 return pWindow;
56
57 vcl::Window* pParent = pWindow->GetParent();
58 if (!pParent)
59 return pWindow;
60
61 return get_top_parent(pParent);
62 }
63 }
UITestLogger()64 UITestLogger::UITestLogger()
65 : maStream()
66 , mbValid(false)
67 {
68 static const char* pFile = std::getenv("LO_COLLECT_UIINFO");
69 if (pFile)
70 {
71 OUString aDirPath("${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER
72 "/" SAL_CONFIGFILE("bootstrap") ":UserInstallation}/uitest/");
73 rtl::Bootstrap::expandMacros(aDirPath);
74 osl::Directory::createPath(aDirPath);
75 OUString aFilePath = aDirPath + OUString::fromUtf8(pFile);
76
77 maStream.Open(aFilePath, StreamMode::READWRITE | StreamMode::TRUNC);
78 mbValid = true;
79 }
80 }
81
logCommand(const OUString & rAction,const css::uno::Sequence<css::beans::PropertyValue> & rArgs)82 void UITestLogger::logCommand(const OUString& rAction,
83 const css::uno::Sequence<css::beans::PropertyValue>& rArgs)
84 {
85 if (!mbValid)
86 return;
87
88 OUStringBuffer aBuffer(rAction);
89
90 if (rArgs.hasElements())
91 {
92 aBuffer.append(" {");
93 for (const css::beans::PropertyValue& rProp : rArgs)
94 {
95 OUString aTypeName = rProp.Value.getValueTypeName();
96
97 if (aTypeName == "long" || aTypeName == "short")
98 {
99 sal_Int32 nValue = 0;
100 rProp.Value >>= nValue;
101 aBuffer.append("\"").append(rProp.Name).append("\": ");
102 aBuffer.append(OUString::number(nValue)).append(", ");
103 }
104 else if (aTypeName == "unsigned long")
105 {
106 sal_uInt32 nValue = 0;
107 rProp.Value >>= nValue;
108 aBuffer.append("\"").append(rProp.Name).append("\": ");
109 aBuffer.append(OUString::number(nValue)).append(", ");
110 }
111 else if (aTypeName == "boolean")
112 {
113 bool bValue = false;
114 rProp.Value >>= bValue;
115 aBuffer.append("\"").append(rProp.Name).append("\": ");
116 if (bValue)
117 aBuffer.append("True, ");
118 else
119 aBuffer.append("False, ");
120 }
121 }
122 aBuffer.append("}");
123 }
124
125 OUString aCommand(aBuffer.makeStringAndClear());
126 maStream.WriteLine(OUStringToOString(aCommand, RTL_TEXTENCODING_UTF8));
127 }
128
129 namespace
130 {
131 // most likely this should be recursive
child_windows_have_focus(VclPtr<vcl::Window> const & xUIElement)132 bool child_windows_have_focus(VclPtr<vcl::Window> const& xUIElement)
133 {
134 sal_Int32 nCount = xUIElement->GetChildCount();
135 for (sal_Int32 i = 0; i < nCount; ++i)
136 {
137 vcl::Window* pChild = xUIElement->GetChild(i);
138 if (pChild->HasFocus())
139 {
140 return true;
141 }
142 if (child_windows_have_focus(VclPtr<vcl::Window>(pChild)))
143 return true;
144 }
145 return false;
146 }
147 }
148
logAction(VclPtr<Control> const & xUIElement,VclEventId nEvent)149 void UITestLogger::logAction(VclPtr<Control> const& xUIElement, VclEventId nEvent)
150 {
151 if (!mbValid)
152 return;
153
154 if (xUIElement->get_id().isEmpty())
155 return;
156
157 std::unique_ptr<UIObject> pUIObject = xUIElement->GetUITestFactory()(xUIElement.get());
158 OUString aAction = pUIObject->get_action(nEvent);
159 if (!xUIElement->HasFocus() && !child_windows_have_focus(xUIElement))
160 {
161 return;
162 }
163
164 if (!aAction.isEmpty())
165 maStream.WriteLine(OUStringToOString(aAction, RTL_TEXTENCODING_UTF8));
166 }
167
log(const OUString & rString)168 void UITestLogger::log(const OUString& rString)
169 {
170 if (!mbValid)
171 return;
172
173 if (rString.isEmpty())
174 return;
175
176 maStream.WriteLine(OUStringToOString(rString, RTL_TEXTENCODING_UTF8));
177 }
178
logKeyInput(VclPtr<vcl::Window> const & xUIElement,const KeyEvent & rEvent)179 void UITestLogger::logKeyInput(VclPtr<vcl::Window> const& xUIElement, const KeyEvent& rEvent)
180 {
181 if (!mbValid)
182 return;
183
184 //We need to check for Parent's ID in case the UI Element is SubEdit of Combobox/SpinField
185 const OUString& rID
186 = xUIElement->get_id().isEmpty() ? xUIElement->GetParent()->get_id() : xUIElement->get_id();
187 if (rID.isEmpty())
188 return;
189
190 sal_Unicode nChar = rEvent.GetCharCode();
191 sal_uInt16 nKeyCode = rEvent.GetKeyCode().GetCode();
192 bool bShift = rEvent.GetKeyCode().IsShift();
193 bool bMod1 = rEvent.GetKeyCode().IsMod1();
194 bool bMod2 = rEvent.GetKeyCode().IsMod2();
195 bool bMod3 = rEvent.GetKeyCode().IsMod3();
196
197 std::map<OUString, sal_uInt16> aKeyMap
198 = { { "ESC", KEY_ESCAPE }, { "TAB", KEY_TAB }, { "DOWN", KEY_DOWN },
199 { "UP", KEY_UP }, { "LEFT", KEY_LEFT }, { "RIGHT", KEY_RIGHT },
200 { "DELETE", KEY_DELETE }, { "INSERT", KEY_INSERT }, { "BACKSPACE", KEY_BACKSPACE },
201 { "RETURN", KEY_RETURN }, { "HOME", KEY_HOME }, { "END", KEY_END },
202 { "PAGEUP", KEY_PAGEUP }, { "PAGEDOWN", KEY_PAGEDOWN } };
203
204 OUString aFound;
205 for (const auto& itr : aKeyMap)
206 {
207 if (itr.second == nKeyCode)
208 {
209 aFound = itr.first;
210 break;
211 }
212 }
213
214 OUString aKeyCode;
215 if (!aFound.isEmpty() || bShift || bMod1 || bMod2 || bMod3)
216 {
217 aKeyCode = "{\"KEYCODE\": \"";
218 if (bShift)
219 aKeyCode += "SHIFT+";
220
221 if (bMod1)
222 aKeyCode += "CTRL+";
223
224 if (bMod2)
225 aKeyCode += "ALT+";
226
227 if (aFound.isEmpty())
228 aKeyCode += OUStringChar(nChar) + "\"}";
229 else
230 aKeyCode += aFound + "\"}";
231 }
232 else
233 {
234 aKeyCode = "{\"TEXT\": \"" + OUStringChar(nChar) + "\"}";
235 }
236
237 std::unique_ptr<UIObject> pUIObject = xUIElement->GetUITestFactory()(xUIElement.get());
238
239 VclPtr<vcl::Window> pParent = xUIElement->GetParent();
240
241 while (!pParent->IsTopWindow())
242 {
243 pParent = pParent->GetParent();
244 }
245
246 OUString aParentID = pParent->get_id();
247
248 OUString aContent;
249
250 if (pUIObject->get_type() == "EditUIObject")
251 {
252 if (aParentID.isEmpty())
253 {
254 VclPtr<vcl::Window> pParent_top = get_top_parent(xUIElement);
255 aParentID = pParent_top->get_id();
256 }
257 if (aParentID.isEmpty())
258 {
259 aContent += "Type on '" + rID + "' " + aKeyCode;
260 }
261 else
262 {
263 aContent += "Type on '" + rID + "' " + aKeyCode + " from " + aParentID;
264 }
265 }
266 else if (pUIObject->get_type() == "SwEditWinUIObject" && rID == "writer_edit")
267 {
268 aContent = "Type on writer " + aKeyCode;
269 }
270 else if (pUIObject->get_type() == "ScGridWinUIObject" && rID == "grid_window")
271 {
272 aContent = "Type on current cell " + aKeyCode;
273 }
274 else if (pUIObject->get_type() == "ImpressWindowUIObject" && rID == "impress_win")
275 {
276 aContent = "Type on impress " + aKeyCode;
277 }
278 else if (pUIObject->get_type() == "WindowUIObject" && rID == "math_edit")
279 {
280 aContent = "Type on math " + aKeyCode;
281 }
282 else if (rID == "draw_win")
283 {
284 aContent = "Type on draw " + aKeyCode;
285 }
286 else
287 {
288 if (aParentID.isEmpty())
289 {
290 VclPtr<vcl::Window> pParent_top = get_top_parent(xUIElement);
291 aParentID = pParent_top->get_id();
292 }
293 if (aParentID.isEmpty())
294 {
295 aContent = "Type on '" + rID + "' " + aKeyCode;
296 }
297 else
298 {
299 aContent = "Type on '" + rID + "' " + aKeyCode + " from " + aParentID;
300 }
301 }
302 maStream.WriteLine(OUStringToOString(aContent, RTL_TEXTENCODING_UTF8));
303 }
304
305 namespace
306 {
StringMapToOUString(const std::map<OUString,OUString> & rParameters)307 OUString StringMapToOUString(const std::map<OUString, OUString>& rParameters)
308 {
309 if (rParameters.empty())
310 return "";
311
312 OUStringBuffer aParameterString(static_cast<int>(rParameters.size()*32));
313 aParameterString.append(" {");
314
315 for (std::map<OUString, OUString>::const_iterator itr = rParameters.begin();
316 itr != rParameters.end(); ++itr)
317 {
318 if (itr != rParameters.begin())
319 aParameterString.append(", ");
320 aParameterString.append("\"")
321 .append(itr->first)
322 .append("\": \"")
323 .append(itr->second)
324 .append("\"");
325 }
326
327 aParameterString.append("}");
328
329 return aParameterString.makeStringAndClear();
330 }
331
GetValueInMapWithIndex(const std::map<OUString,OUString> & rParameters,sal_Int32 index)332 OUString GetValueInMapWithIndex(const std::map<OUString, OUString>& rParameters, sal_Int32 index)
333 {
334 sal_Int32 j = 0;
335
336 std::map<OUString, OUString>::const_iterator itr = rParameters.begin();
337
338 for (; itr != rParameters.end() && j < index; ++itr, ++j)
339 ;
340
341 assert(itr != rParameters.end());
342
343 return itr->second;
344 }
345
GetKeyInMapWithIndex(const std::map<OUString,OUString> & rParameters,sal_Int32 index)346 OUString GetKeyInMapWithIndex(const std::map<OUString, OUString>& rParameters, sal_Int32 index)
347 {
348 sal_Int32 j = 0;
349
350 std::map<OUString, OUString>::const_iterator itr = rParameters.begin();
351
352 for (; itr != rParameters.end() && j < index; ++itr, ++j)
353 ;
354
355 assert(itr != rParameters.end());
356
357 return itr->first;
358 }
359 }
360
logEvent(const EventDescription & rDescription)361 void UITestLogger::logEvent(const EventDescription& rDescription)
362 {
363 OUString aParameterString = StringMapToOUString(rDescription.aParameters);
364
365 //here we will customize our statements depending on the caller of this function
366 OUString aLogLine;
367 //first check on general commands
368 if (rDescription.aAction == "SET")
369 {
370 aLogLine = "Set Zoom to " + GetValueInMapWithIndex(rDescription.aParameters, 0);
371 }
372 else if (rDescription.aAction == "SIDEBAR")
373 {
374 aLogLine = "From SIDEBAR Choose " + aParameterString;
375 }
376 else if (rDescription.aAction == "SELECT" && rDescription.aID.isEmpty())
377 {
378 aLogLine = "Select " + aParameterString;
379 }
380 else if (rDescription.aID == "writer_edit")
381 {
382 if (rDescription.aAction == "GOTO")
383 {
384 aLogLine = "GOTO page number " + GetValueInMapWithIndex(rDescription.aParameters, 0);
385 }
386 else if (rDescription.aAction == "SELECT")
387 {
388 OUString to = GetValueInMapWithIndex(rDescription.aParameters, 0);
389 OUString from = GetValueInMapWithIndex(rDescription.aParameters, 1);
390 aLogLine = "Select from Pos " + from + " to Pos " + to;
391 }
392 else if (rDescription.aAction == "CREATE_TABLE")
393 {
394 OUString size = GetValueInMapWithIndex(rDescription.aParameters, 0);
395 aLogLine = "Create Table with " + size;
396 ;
397 }
398 else if (rDescription.aAction == "COPY")
399 {
400 aLogLine = "Copy the Selected Text";
401 }
402 else if (rDescription.aAction == "CUT")
403 {
404 aLogLine = "Cut the Selected Text";
405 }
406 else if (rDescription.aAction == "PASTE")
407 {
408 aLogLine = "Paste in the Current Cursor Location";
409 }
410 else if (rDescription.aAction == "BREAK_PAGE")
411 {
412 aLogLine = "Insert Break Page";
413 }
414 }
415 else if (rDescription.aID == "grid_window")
416 {
417 if (rDescription.aAction == "SELECT")
418 {
419 OUString type = GetKeyInMapWithIndex(rDescription.aParameters, 0);
420 if (type == "CELL" || type == "RANGE")
421 {
422 aLogLine = "Select from calc" + aParameterString;
423 }
424 else if (type == "TABLE")
425 {
426 aLogLine = "Switch to sheet number "
427 + GetValueInMapWithIndex(rDescription.aParameters, 0);
428 }
429 }
430 else if (rDescription.aAction == "LAUNCH")
431 {
432 aLogLine = "Launch AutoFilter from Col "
433 + GetValueInMapWithIndex(rDescription.aParameters, 2) + " and Row "
434 + GetValueInMapWithIndex(rDescription.aParameters, 1);
435 }
436 else if (rDescription.aAction == "DELETE_CONTENT")
437 {
438 aLogLine = "Remove Content from This " + aParameterString;
439 }
440 else if (rDescription.aAction == "DELETE_CELLS")
441 {
442 aLogLine = "Delete The Cells in" + aParameterString;
443 }
444 else if (rDescription.aAction == "INSERT_CELLS")
445 {
446 aLogLine = "Insert Cell around the " + aParameterString;
447 }
448 else if (rDescription.aAction == "CUT")
449 {
450 aLogLine = "CUT the selected " + aParameterString;
451 }
452 else if (rDescription.aAction == "COPY")
453 {
454 aLogLine = "COPY the selected " + aParameterString;
455 }
456 else if (rDescription.aAction == "PASTE")
457 {
458 aLogLine = "Paste in the " + aParameterString;
459 }
460 else if (rDescription.aAction == "MERGE_CELLS")
461 {
462 aLogLine = "Merge " + aParameterString;
463 }
464 else if (rDescription.aAction == "UNMERGE_CELL")
465 {
466 aLogLine = "Delete the merged " + aParameterString;
467 }
468 else if (rDescription.aAction == "Rename_Sheet")
469 {
470 aLogLine = "Rename The Selected Tab to \""
471 + GetValueInMapWithIndex(rDescription.aParameters, 0) + "\"";
472 }
473 else if (rDescription.aAction == "InsertTab")
474 {
475 aLogLine = "Insert New Tab ";
476 }
477 }
478 else if (rDescription.aID == "impress_win_or_draw_win")
479 {
480 if (rDescription.aAction == "Insert_New_Page_or_Slide")
481 {
482 if (UITestLogger::getInstance().getAppName() == "impress")
483 {
484 aLogLine = "Insert New Slide at Position "
485 + GetValueInMapWithIndex(rDescription.aParameters, 0);
486 }
487 else if (UITestLogger::getInstance().getAppName() == "draw")
488 {
489 aLogLine = "Insert New Page at Position "
490 + GetValueInMapWithIndex(rDescription.aParameters, 0);
491 }
492 }
493 else if (rDescription.aAction == "Delete_Slide_or_Page")
494 {
495 if (UITestLogger::getInstance().getAppName() == "impress")
496 {
497 aLogLine
498 = "Delete Slide number " + GetValueInMapWithIndex(rDescription.aParameters, 0);
499 }
500 else if (UITestLogger::getInstance().getAppName() == "draw")
501 {
502 aLogLine
503 = "Delete Page number " + GetValueInMapWithIndex(rDescription.aParameters, 0);
504 }
505 }
506 else if (rDescription.aAction == "Duplicate")
507 {
508 aLogLine = "Duplicate The Selected Slide ";
509 }
510 else if (rDescription.aAction == "RENAME")
511 {
512 if (UITestLogger::getInstance().getAppName() == "impress")
513 {
514 aLogLine = "Rename The Selected Slide from \""
515 + GetValueInMapWithIndex(rDescription.aParameters, 1) + "\" to \""
516 + GetValueInMapWithIndex(rDescription.aParameters, 0) + "\"";
517 }
518 else if (UITestLogger::getInstance().getAppName() == "draw")
519 {
520 aLogLine = "Rename The Selected Page from \""
521 + GetValueInMapWithIndex(rDescription.aParameters, 1) + "\" to \""
522 + GetValueInMapWithIndex(rDescription.aParameters, 0) + "\"";
523 }
524 }
525 }
526 else if (rDescription.aParent == "element_selector")
527 {
528 aLogLine = "Select element no " + rDescription.aID + " From " + rDescription.aParent;
529 }
530 else
531 {
532 aLogLine = rDescription.aKeyWord + " Action:" + rDescription.aAction + " Id:"
533 + rDescription.aID + " Parent:" + rDescription.aParent + aParameterString;
534 }
535 log(aLogLine);
536 }
537
getInstance()538 UITestLogger& UITestLogger::getInstance()
539 {
540 ImplSVData* const pSVData = ImplGetSVData();
541 assert(pSVData);
542
543 if (!pSVData->maWinData.m_pUITestLogger)
544 {
545 pSVData->maWinData.m_pUITestLogger.reset(new UITestLogger);
546 }
547
548 return *pSVData->maWinData.m_pUITestLogger;
549 }
550
551 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
552