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