android.spec.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. /*jshint node: true, jasmine: true */
  2. /*
  3. *
  4. * Licensed to the Apache Software Foundation (ASF) under one
  5. * or more contributor license agreements. See the NOTICE file
  6. * distributed with this work for additional information
  7. * regarding copyright ownership. The ASF licenses this file
  8. * to you under the Apache License, Version 2.0 (the
  9. * "License"); you may not use this file except in compliance
  10. * with the License. You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing,
  15. * software distributed under the License is distributed on an
  16. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  17. * KIND, either express or implied. See the License for the
  18. * specific language governing permissions and limitations
  19. * under the License.
  20. *
  21. */
  22. // these tests are meant to be executed by Cordova ParaMedic Appium runner
  23. // you can find it here: https://github.com/apache/cordova-paramedic/
  24. // it is not necessary to do a full CI setup to run these tests
  25. // Run:
  26. // node cordova-paramedic/main.js --platform android --plugin cordova-plugin-camera --skipMainTests --target <emulator name>
  27. // Please note only Android 5.1 and 4.4 are supported at this point.
  28. 'use strict';
  29. var wdHelper = global.WD_HELPER;
  30. var screenshotHelper = global.SCREENSHOT_HELPER;
  31. var wd = wdHelper.getWD();
  32. var cameraConstants = require('../../www/CameraConstants');
  33. var cameraHelper = require('../helpers/cameraHelper');
  34. var MINUTE = 60 * 1000;
  35. var BACK_BUTTON = 4;
  36. var DEFAULT_SCREEN_WIDTH = 360;
  37. var DEFAULT_SCREEN_HEIGHT = 567;
  38. var DEFAULT_WEBVIEW_CONTEXT = 'WEBVIEW';
  39. var PROMISE_PREFIX = 'appium_camera_promise_';
  40. var CONTEXT_NATIVE_APP = 'NATIVE_APP';
  41. describe('Camera tests Android.', function () {
  42. var driver;
  43. // the name of webview context, it will be changed to match needed context if there are named ones:
  44. var webviewContext = DEFAULT_WEBVIEW_CONTEXT;
  45. // this indicates that the device library has the test picture:
  46. var isTestPictureSaved = false;
  47. // we need to know the screen width and height to properly click on an image in the gallery:
  48. var screenWidth = DEFAULT_SCREEN_WIDTH;
  49. var screenHeight = DEFAULT_SCREEN_HEIGHT;
  50. // promise count to use in promise ID
  51. var promiseCount = 0;
  52. // determine if Appium session is created successfully
  53. var appiumSessionStarted = false;
  54. // determine if camera is present on the device/emulator
  55. var cameraAvailable = false;
  56. // determine if emulator is within a range of acceptable resolutions able to run these tests
  57. var isResolutionBad = true;
  58. // a path to the image we add to the gallery before test run
  59. var fillerImagePath;
  60. function getNextPromiseId() {
  61. promiseCount += 1;
  62. return getCurrentPromiseId();
  63. }
  64. function getCurrentPromiseId() {
  65. return PROMISE_PREFIX + promiseCount;
  66. }
  67. function gracefullyFail(error) {
  68. fail(error);
  69. return driver
  70. .quit()
  71. .then(function () {
  72. return getDriver();
  73. });
  74. }
  75. // combinines specified options in all possible variations
  76. // you can add more options to test more scenarios
  77. function generateOptions() {
  78. var sourceTypes = [
  79. cameraConstants.PictureSourceType.CAMERA,
  80. cameraConstants.PictureSourceType.PHOTOLIBRARY
  81. ];
  82. var destinationTypes = cameraConstants.DestinationType;
  83. var encodingTypes = cameraConstants.EncodingType;
  84. var allowEditOptions = [ true, false ];
  85. var correctOrientationOptions = [ true, false ];
  86. return cameraHelper.generateSpecs(sourceTypes, destinationTypes, encodingTypes, allowEditOptions, correctOrientationOptions);
  87. }
  88. // invokes Camera.getPicture() with the specified options
  89. // and goes through all UI interactions unless 'skipUiInteractions' is true
  90. function getPicture(options, skipUiInteractions) {
  91. var promiseId = getNextPromiseId();
  92. if (!options) {
  93. options = {};
  94. }
  95. return driver
  96. .context(webviewContext)
  97. .execute(cameraHelper.getPicture, [options, promiseId])
  98. .context(CONTEXT_NATIVE_APP)
  99. .then(function () {
  100. if (skipUiInteractions) {
  101. return;
  102. }
  103. // selecting a picture from gallery
  104. if (options.hasOwnProperty('sourceType') &&
  105. (options.sourceType === cameraConstants.PictureSourceType.PHOTOLIBRARY ||
  106. options.sourceType === cameraConstants.PictureSourceType.SAVEDPHOTOALBUM)) {
  107. var tapTile = new wd.TouchAction();
  108. var swipeRight = new wd.TouchAction();
  109. tapTile
  110. .tap({
  111. x: Math.round(screenWidth / 4),
  112. y: Math.round(screenHeight / 4)
  113. });
  114. swipeRight
  115. .press({x: 10, y: Math.round(screenHeight / 4)})
  116. .wait(300)
  117. .moveTo({x: Math.round(screenWidth - (screenWidth / 8)), y: 0})
  118. .wait(1500)
  119. .release()
  120. .wait(1000);
  121. if (options.allowEdit) {
  122. return driver
  123. // always wait before performing touchAction
  124. .sleep(7000)
  125. .performTouchAction(tapTile);
  126. }
  127. return driver
  128. .waitForElementByAndroidUIAutomator('new UiSelector().text("Gallery");', 20000)
  129. .fail(function () {
  130. // If the Gallery button is not present, swipe right to reveal the Gallery button!
  131. return driver
  132. .performTouchAction(swipeRight)
  133. .waitForElementByAndroidUIAutomator('new UiSelector().text("Gallery");', 20000)
  134. })
  135. .click()
  136. // always wait before performing touchAction
  137. .sleep(7000)
  138. .performTouchAction(tapTile);
  139. }
  140. // taking a picture from camera
  141. return driver
  142. .waitForElementByAndroidUIAutomator('new UiSelector().resourceIdMatches(".*shutter.*")', MINUTE / 2)
  143. .click()
  144. .waitForElementByAndroidUIAutomator('new UiSelector().resourceIdMatches(".*done.*")', MINUTE / 2)
  145. .click();
  146. })
  147. .then(function () {
  148. if (skipUiInteractions) {
  149. return;
  150. }
  151. if (options.allowEdit) {
  152. return driver
  153. .waitForElementByAndroidUIAutomator('new UiSelector().text("Save")', MINUTE)
  154. .click();
  155. }
  156. })
  157. .fail(function (failure) {
  158. throw failure;
  159. });
  160. }
  161. // checks if the picture was successfully taken
  162. // if shouldLoad is falsy, ensures that the error callback was called
  163. function checkPicture(shouldLoad, options) {
  164. if (!options) {
  165. options = {};
  166. }
  167. return driver
  168. .context(webviewContext)
  169. .setAsyncScriptTimeout(MINUTE / 2)
  170. .executeAsync(cameraHelper.checkPicture, [getCurrentPromiseId(), options])
  171. .then(function (result) {
  172. if (shouldLoad) {
  173. if (result !== 'OK') {
  174. fail(result);
  175. }
  176. } else if (result.indexOf('ERROR') === -1) {
  177. throw 'Unexpected success callback with result: ' + result;
  178. }
  179. });
  180. }
  181. // deletes the latest image from the gallery
  182. function deleteImage() {
  183. var holdTile = new wd.TouchAction();
  184. holdTile
  185. .press({x: Math.round(screenWidth / 4), y: Math.round(screenHeight / 5)})
  186. .wait(1000)
  187. .release();
  188. return driver
  189. // always wait before performing touchAction
  190. .sleep(7000)
  191. .performTouchAction(holdTile)
  192. .elementByAndroidUIAutomator('new UiSelector().text("Delete")')
  193. .then(function (element) {
  194. return element
  195. .click()
  196. .elementByAndroidUIAutomator('new UiSelector().text("OK")')
  197. .click();
  198. }, function () {
  199. // couldn't find Delete menu item. Possibly there is no image.
  200. return driver;
  201. });
  202. }
  203. function getDriver() {
  204. driver = wdHelper.getDriver('Android');
  205. return driver.getWebviewContext()
  206. .then(function(context) {
  207. webviewContext = context;
  208. return driver.context(webviewContext);
  209. })
  210. .waitForDeviceReady()
  211. .injectLibraries()
  212. .deleteFillerImage(fillerImagePath)
  213. .then(function () {
  214. fillerImagePath = null;
  215. })
  216. .addFillerImage()
  217. .then(function (result) {
  218. if (result && result.indexOf('ERROR:') === 0) {
  219. throw new Error(result);
  220. } else {
  221. fillerImagePath = result;
  222. }
  223. });
  224. }
  225. function recreateSession() {
  226. return driver
  227. .quit()
  228. .finally(function () {
  229. return getDriver();
  230. });
  231. }
  232. function tryRunSpec(spec) {
  233. return driver
  234. .then(spec)
  235. .fail(function () {
  236. return recreateSession()
  237. .then(spec)
  238. .fail(function() {
  239. return recreateSession()
  240. .then(spec);
  241. });
  242. })
  243. .fail(gracefullyFail);
  244. }
  245. // produces a generic spec function which
  246. // takes a picture with specified options
  247. // and then verifies it
  248. function generateSpec(options) {
  249. return function () {
  250. return driver
  251. .then(function () {
  252. return getPicture(options);
  253. })
  254. .then(function () {
  255. return checkPicture(true, options);
  256. });
  257. };
  258. }
  259. function checkSession(done, skipResolutionCheck) {
  260. if (!appiumSessionStarted) {
  261. fail('Failed to start a session ' + (lastFailureReason ? lastFailureReason : ''));
  262. done();
  263. }
  264. if (!skipResolutionCheck && isResolutionBad) {
  265. fail('The resolution of this target device is not within the appropriate range of width: blah-blah and height: bleh-bleh. The target\'s current resolution is: ' + isResolutionBad);
  266. }
  267. }
  268. function checkCamera(pending) {
  269. if (!cameraAvailable) {
  270. pending('This test requires a functioning camera on the Android device/emulator, and this test suite\'s functional camera test failed on your target environment.');
  271. }
  272. }
  273. afterAll(function (done) {
  274. checkSession(done);
  275. driver
  276. .quit()
  277. .done(done);
  278. }, MINUTE);
  279. it('camera.ui.util configuring driver and starting a session', function (done) {
  280. getDriver()
  281. .then(function () {
  282. appiumSessionStarted = true;
  283. }, fail)
  284. .done(done);
  285. }, 10 * MINUTE);
  286. it('camera.ui.util determine screen dimensions', function (done) {
  287. checkSession(done, /*skipResolutionCheck?*/ true); // skip the resolution check here since we are about to find out in this spec!
  288. driver
  289. .context(CONTEXT_NATIVE_APP)
  290. .getWindowSize()
  291. .then(function (size) {
  292. screenWidth = Number(size.width);
  293. screenHeight = Number(size.height);
  294. isResolutionBad = false;
  295. /*
  296. TODO: what are acceptable resolution values?
  297. need to check what the emulators used in CI return.
  298. and also what local device definitions work and dont
  299. */
  300. })
  301. .done(done);
  302. }, MINUTE);
  303. it('camera.ui.util determine camera availability', function (done) {
  304. checkSession(done);
  305. var opts = {
  306. sourceType: cameraConstants.PictureSourceType.CAMERA,
  307. saveToPhotoAlbum: false
  308. };
  309. return driver
  310. .then(function () {
  311. return getPicture(opts);
  312. })
  313. .then(function () {
  314. cameraAvailable = true;
  315. }, function () {
  316. return recreateSession();
  317. })
  318. .done(done);
  319. }, 5 * MINUTE);
  320. describe('Specs.', function () {
  321. // getPicture() with saveToPhotoLibrary = true
  322. it('camera.ui.spec.1 Saving a picture to the photo library', function (done) {
  323. checkSession(done);
  324. checkCamera(pending);
  325. var spec = generateSpec({
  326. quality: 50,
  327. allowEdit: false,
  328. sourceType: cameraConstants.PictureSourceType.CAMERA,
  329. saveToPhotoAlbum: true
  330. });
  331. tryRunSpec(spec)
  332. .then(function () {
  333. isTestPictureSaved = true;
  334. })
  335. .done(done);
  336. }, 10 * MINUTE);
  337. // getPicture() with mediaType: VIDEO, sourceType: PHOTOLIBRARY
  338. it('camera.ui.spec.2 Selecting only videos', function (done) {
  339. checkSession(done);
  340. var spec = function () {
  341. var options = { sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
  342. mediaType: cameraConstants.MediaType.VIDEO };
  343. return driver
  344. .then(function () {
  345. return getPicture(options, true);
  346. })
  347. .context(CONTEXT_NATIVE_APP)
  348. .then(function () {
  349. // try to find "Gallery" menu item
  350. // if there's none, the gallery should be already opened
  351. return driver
  352. .waitForElementByAndroidUIAutomator('new UiSelector().text("Gallery")', 20000)
  353. .then(function (element) {
  354. return element.click();
  355. }, function () {
  356. return driver;
  357. });
  358. })
  359. .then(function () {
  360. // if the gallery is opened on the videos page,
  361. // there should be a "Choose video" caption
  362. return driver
  363. .elementByAndroidUIAutomator('new UiSelector().text("Choose video")')
  364. .fail(function () {
  365. throw 'Couldn\'t find "Choose video" element.';
  366. });
  367. })
  368. .deviceKeyEvent(BACK_BUTTON)
  369. .elementByAndroidUIAutomator('new UiSelector().text("Gallery")')
  370. .deviceKeyEvent(BACK_BUTTON)
  371. .finally(function () {
  372. return driver
  373. .elementById('action_bar_title')
  374. .then(function () {
  375. // success means we're still in native app
  376. return driver
  377. .deviceKeyEvent(BACK_BUTTON)
  378. // give native app some time to close
  379. .sleep(2000)
  380. // try again! because every ~30th build
  381. // on Sauce Labs this backbutton doesn't work
  382. .elementById('action_bar_title')
  383. .then(function () {
  384. // success means we're still in native app
  385. return driver
  386. .deviceKeyEvent(BACK_BUTTON);
  387. }, function () {
  388. // error means we're already in webview
  389. return driver;
  390. });
  391. }, function () {
  392. // error means we're already in webview
  393. return driver;
  394. });
  395. });
  396. };
  397. tryRunSpec(spec).done(done);
  398. }, 10 * MINUTE);
  399. // getPicture(), then dismiss
  400. // wait for the error callback to be called
  401. it('camera.ui.spec.3 Dismissing the camera', function (done) {
  402. checkSession(done);
  403. checkCamera(pending);
  404. var spec = function () {
  405. var options = {
  406. quality: 50,
  407. allowEdit: true,
  408. sourceType: cameraConstants.PictureSourceType.CAMERA,
  409. destinationType: cameraConstants.DestinationType.FILE_URI
  410. };
  411. return driver
  412. .then(function () {
  413. return getPicture(options, true);
  414. })
  415. .context(CONTEXT_NATIVE_APP)
  416. .waitForElementByAndroidUIAutomator('new UiSelector().resourceIdMatches(".*cancel.*")', MINUTE / 2)
  417. .click()
  418. .then(function () {
  419. return checkPicture(false);
  420. });
  421. };
  422. tryRunSpec(spec).done(done);
  423. }, 10 * MINUTE);
  424. // getPicture(), then take picture but dismiss the edit
  425. // wait for the error callback to be called
  426. it('camera.ui.spec.4 Dismissing the edit', function (done) {
  427. checkSession(done);
  428. checkCamera(pending);
  429. var spec = function () {
  430. var options = {
  431. quality: 50,
  432. allowEdit: true,
  433. sourceType: cameraConstants.PictureSourceType.CAMERA,
  434. destinationType: cameraConstants.DestinationType.FILE_URI
  435. };
  436. return driver
  437. .then(function () {
  438. return getPicture(options, true);
  439. })
  440. .waitForElementByAndroidUIAutomator('new UiSelector().resourceIdMatches(".*shutter.*")', MINUTE / 2)
  441. .click()
  442. .waitForElementByAndroidUIAutomator('new UiSelector().resourceIdMatches(".*done.*")', MINUTE / 2)
  443. .click()
  444. .waitForElementByAndroidUIAutomator('new UiSelector().resourceIdMatches(".*discard.*")', MINUTE / 2)
  445. .click()
  446. .then(function () {
  447. return checkPicture(false);
  448. });
  449. };
  450. tryRunSpec(spec).done(done);
  451. }, 10 * MINUTE);
  452. it('camera.ui.spec.5 Verifying target image size, sourceType=CAMERA', function (done) {
  453. checkSession(done);
  454. checkCamera(pending);
  455. var spec = generateSpec({
  456. quality: 50,
  457. allowEdit: false,
  458. sourceType: cameraConstants.PictureSourceType.CAMERA,
  459. saveToPhotoAlbum: false,
  460. targetWidth: 210,
  461. targetHeight: 210
  462. });
  463. tryRunSpec(spec).done(done);
  464. }, 10 * MINUTE);
  465. it('camera.ui.spec.6 Verifying target image size, sourceType=PHOTOLIBRARY', function (done) {
  466. checkSession(done);
  467. var spec = generateSpec({
  468. quality: 50,
  469. allowEdit: false,
  470. sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
  471. saveToPhotoAlbum: false,
  472. targetWidth: 210,
  473. targetHeight: 210
  474. });
  475. tryRunSpec(spec).done(done);
  476. }, 10 * MINUTE);
  477. it('camera.ui.spec.7 Verifying target image size, sourceType=CAMERA, DestinationType=NATIVE_URI', function (done) {
  478. checkSession(done);
  479. checkCamera(pending);
  480. var spec = generateSpec({
  481. quality: 50,
  482. allowEdit: false,
  483. sourceType: cameraConstants.PictureSourceType.CAMERA,
  484. destinationType: cameraConstants.DestinationType.NATIVE_URI,
  485. saveToPhotoAlbum: false,
  486. targetWidth: 210,
  487. targetHeight: 210
  488. });
  489. tryRunSpec(spec).done(done);
  490. }, 10 * MINUTE);
  491. it('camera.ui.spec.8 Verifying target image size, sourceType=PHOTOLIBRARY, DestinationType=NATIVE_URI', function (done) {
  492. checkSession(done);
  493. var spec = generateSpec({
  494. quality: 50,
  495. allowEdit: false,
  496. sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
  497. destinationType: cameraConstants.DestinationType.NATIVE_URI,
  498. saveToPhotoAlbum: false,
  499. targetWidth: 210,
  500. targetHeight: 210
  501. });
  502. tryRunSpec(spec).done(done);
  503. }, 10 * MINUTE);
  504. it('camera.ui.spec.9 Verifying target image size, sourceType=CAMERA, DestinationType=NATIVE_URI, quality=100', function (done) {
  505. checkSession(done);
  506. checkCamera(pending);
  507. var spec = generateSpec({
  508. quality: 100,
  509. allowEdit: true,
  510. sourceType: cameraConstants.PictureSourceType.CAMERA,
  511. destinationType: cameraConstants.DestinationType.NATIVE_URI,
  512. saveToPhotoAlbum: false,
  513. targetWidth: 305,
  514. targetHeight: 305
  515. });
  516. tryRunSpec(spec).done(done);
  517. }, 10 * MINUTE);
  518. it('camera.ui.spec.10 Verifying target image size, sourceType=PHOTOLIBRARY, DestinationType=NATIVE_URI, quality=100', function (done) {
  519. checkSession(done);
  520. var spec = generateSpec({
  521. quality: 100,
  522. allowEdit: true,
  523. sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
  524. destinationType: cameraConstants.DestinationType.NATIVE_URI,
  525. saveToPhotoAlbum: false,
  526. targetWidth: 305,
  527. targetHeight: 305
  528. });
  529. tryRunSpec(spec).done(done);
  530. }, 10 * MINUTE);
  531. // combine various options for getPicture()
  532. generateOptions().forEach(function (spec) {
  533. it('camera.ui.spec.11.' + spec.id + ' Combining options. ' + spec.description, function (done) {
  534. checkSession(done);
  535. if (spec.options.sourceType == cameraConstants.PictureSourceType.CAMERA) {
  536. checkCamera(pending);
  537. }
  538. var s = generateSpec(spec.options);
  539. tryRunSpec(s).done(done);
  540. }, 10 * MINUTE);
  541. });
  542. it('camera.ui.util Delete filler picture from device library', function (done) {
  543. driver
  544. .context(webviewContext)
  545. .deleteFillerImage(fillerImagePath)
  546. .done(done);
  547. }, MINUTE);
  548. it('camera.ui.util Delete taken picture from device library', function (done) {
  549. checkSession(done);
  550. if (!isTestPictureSaved) {
  551. // couldn't save test picture earlier, so nothing to delete here
  552. done();
  553. return;
  554. }
  555. // delete exactly one latest picture
  556. // this should be the picture we've taken in the first spec
  557. driver
  558. .context(CONTEXT_NATIVE_APP)
  559. .deviceKeyEvent(BACK_BUTTON)
  560. .sleep(1000)
  561. .deviceKeyEvent(BACK_BUTTON)
  562. .sleep(1000)
  563. .deviceKeyEvent(BACK_BUTTON)
  564. .elementById('Apps')
  565. .click()
  566. .elementByAndroidUIAutomator('new UiSelector().text("Gallery")')
  567. .click()
  568. .elementByAndroidUIAutomator('new UiSelector().textContains("Pictures")')
  569. .click()
  570. .then(deleteImage)
  571. .deviceKeyEvent(BACK_BUTTON)
  572. .sleep(1000)
  573. .deviceKeyEvent(BACK_BUTTON)
  574. .sleep(1000)
  575. .deviceKeyEvent(BACK_BUTTON)
  576. .fail(fail)
  577. .finally(done);
  578. }, 3 * MINUTE);
  579. });
  580. });