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 16 #include <future> 17 #include "ui_driver.h" 18 19 namespace OHOS::uitest { 20 using namespace std; 21 using namespace nlohmann; 22 23 class TreeSnapshotTaker : public WidgetVisitor { 24 public: TreeSnapshotTaker(stringstream & receiver)25 explicit TreeSnapshotTaker(stringstream &receiver) : receiver_(receiver) {}; 26 ~TreeSnapshotTaker()27 ~TreeSnapshotTaker() {} 28 Visit(const Widget & widget)29 void Visit(const Widget &widget) override 30 { 31 receiver_ << widget.GetAttr(ATTR_NAMES[UiAttr::TYPE], "") << "/"; 32 receiver_ << widget.GetAttr(ATTR_NAMES[UiAttr::TEXT], "") << ";"; 33 } 34 35 private: 36 stringstream &receiver_; 37 }; 38 UpdateUi(bool updateUiTree,ApiCallErr & error)39 void UiDriver::UpdateUi(bool updateUiTree, ApiCallErr &error) 40 { 41 UiController::InstallForDevice(deviceName_); 42 uiController_ = UiController::GetController(deviceName_); 43 if (uiController_ == nullptr) { 44 LOG_E("%{public}s", (string("No available UiController for device: ") + string(deviceName_)).c_str()); 45 error = ApiCallErr(INTERNAL_ERROR, "No available UiController currently"); 46 return; 47 } 48 if (!updateUiTree) { 49 return; 50 } 51 widgetTree_ = make_unique<WidgetTree>(""); 52 auto domData = json(); 53 uiController_->GetCurrentUiDom(domData); 54 widgetTree_->ConstructFromDom(domData, true); 55 } 56 57 /**Inflate widget-image attributes from the given widget-object and the selector.*/ Widget2Image(const Widget & widget,WidgetImage & image,const WidgetSelector & selector)58 static void Widget2Image(const Widget &widget, WidgetImage &image, const WidgetSelector &selector) 59 { 60 map<string, string> attributes; 61 widget.DumpAttributes(attributes); 62 image.SetAttributes(attributes); 63 image.SetSelectionDesc(selector.Describe()); 64 } 65 RetrieveWidget(const WidgetImage & img,ApiCallErr & err,bool updateUi)66 const Widget *UiDriver::RetrieveWidget(const WidgetImage &img, ApiCallErr &err, bool updateUi) 67 { 68 if (updateUi) { 69 UpdateUi(true, err); 70 if (err.code_ != NO_ERROR) { 71 return nullptr; 72 } 73 } 74 // retrieve widget by hashcode or by hierarchy 75 auto hashcodeMatcher = WidgetAttrMatcher(ATTR_HASHCODE, img.GetHashCode(), EQ); 76 auto hierarchyMatcher = WidgetAttrMatcher(ATTR_HIERARCHY, img.GetHierarchy(), EQ); 77 auto anyMatcher = Any(hashcodeMatcher, hierarchyMatcher); 78 vector<reference_wrapper<const Widget>> recv; 79 auto visitor = MatchedWidgetCollector(anyMatcher, recv); 80 widgetTree_->DfsTraverse(visitor); 81 stringstream msg; 82 msg << "Widget: " << img.GetSelectionDesc(); 83 msg << "dose not exist on current UI! Check if the UI has changed after you go the widget object"; 84 if (recv.empty()) { 85 msg << "(NoCandidates)"; 86 LOG_W("%{public}s", msg.str().c_str()); 87 err = ApiCallErr(WIDGET_LOST, msg.str()); 88 return nullptr; 89 } 90 DCHECK(recv.size() == 1); 91 auto &widget = recv.at(0).get(); 92 // check equality 93 WidgetImage newImage = WidgetImage(); 94 WidgetSelector selector; // dummy selector 95 Widget2Image(widget, newImage, selector); 96 if (!img.Compare(newImage)) { 97 msg << " (CompareEqualsFailed)"; 98 LOG_W("%{public}s", msg.str().c_str()); 99 err = ApiCallErr(WIDGET_LOST, msg.str()); 100 return nullptr; 101 } 102 return &widget; 103 } 104 InjectGenericClick(PointerOp type,const Point & point,const UiController & controller,const UiDriveOptions & options)105 static void InjectGenericClick(PointerOp type, const Point &point, const UiController &controller, 106 const UiDriveOptions &options) 107 { 108 auto action = GenericClick(type); 109 vector<TouchEvent> events; 110 action.Decompose(events, point, options); 111 if (events.empty()) { return; } 112 controller.InjectTouchEventSequence(events); 113 controller.WaitForUiSteady(options.uiSteadyThresholdMs_, options.waitUiSteadyMaxMs_); 114 } 115 InjectGenericSwipe(PointerOp type,const Point & point0,const Point & point1,const UiController & controller,const UiDriveOptions & options)116 static void InjectGenericSwipe(PointerOp type, const Point &point0, const Point &point1, 117 const UiController &controller, const UiDriveOptions &options) 118 { 119 auto action = GenericSwipe(type); 120 vector<TouchEvent> events; 121 action.Decompose(events, point0, point1, options); 122 if (events.empty()) { return; } 123 controller.InjectTouchEventSequence(events); 124 controller.WaitForUiSteady(options.uiSteadyThresholdMs_, options.waitUiSteadyMaxMs_); 125 } 126 127 /**Convert WidgetOperation to PointerActions and do injection.*/ InjectWidgetOperate(const Rect & bounds,WidgetOp operate,const UiController & controller,const UiDriveOptions & options)128 static void InjectWidgetOperate(const Rect &bounds, WidgetOp operate, const UiController &controller, 129 const UiDriveOptions &options) 130 { 131 const int32_t cx = bounds.GetCenterX(); 132 const int32_t cy = bounds.GetCenterY(); 133 switch (operate) { 134 case WidgetOp::CLICK: 135 InjectGenericClick(PointerOp::CLICK_P, {cx, cy}, controller, options); 136 break; 137 case WidgetOp::LONG_CLICK: 138 InjectGenericClick(PointerOp::LONG_CLICK_P, {cx, cy}, controller, options); 139 break; 140 case WidgetOp::DOUBLE_CLICK: 141 InjectGenericClick(PointerOp::DOUBLE_CLICK_P, {cx, cy}, controller, options); 142 break; 143 case WidgetOp::SWIPE_L2R: 144 InjectGenericSwipe(PointerOp::SWIPE_P, {bounds.left_, cy}, {bounds.right_, cy}, controller, options); 145 break; 146 case WidgetOp::SWIPE_R2L: 147 InjectGenericSwipe(PointerOp::SWIPE_P, {bounds.right_, cy}, {bounds.left_, cy}, controller, options); 148 break; 149 case WidgetOp::SWIPE_T2B: 150 InjectGenericSwipe(PointerOp::SWIPE_P, {cx, bounds.top_}, {cx, bounds.bottom_}, controller, options); 151 break; 152 case WidgetOp::SWIPE_B2T: 153 InjectGenericSwipe(PointerOp::SWIPE_P, {cx, bounds.bottom_}, {cx, bounds.top_}, controller, options); 154 break; 155 } 156 } 157 InjectKeyAction(const KeyAction & action,const UiController & controller,const UiDriveOptions & options)158 static void InjectKeyAction(const KeyAction &action, const UiController &controller, const UiDriveOptions &options) 159 { 160 vector<KeyEvent> events; 161 action.ComputeEvents(events, options); 162 if (events.empty()) { return; } 163 controller.InjectKeyEventSequence(events); 164 controller.WaitForUiSteady(options.uiSteadyThresholdMs_, options.waitUiSteadyMaxMs_); 165 } 166 FindScrollWidget(const WidgetImage & img) const167 const Widget *UiDriver::FindScrollWidget(const WidgetImage &img) const 168 { 169 vector<reference_wrapper<const Widget>> recv; 170 static constexpr string_view attrType = ATTR_NAMES[UiAttr::TYPE]; 171 // scrollable widget usually has unique type on a UI frame, some find it by type 172 auto typeMatcher = WidgetAttrMatcher(attrType, img.GetAttribute(attrType, ""), EQ); 173 auto visitor = MatchedWidgetCollector(typeMatcher, recv); 174 widgetTree_->DfsTraverse(visitor); 175 if (recv.empty()) { 176 return nullptr; 177 } 178 return &(recv.at(0).get()); 179 } 180 UiDriver(string_view device)181 UiDriver::UiDriver(string_view device) : deviceName_(device) {} 182 TriggerKey(const KeyAction & action,ApiCallErr & error)183 void UiDriver::TriggerKey(const KeyAction &action, ApiCallErr &error) 184 { 185 UpdateUi(false, error); 186 if (error.code_ != NO_ERROR) { 187 return; 188 } 189 InjectKeyAction(action, *uiController_, options_); 190 } 191 PerformWidgetOperate(const WidgetImage & image,WidgetOp type,ApiCallErr & error)192 void UiDriver::PerformWidgetOperate(const WidgetImage &image, WidgetOp type, ApiCallErr &error) 193 { 194 auto widget = RetrieveWidget(image, error); 195 if (widget == nullptr || error.code_ != NO_ERROR) { 196 return; 197 } 198 InjectWidgetOperate(widget->GetBounds(), type, *uiController_, options_); 199 } 200 InputText(const WidgetImage & image,string_view text,ApiCallErr & error)201 void UiDriver::InputText(const WidgetImage &image, string_view text, ApiCallErr &error) 202 { 203 auto widget = RetrieveWidget(image, error); 204 if (widget == nullptr || error.code_ != NO_ERROR) { 205 return; 206 } 207 LOG_D("Injecting string '%{public}s' into widget: %{public}s", text.data(), widget->ToStr().c_str()); 208 #ifdef __DOUBLE_FRAMEWORK__ 209 // click on the target widget to gain focus 210 InjectWidgetOperate(widget->GetBounds(), WidgetOp::CLICK, *uiController_, options_); 211 // set text to clipboard 212 uiController_->PutTextToClipboard(text); 213 // trigger Ctrl+V to paste text 214 auto pasteKey = Paste(); 215 InjectKeyAction(pasteKey, *uiController_, options_); 216 #else 217 vector<char> chars(text.begin(), text.end()); // decompose to sing-char input sequence 218 static constexpr char charDelete = 0x7F; 219 chars.insert(chars.begin(), charDelete); 220 vector<pair<int32_t, int32_t>> keyCodes; 221 for (auto ch: chars) { 222 int32_t code = KEYCODE_NONE; 223 int32_t ctrlCode = KEYCODE_NONE; 224 if (!uiController_->GetCharKeyCode(ch, code, ctrlCode)) { 225 error = ApiCallErr(USAGE_ERROR, string("Cannot input char ") + ch); 226 return; 227 } 228 keyCodes.emplace_back(make_pair(code, ctrlCode)); 229 } 230 InjectWidgetOperate(widget->GetBounds(), WidgetOp::CLICK, *uiController_, options_); 231 static constexpr uint32_t focusTimeMs = 200; 232 static constexpr uint32_t typeCharTimeMs = 50; 233 DelayMs(focusTimeMs); // short delay to ensure focus gaining 234 vector<KeyEvent> events; 235 for (auto &pair : keyCodes) { 236 if (pair.second != KEYCODE_NONE) { 237 events.emplace_back(KeyEvent {ActionStage::DOWN, pair.second, 0}); 238 } 239 events.emplace_back(KeyEvent {ActionStage::DOWN, pair.first, typeCharTimeMs}); 240 events.emplace_back(KeyEvent {ActionStage::UP, pair.first, 0}); 241 if (pair.second != KEYCODE_NONE) { 242 events.emplace_back(KeyEvent {ActionStage::UP, pair.second, 0}); 243 } 244 uiController_->InjectKeyEventSequence(events); 245 events.clear(); 246 } 247 uiController_->WaitForUiSteady(options_.uiSteadyThresholdMs_, options_.waitUiSteadyMaxMs_); 248 #endif 249 } 250 TakeScopeUiSnapshot(const WidgetTree & tree,const Widget & root)251 static string TakeScopeUiSnapshot(const WidgetTree &tree, const Widget &root) 252 { 253 stringstream os; 254 TreeSnapshotTaker snapshotTaker(os); 255 tree.DfsTraverseDescendants(snapshotTaker, root); 256 return os.str(); 257 } 258 ScrollSearch(const WidgetImage & img,const WidgetSelector & selector,ApiCallErr & err,int32_t deadZoneSize)259 unique_ptr<WidgetImage> UiDriver::ScrollSearch(const WidgetImage &img, const WidgetSelector &selector, 260 ApiCallErr &err, int32_t deadZoneSize) 261 { 262 vector<TouchEvent> scrollEvents; 263 bool scrollingUp = true; 264 string prevSnapshot; 265 vector<reference_wrapper<const Widget>> receiver; 266 while (true) { 267 auto scrollWidget = RetrieveWidget(img, err); 268 if (scrollWidget == nullptr) { 269 scrollWidget = FindScrollWidget(img); 270 if (scrollWidget != nullptr) { 271 err = ApiCallErr(NO_ERROR); 272 } 273 } 274 if (scrollWidget == nullptr || err.code_ != NO_ERROR) { 275 return nullptr; 276 } 277 selector.Select(*widgetTree_, receiver); 278 if (!receiver.empty()) { 279 auto image = make_unique<WidgetImage>(); 280 Widget2Image(receiver.at(0), *image, selector); 281 return image; 282 } 283 string snapshot = TakeScopeUiSnapshot(*widgetTree_, *scrollWidget); 284 if (snapshot == prevSnapshot) { 285 // scrolling down to bottom, search completed with failure 286 if (!scrollingUp) { 287 auto msg = string("Scroll search widget failed: ") + selector.Describe(); 288 LOG_W("%{public}s", msg.c_str()); 289 return nullptr; 290 } else { 291 // scrolling down to top, change direction 292 scrollingUp = false; 293 } 294 } 295 prevSnapshot = snapshot; 296 // execute scrolling on the scroll_widget without update UI 297 const auto type = scrollingUp ? WidgetOp::SWIPE_T2B : WidgetOp::SWIPE_B2T; 298 auto bounds = scrollWidget->GetBounds(); 299 if (deadZoneSize > 0) { 300 // scroll widget from its deadZone maybe unresponsive 301 bounds.top_ += deadZoneSize; 302 bounds.bottom_ -= deadZoneSize; 303 } 304 InjectWidgetOperate(bounds, type, *uiController_, options_); 305 } 306 } 307 DragWidgetToAnother(const WidgetImage & imgFrom,const WidgetImage & imgTo,ApiCallErr & err)308 void UiDriver::DragWidgetToAnother(const WidgetImage &imgFrom, const WidgetImage &imgTo, ApiCallErr &err) 309 { 310 auto widgetFrom = RetrieveWidget(imgFrom, err); 311 if (widgetFrom == nullptr || err.code_ != NO_ERROR) { 312 return; 313 } 314 auto widgetTo = RetrieveWidget(imgTo, err, false); 315 if (widgetTo == nullptr || err.code_ != NO_ERROR) { 316 return; 317 } 318 auto boundsFrom = widgetFrom->GetBounds(); 319 auto boundsTo = widgetTo->GetBounds(); 320 auto centerFrom = Point {boundsFrom.GetCenterX(), boundsFrom.GetCenterY()}; 321 auto centerTo = Point {boundsTo.GetCenterX(), boundsTo.GetCenterY()}; 322 InjectGenericSwipe(PointerOp::DRAG_P, centerFrom, centerTo, *uiController_, options_); 323 } 324 FindWidgets(const WidgetSelector & select,vector<unique_ptr<WidgetImage>> & rev,ApiCallErr & err)325 void UiDriver::FindWidgets(const WidgetSelector &select, vector<unique_ptr<WidgetImage>> &rev, ApiCallErr &err) 326 { 327 UpdateUi(true, err); 328 if (err.code_ != NO_ERROR) { 329 return; 330 } 331 vector<reference_wrapper<const Widget>> widgets; 332 select.Select(*widgetTree_, widgets); 333 // covert widgets to images as return value 334 uint32_t index = 0; 335 for (auto &ref:widgets) { 336 auto image = make_unique<WidgetImage>(); 337 Widget2Image(ref.get(), *image, select); 338 // at sometime, more than one widgets are found, add the node index to the description 339 auto origDesc = image->GetSelectionDesc(); 340 auto newDesc = origDesc + "(index=" + to_string(index) + ")"; 341 image->SetSelectionDesc(newDesc); 342 rev.emplace_back(move(image)); 343 index++; 344 } 345 } 346 WaitForWidget(const WidgetSelector & select,uint32_t maxMs,ApiCallErr & err)347 unique_ptr<WidgetImage> UiDriver::WaitForWidget(const WidgetSelector &select, uint32_t maxMs, ApiCallErr &err) 348 { 349 const uint32_t sliceMs = 20; 350 const auto startMs = GetCurrentMillisecond(); 351 vector<unique_ptr<WidgetImage>> receiver; 352 do { 353 FindWidgets(select, receiver, err); 354 if (err.code_ != NO_ERROR) { // abort on error 355 return nullptr; 356 } 357 if (!receiver.empty()) { 358 return move(receiver.at(0)); 359 } 360 DelayMs(sliceMs); 361 } while (GetCurrentMillisecond() - startMs < maxMs); 362 return nullptr; 363 } 364 UpdateWidgetImage(WidgetImage & image,ApiCallErr & error)365 void UiDriver::UpdateWidgetImage(WidgetImage &image, ApiCallErr &error) 366 { 367 auto widget = RetrieveWidget(image, error); 368 if (widget == nullptr || error.code_ != NO_ERROR) { 369 return; 370 } 371 auto selectionDesc = image.GetSelectionDesc(); 372 WidgetSelector selector; // dummy selector 373 Widget2Image(*widget, image, selector); 374 image.SetSelectionDesc(selectionDesc); 375 } 376 DelayMs(uint32_t ms)377 void UiDriver::DelayMs(uint32_t ms) 378 { 379 if (ms > 0) { 380 this_thread::sleep_for(chrono::milliseconds(ms)); 381 } 382 } 383 PerformGenericClick(PointerOp type,const Point & point,ApiCallErr & err)384 void UiDriver::PerformGenericClick(PointerOp type, const Point &point, ApiCallErr &err) 385 { 386 UpdateUi(false, err); 387 if (err.code_ != NO_ERROR) { 388 return; 389 } 390 InjectGenericClick(type, point, *uiController_, options_); 391 } 392 PerformGenericSwipe(PointerOp type,const Point & fromPoint,const Point & toPoint,ApiCallErr & err)393 void UiDriver::PerformGenericSwipe(PointerOp type, const Point &fromPoint, const Point &toPoint, ApiCallErr &err) 394 { 395 UpdateUi(false, err); 396 if (err.code_ != NO_ERROR) { 397 return; 398 } 399 InjectGenericSwipe(type, fromPoint, toPoint, *uiController_, options_); 400 } 401 TakeScreenCap(string_view savePath,ApiCallErr & err)402 void UiDriver::TakeScreenCap(string_view savePath, ApiCallErr &err) 403 { 404 UpdateUi(false, err); 405 if (err.code_ != NO_ERROR) { 406 return; 407 } 408 stringstream errorRecv; 409 if (!uiController_->TakeScreenCap(savePath, errorRecv)) { 410 LOG_W("ScreenCap failed: %{public}s", errorRecv.str().c_str()); 411 } else { 412 LOG_D("ScreenCap saved to %{public}s", savePath.data()); 413 } 414 } 415 WriteIntoParcel(json & data) const416 void UiDriver::WriteIntoParcel(json &data) const 417 { 418 data["device_name"] = deviceName_; 419 json options; 420 options_.WriteIntoParcel(options); 421 data["options"] = options; 422 } 423 ReadFromParcel(const json & data)424 void UiDriver::ReadFromParcel(const json &data) 425 { 426 deviceName_ = data["device_name"]; 427 options_.ReadFromParcel(data["options"]); 428 } 429 } // namespace uitest