/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include UIObject::~UIObject() { } StringMap UIObject::get_state() { StringMap aMap; aMap[u"NotImplemented"_ustr] = "NotImplemented"; return aMap; } void UIObject::execute(const OUString& /*rAction*/, const StringMap& /*rParameters*/) { // should never be called throw std::exception(); } OUString UIObject::get_type() const { return u"Generic UIObject"_ustr; } std::unique_ptr UIObject::get_child(const OUString&) { return std::unique_ptr(); } std::set UIObject::get_children() const { return std::set(); } OUString UIObject::dumpState() const { return OUString(); } OUString UIObject::dumpHierarchy() const { return OUString(); } OUString UIObject::get_action(VclEventId /*nEvent*/) const { return OUString(); } namespace { bool isDialogWindow(vcl::Window const * pWindow) { WindowType nType = pWindow->GetType(); if (nType == WindowType::DIALOG || nType == WindowType::MODELESSDIALOG) return true; // MESSBOX, INFOBOX, WARNINGBOX, ERRORBOX, QUERYBOX if (nType >= WindowType::MESSBOX && nType <= WindowType::QUERYBOX) return true; if (nType == WindowType::TABDIALOG) return true; return false; } bool isTopWindow(vcl::Window const * pWindow) { WindowType eType = pWindow->GetType(); if (eType == WindowType::FLOATINGWINDOW) { return pWindow->GetStyle() & WB_SYSTEMFLOATWIN; } return false; } vcl::Window* get_top_parent(vcl::Window* pWindow) { if (isDialogWindow(pWindow) || isTopWindow(pWindow)) return pWindow; vcl::Window* pParent = pWindow->GetParent(); if (!pParent) return pWindow; return get_top_parent(pParent); } std::vector generate_key_events_from_text(std::u16string_view rStr) { std::vector aEvents; vcl::KeyCode aCode; for (size_t i = 0, n = rStr.size(); i != n; ++i) { aEvents.emplace_back(rStr[i], aCode); } return aEvents; } sal_uInt16 get_key(sal_Unicode cChar, bool& bShift) { bShift = false; if (cChar >= 'a' && cChar <= 'z') return KEY_A + (cChar - 'a'); else if (cChar >= 'A' && cChar <= 'Z') { bShift = true; return KEY_A + (cChar - 'A'); } else if (cChar >= '0' && cChar <= '9') return KEY_0 + (cChar - 'A'); return cChar; } bool isFunctionKey(const OUString& rStr, sal_uInt16& rKeyCode) { std::map aFunctionKeyMap = { {"F1", KEY_F1}, {"F2", KEY_F2}, {"F3", KEY_F3}, {"F4", KEY_F4}, {"F5", KEY_F5}, {"F6", KEY_F6}, {"F7", KEY_F7}, {"F8", KEY_F8}, {"F9", KEY_F9}, {"F10", KEY_F10}, {"F11", KEY_F11}, {"F12", KEY_F12} }; rKeyCode = 0; auto itr = aFunctionKeyMap.find(rStr); if (itr == aFunctionKeyMap.end()) return false; rKeyCode = itr->second; return true; } std::vector generate_key_events_from_keycode(std::u16string_view rStr) { std::vector aEvents; std::map aKeyMap = { {"ESC", KEY_ESCAPE}, {"TAB", KEY_TAB}, {"DOWN", KEY_DOWN}, {"UP", KEY_UP}, {"LEFT", KEY_LEFT}, {"RIGHT", KEY_RIGHT}, {"DELETE", KEY_DELETE}, {"INSERT", KEY_INSERT}, {"SPACE", KEY_SPACE}, {"BACKSPACE", KEY_BACKSPACE}, {"RETURN", KEY_RETURN}, {"HOME", KEY_HOME}, {"END", KEY_END}, {"PAGEUP", KEY_PAGEUP}, {"PAGEDOWN", KEY_PAGEDOWN} }; // split string along '+' // then translate to keycodes bool bShift = false; bool bMod1 = false; bool bMod2 = false; OUString aRemainingText; std::vector aTokens = comphelper::string::split(rStr, '+'); for (auto const& token : aTokens) { OUString aToken = token.trim(); if (aToken == "CTRL") { bMod1 = true; } else if (aToken == "SHIFT") { bShift = true; } else if (aToken == "ALT") { bMod2 = true; } else aRemainingText = aToken; } sal_uInt16 nFunctionKey = 0; if (isFunctionKey(aRemainingText, nFunctionKey)) { vcl::KeyCode aCode(nFunctionKey, bShift, bMod1, bMod2, false); aEvents.emplace_back(0, aCode); } else if (aKeyMap.contains(aRemainingText)) { sal_uInt16 nKey = aKeyMap[aRemainingText]; vcl::KeyCode aCode(nKey, bShift, bMod1, bMod2, false); aEvents.emplace_back( 'a', aCode); } else { for (sal_Int32 i = 0; i < aRemainingText.getLength(); ++i) { bool bShiftThroughKey = false; sal_uInt16 nKey = get_key(aRemainingText[i], bShiftThroughKey); vcl::KeyCode aCode(nKey, bShift || bShiftThroughKey, bMod1, bMod2, false); aEvents.emplace_back(aRemainingText[i], aCode); } } return aEvents; } OUString to_string(const Point& rPos) { OUString sStr = OUString::number(rPos.X()) + "x" + OUString::number(rPos.Y()); return sStr; } OUString to_string(const Size& rSize) { OUString sStr = OUString::number(rSize.Width()) + "x" + OUString::number(rSize.Height()); return sStr; } } WindowUIObject::WindowUIObject(const VclPtr& xWindow): mxWindow(xWindow) { } StringMap WindowUIObject::get_state() { // Double-buffering is not interesting for uitesting, but can result in direct paint for a // double-buffered widget, which is incorrect. if (mxWindow->SupportsDoubleBuffering()) mxWindow->RequestDoubleBuffering(false); StringMap aMap; aMap[u"Visible"_ustr] = OUString::boolean(mxWindow->IsVisible()); aMap[u"ReallyVisible"_ustr] = OUString::boolean(mxWindow->IsReallyVisible()); aMap[u"Enabled"_ustr] = OUString::boolean(mxWindow->IsEnabled()); aMap[u"HasFocus"_ustr] = OUString::boolean(mxWindow->HasChildPathFocus()); aMap[u"WindowType"_ustr] = OUString::number(static_cast(mxWindow->GetType()), 16); Point aPos = mxWindow->GetPosPixel(); aMap[u"RelPosition"_ustr] = to_string(aPos); aMap[u"Size"_ustr] = to_string(mxWindow->GetSizePixel()); aMap[u"ID"_ustr] = mxWindow->get_id(); vcl::Window* pParent = mxWindow->GetParent(); if (pParent) aMap[u"Parent"_ustr] = mxWindow->GetParent()->get_id(); bool bIgnoreAllExceptTop = isDialogWindow(mxWindow.get()); while(pParent) { Point aParentPos = pParent->GetPosPixel(); if (!bIgnoreAllExceptTop) aPos += aParentPos; if (isDialogWindow(pParent)) { bIgnoreAllExceptTop = true; } pParent = pParent->GetParent(); if (!pParent && bIgnoreAllExceptTop) aPos += aParentPos; } aMap[u"AbsPosition"_ustr] = to_string(aPos); aMap[u"Text"_ustr] = mxWindow->GetText(); aMap[u"DisplayText"_ustr] = mxWindow->GetDisplayText(); return aMap; } void WindowUIObject::execute(const OUString& rAction, const StringMap& rParameters) { if (rAction == "SET") { for (auto const& parameter : rParameters) { std::cout << parameter.first; } } else if (rAction == "TYPE") { auto it = rParameters.find(u"TEXT"_ustr); if (it != rParameters.end()) { const OUString& rText = it->second; auto aKeyEvents = generate_key_events_from_text(rText); for (auto const& keyEvent : aKeyEvents) { mxWindow->KeyInput(keyEvent); } } else if (rParameters.contains(u"KEYCODE"_ustr)) { auto itr = rParameters.find(u"KEYCODE"_ustr); assert(itr != rParameters.end()); const OUString rText = itr->second; auto aKeyEvents = generate_key_events_from_keycode(rText); for (auto const& keyEvent : aKeyEvents) { mxWindow->KeyInput(keyEvent); } } else { OStringBuffer buf; for (auto const & rPair : rParameters) buf.append("," + rPair.first.toUtf8() + "=" + rPair.second.toUtf8()); SAL_WARN("vcl.uitest", "missing parameter TEXT to action TYPE " << buf.makeStringAndClear()); throw std::logic_error("missing parameter TEXT to action TYPE"); } } else if (rAction == "FOCUS") { mxWindow->GrabFocus(); } else { OStringBuffer buf; for (auto const & rPair : rParameters) buf.append("," + rPair.first.toUtf8() + "=" + rPair.second.toUtf8()); SAL_WARN("vcl.uitest", "unknown action for " << get_name() << ". Action: " << rAction << buf.makeStringAndClear()); throw std::logic_error("unknown action"); } } OUString WindowUIObject::get_type() const { return get_name(); } bool WindowUIObject::equals(const UIObject& rOther) const { const WindowUIObject* pOther = dynamic_cast(&rOther); if (!pOther) return false; return mxWindow.get() == pOther->mxWindow.get(); } namespace { vcl::Window* findChild(vcl::Window* pParent, const OUString& rID, bool bRequireVisible = false, OUStringBuffer* debug = nullptr) { if (!pParent || pParent->isDisposed()) return nullptr; if (pParent->get_id() == rID) return pParent; size_t nCount = pParent->GetChildCount(); for (size_t i = 0; i < nCount; ++i) { vcl::Window* pChild = pParent->GetChild(i); bool bCandidate = !bRequireVisible || pChild->IsVisible(); if (!bCandidate) continue; if (pChild->get_id() == rID) return pChild; if (debug) debug->append(pChild->get_id() + " "); vcl::Window* pResult = findChild(pChild, rID, bRequireVisible, debug); if (pResult) return pResult; } return nullptr; } void addChildren(vcl::Window const * pParent, std::set& rChildren) { if (!pParent) return; size_t nCount = pParent->GetChildCount(); for (size_t i = 0; i < nCount; ++i) { vcl::Window* pChild = pParent->GetChild(i); if (pChild) { OUString aId = pChild->get_id(); if (!aId.isEmpty()) { auto ret = rChildren.insert(aId); SAL_INFO_IF(!ret.second, "vcl.uitest", "duplicate ids '" << aId << "' for ui elements. violates locally unique requirement"); } addChildren(pChild, rChildren); } } } } std::unique_ptr WindowUIObject::get_child(const OUString& rID) { // in a first step try the real children before moving to the top level parent // This makes it easier to handle cases with the same ID as there is a way // to resolve conflicts OUStringBuffer debug; vcl::Window* pWindow = findChild(mxWindow.get(), rID, false, &debug); if (!pWindow) { vcl::Window* pDialogParent = get_top_parent(mxWindow.get()); pWindow = findChild(pDialogParent, rID, false, &debug); } if (!pWindow) throw css::uno::RuntimeException("Could not find child with id: " + rID + " children were " + std::u16string_view(debug)); FactoryFunction aFunction = pWindow->GetUITestFactory(); return aFunction(pWindow); } std::unique_ptr WindowUIObject::get_visible_child(const OUString& rID) { // in a first step try the real children before moving to the top level parent // This makes it easier to handle cases with the same ID as there is a way // to resolve conflicts vcl::Window* pWindow = findChild(mxWindow.get(), rID, true); if (!pWindow) { vcl::Window* pDialogParent = get_top_parent(mxWindow.get()); pWindow = findChild(pDialogParent, rID, true); } if (!pWindow) throw css::uno::RuntimeException("Could not find child with id: " + rID); FactoryFunction aFunction = pWindow->GetUITestFactory(); return aFunction(pWindow); } std::set WindowUIObject::get_children() const { std::set aChildren; vcl::Window* pDialogParent = get_top_parent(mxWindow.get()); if (!pDialogParent->isDisposed()) { aChildren.insert(pDialogParent->get_id()); addChildren(pDialogParent, aChildren); } return aChildren; } OUString WindowUIObject::get_name() const { return u"WindowUIObject"_ustr; } namespace { OUString escape(const OUString& rStr) { return rStr.replaceAll("\"", "\\\""); } } OUString WindowUIObject::dumpState() const { OUStringBuffer aStateString = "{\"name\":\"" + mxWindow->get_id() + "\""; aStateString.append(", \"ImplementationName\":\"").appendAscii(typeid(*mxWindow).name()).append("\""); StringMap aState = const_cast(this)->get_state(); for (auto const& elem : aState) { OUString property = ",\"" + elem.first + "\":\"" + escape(elem.second) + "\""; aStateString.append(property); } size_t nCount = mxWindow->GetChildCount(); if (nCount) aStateString.append(",\"children\":["); for (size_t i = 0; i < nCount; ++i) { if (i != 0) { aStateString.append(","); } vcl::Window* pChild = mxWindow->GetChild(i); std::unique_ptr pChildWrapper = pChild->GetUITestFactory()(pChild); OUString children = pChildWrapper->dumpState(); aStateString.append(children); } if (nCount) aStateString.append("]"); aStateString.append("}"); OUString aString = aStateString.makeStringAndClear(); return aString.replaceAll("\n", "\\n"); } OUString WindowUIObject::dumpHierarchy() const { vcl::Window* pDialogParent = get_top_parent(mxWindow.get()); std::unique_ptr pParentWrapper = pDialogParent->GetUITestFactory()(pDialogParent); return pParentWrapper->dumpState(); } OUString WindowUIObject::get_action(VclEventId nEvent) const { OUString aActionName; switch (nEvent) { case VclEventId::ControlGetFocus: case VclEventId::ControlLoseFocus: return OUString(); case VclEventId::ButtonClick: case VclEventId::CheckboxToggle: aActionName = "CLICK"; break; case VclEventId::EditModify: aActionName = "TYPE"; break; default: aActionName = OUString::number(static_cast(nEvent)); } return "Action on element: " + mxWindow->get_id() + " with action : " + aActionName; } std::unique_ptr WindowUIObject::create(vcl::Window* pWindow) { return std::unique_ptr(new WindowUIObject(pWindow)); } ButtonUIObject::ButtonUIObject(const VclPtr