You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

672 lines
27 KiB

  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. .then(function () {
  213. var options = {
  214. quality: 50,
  215. allowEdit: false,
  216. sourceType: cameraConstants.PictureSourceType.SAVEDPHOTOALBUM,
  217. saveToPhotoAlbum: false,
  218. targetWidth: 210,
  219. targetHeight: 210
  220. };
  221. return driver
  222. .then(function () { return getPicture(options, true); })
  223. .context(CONTEXT_NATIVE_APP)
  224. // case insensitive select, will be handy with Android 7 support
  225. .elementByXPath('//android.widget.Button[translate(@text, "alow", "ALOW")="ALLOW"]')
  226. .click()
  227. .fail(function noAlert() { })
  228. .deviceKeyEvent(BACK_BUTTON)
  229. .sleep(2000)
  230. .elementById('action_bar_title')
  231. .then(function () {
  232. // success means we're still in native app
  233. return driver
  234. .deviceKeyEvent(BACK_BUTTON);
  235. }, function () {
  236. // error means we're already in webview
  237. return driver;
  238. });
  239. })
  240. .then(function () {
  241. // doing it inside a function because otherwise
  242. // it would not hook up to the webviewContext var change
  243. // in the first methods of this chain
  244. return driver.context(webviewContext);
  245. })
  246. .deleteFillerImage(fillerImagePath)
  247. .then(function () {
  248. fillerImagePath = null;
  249. })
  250. .addFillerImage()
  251. .then(function (result) {
  252. if (result && result.indexOf('ERROR:') === 0) {
  253. throw new Error(result);
  254. } else {
  255. fillerImagePath = result;
  256. }
  257. });
  258. }
  259. function recreateSession() {
  260. return driver
  261. .quit()
  262. .finally(function () {
  263. return getDriver();
  264. });
  265. }
  266. function tryRunSpec(spec) {
  267. return driver
  268. .then(spec)
  269. .fail(function () {
  270. return recreateSession()
  271. .then(spec)
  272. .fail(function() {
  273. return recreateSession()
  274. .then(spec);
  275. });
  276. })
  277. .fail(gracefullyFail);
  278. }
  279. // produces a generic spec function which
  280. // takes a picture with specified options
  281. // and then verifies it
  282. function generateSpec(options) {
  283. return function () {
  284. return driver
  285. .then(function () {
  286. return getPicture(options);
  287. })
  288. .then(function () {
  289. return checkPicture(true, options);
  290. });
  291. };
  292. }
  293. function checkSession(done, skipResolutionCheck) {
  294. if (!appiumSessionStarted) {
  295. fail('Failed to start a session ' + (lastFailureReason ? lastFailureReason : ''));
  296. done();
  297. }
  298. if (!skipResolutionCheck && isResolutionBad) {
  299. 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);
  300. }
  301. }
  302. function checkCamera(pending) {
  303. if (!cameraAvailable) {
  304. 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.');
  305. }
  306. }
  307. afterAll(function (done) {
  308. checkSession(done);
  309. driver
  310. .quit()
  311. .done(done);
  312. }, MINUTE);
  313. it('camera.ui.util configuring driver and starting a session', function (done) {
  314. getDriver()
  315. .then(function () {
  316. appiumSessionStarted = true;
  317. }, fail)
  318. .done(done);
  319. }, 10 * MINUTE);
  320. it('camera.ui.util determine screen dimensions', function (done) {
  321. checkSession(done, /*skipResolutionCheck?*/ true); // skip the resolution check here since we are about to find out in this spec!
  322. driver
  323. .context(CONTEXT_NATIVE_APP)
  324. .getWindowSize()
  325. .then(function (size) {
  326. screenWidth = Number(size.width);
  327. screenHeight = Number(size.height);
  328. isResolutionBad = false;
  329. /*
  330. TODO: what are acceptable resolution values?
  331. need to check what the emulators used in CI return.
  332. and also what local device definitions work and dont
  333. */
  334. })
  335. .done(done);
  336. }, MINUTE);
  337. it('camera.ui.util determine camera availability', function (done) {
  338. checkSession(done);
  339. var opts = {
  340. sourceType: cameraConstants.PictureSourceType.CAMERA,
  341. saveToPhotoAlbum: false
  342. };
  343. return driver
  344. .then(function () {
  345. return getPicture(opts);
  346. })
  347. .then(function () {
  348. cameraAvailable = true;
  349. }, function () {
  350. return recreateSession();
  351. })
  352. .done(done);
  353. }, 5 * MINUTE);
  354. describe('Specs.', function () {
  355. // getPicture() with saveToPhotoLibrary = true
  356. it('camera.ui.spec.1 Saving a picture to the photo library', function (done) {
  357. checkSession(done);
  358. checkCamera(pending);
  359. var spec = generateSpec({
  360. quality: 50,
  361. allowEdit: false,
  362. sourceType: cameraConstants.PictureSourceType.CAMERA,
  363. saveToPhotoAlbum: true
  364. });
  365. tryRunSpec(spec)
  366. .then(function () {
  367. isTestPictureSaved = true;
  368. })
  369. .done(done);
  370. }, 10 * MINUTE);
  371. // getPicture() with mediaType: VIDEO, sourceType: PHOTOLIBRARY
  372. it('camera.ui.spec.2 Selecting only videos', function (done) {
  373. checkSession(done);
  374. var spec = function () {
  375. var options = { sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
  376. mediaType: cameraConstants.MediaType.VIDEO };
  377. return driver
  378. .then(function () {
  379. return getPicture(options, true);
  380. })
  381. .context(CONTEXT_NATIVE_APP)
  382. .then(function () {
  383. // try to find "Gallery" menu item
  384. // if there's none, the gallery should be already opened
  385. return driver
  386. .waitForElementByAndroidUIAutomator('new UiSelector().text("Gallery")', 20000)
  387. .then(function (element) {
  388. return element.click();
  389. }, function () {
  390. return driver;
  391. });
  392. })
  393. .then(function () {
  394. // if the gallery is opened on the videos page,
  395. // there should be a "Choose video" caption
  396. return driver
  397. .elementByAndroidUIAutomator('new UiSelector().text("Choose video")')
  398. .fail(function () {
  399. throw 'Couldn\'t find a "Choose video" element.';
  400. });
  401. })
  402. .deviceKeyEvent(BACK_BUTTON)
  403. .elementByAndroidUIAutomator('new UiSelector().text("Gallery")')
  404. .deviceKeyEvent(BACK_BUTTON)
  405. .finally(function () {
  406. return driver
  407. .elementById('action_bar_title')
  408. .then(function () {
  409. // success means we're still in native app
  410. return driver
  411. .deviceKeyEvent(BACK_BUTTON)
  412. // give native app some time to close
  413. .sleep(2000)
  414. // try again! because every ~30th build
  415. // on Sauce Labs this backbutton doesn't work
  416. .elementById('action_bar_title')
  417. .then(function () {
  418. // success means we're still in native app
  419. return driver
  420. .deviceKeyEvent(BACK_BUTTON);
  421. }, function () {
  422. // error means we're already in webview
  423. return driver;
  424. });
  425. }, function () {
  426. // error means we're already in webview
  427. return driver;
  428. });
  429. });
  430. };
  431. tryRunSpec(spec).done(done);
  432. }, 10 * MINUTE);
  433. // getPicture(), then dismiss
  434. // wait for the error callback to be called
  435. it('camera.ui.spec.3 Dismissing the camera', function (done) {
  436. checkSession(done);
  437. checkCamera(pending);
  438. var spec = function () {
  439. var options = {
  440. quality: 50,
  441. allowEdit: true,
  442. sourceType: cameraConstants.PictureSourceType.CAMERA,
  443. destinationType: cameraConstants.DestinationType.FILE_URI
  444. };
  445. return driver
  446. .then(function () {
  447. return getPicture(options, true);
  448. })
  449. .context(CONTEXT_NATIVE_APP)
  450. .waitForElementByAndroidUIAutomator('new UiSelector().resourceIdMatches(".*cancel.*")', MINUTE / 2)
  451. .click()
  452. .then(function () {
  453. return checkPicture(false);
  454. });
  455. };
  456. tryRunSpec(spec).done(done);
  457. }, 10 * MINUTE);
  458. // getPicture(), then take picture but dismiss the edit
  459. // wait for the error callback to be called
  460. it('camera.ui.spec.4 Dismissing the edit', function (done) {
  461. checkSession(done);
  462. checkCamera(pending);
  463. var spec = function () {
  464. var options = {
  465. quality: 50,
  466. allowEdit: true,
  467. sourceType: cameraConstants.PictureSourceType.CAMERA,
  468. destinationType: cameraConstants.DestinationType.FILE_URI
  469. };
  470. return driver
  471. .then(function () {
  472. return getPicture(options, true);
  473. })
  474. .waitForElementByAndroidUIAutomator('new UiSelector().resourceIdMatches(".*shutter.*")', MINUTE / 2)
  475. .click()
  476. .waitForElementByAndroidUIAutomator('new UiSelector().resourceIdMatches(".*done.*")', MINUTE / 2)
  477. .click()
  478. .waitForElementByAndroidUIAutomator('new UiSelector().resourceIdMatches(".*discard.*")', MINUTE / 2)
  479. .click()
  480. .then(function () {
  481. return checkPicture(false);
  482. });
  483. };
  484. tryRunSpec(spec).done(done);
  485. }, 10 * MINUTE);
  486. it('camera.ui.spec.5 Verifying target image size, sourceType=CAMERA', function (done) {
  487. checkSession(done);
  488. checkCamera(pending);
  489. var spec = generateSpec({
  490. quality: 50,
  491. allowEdit: false,
  492. sourceType: cameraConstants.PictureSourceType.CAMERA,
  493. saveToPhotoAlbum: false,
  494. targetWidth: 210,
  495. targetHeight: 210
  496. });
  497. tryRunSpec(spec).done(done);
  498. }, 10 * MINUTE);
  499. it('camera.ui.spec.6 Verifying target image size, sourceType=PHOTOLIBRARY', function (done) {
  500. checkSession(done);
  501. var spec = generateSpec({
  502. quality: 50,
  503. allowEdit: false,
  504. sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
  505. saveToPhotoAlbum: false,
  506. targetWidth: 210,
  507. targetHeight: 210
  508. });
  509. tryRunSpec(spec).done(done);
  510. }, 10 * MINUTE);
  511. it('camera.ui.spec.7 Verifying target image size, sourceType=CAMERA, DestinationType=NATIVE_URI', function (done) {
  512. checkSession(done);
  513. checkCamera(pending);
  514. var spec = generateSpec({
  515. quality: 50,
  516. allowEdit: false,
  517. sourceType: cameraConstants.PictureSourceType.CAMERA,
  518. destinationType: cameraConstants.DestinationType.NATIVE_URI,
  519. saveToPhotoAlbum: false,
  520. targetWidth: 210,
  521. targetHeight: 210
  522. });
  523. tryRunSpec(spec).done(done);
  524. }, 10 * MINUTE);
  525. it('camera.ui.spec.8 Verifying target image size, sourceType=PHOTOLIBRARY, DestinationType=NATIVE_URI', function (done) {
  526. checkSession(done);
  527. var spec = generateSpec({
  528. quality: 50,
  529. allowEdit: false,
  530. sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
  531. destinationType: cameraConstants.DestinationType.NATIVE_URI,
  532. saveToPhotoAlbum: false,
  533. targetWidth: 210,
  534. targetHeight: 210
  535. });
  536. tryRunSpec(spec).done(done);
  537. }, 10 * MINUTE);
  538. it('camera.ui.spec.9 Verifying target image size, sourceType=CAMERA, DestinationType=NATIVE_URI, quality=100', function (done) {
  539. checkSession(done);
  540. checkCamera(pending);
  541. var spec = generateSpec({
  542. quality: 100,
  543. allowEdit: true,
  544. sourceType: cameraConstants.PictureSourceType.CAMERA,
  545. destinationType: cameraConstants.DestinationType.NATIVE_URI,
  546. saveToPhotoAlbum: false,
  547. targetWidth: 305,
  548. targetHeight: 305
  549. });
  550. tryRunSpec(spec).done(done);
  551. }, 10 * MINUTE);
  552. it('camera.ui.spec.10 Verifying target image size, sourceType=PHOTOLIBRARY, DestinationType=NATIVE_URI, quality=100', function (done) {
  553. checkSession(done);
  554. var spec = generateSpec({
  555. quality: 100,
  556. allowEdit: true,
  557. sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
  558. destinationType: cameraConstants.DestinationType.NATIVE_URI,
  559. saveToPhotoAlbum: false,
  560. targetWidth: 305,
  561. targetHeight: 305
  562. });
  563. tryRunSpec(spec).done(done);
  564. }, 10 * MINUTE);
  565. // combine various options for getPicture()
  566. generateOptions().forEach(function (spec) {
  567. it('camera.ui.spec.11.' + spec.id + ' Combining options. ' + spec.description, function (done) {
  568. checkSession(done);
  569. if (spec.options.sourceType == cameraConstants.PictureSourceType.CAMERA) {
  570. checkCamera(pending);
  571. }
  572. var s = generateSpec(spec.options);
  573. tryRunSpec(s).done(done);
  574. }, 10 * MINUTE);
  575. });
  576. it('camera.ui.util Delete filler picture from device library', function (done) {
  577. driver
  578. .context(webviewContext)
  579. .deleteFillerImage(fillerImagePath)
  580. .done(done);
  581. }, MINUTE);
  582. it('camera.ui.util Delete taken picture from device library', function (done) {
  583. checkSession(done);
  584. if (!isTestPictureSaved) {
  585. // couldn't save test picture earlier, so nothing to delete here
  586. done();
  587. return;
  588. }
  589. // delete exactly one latest picture
  590. // this should be the picture we've taken in the first spec
  591. driver
  592. .context(CONTEXT_NATIVE_APP)
  593. .deviceKeyEvent(BACK_BUTTON)
  594. .sleep(1000)
  595. .deviceKeyEvent(BACK_BUTTON)
  596. .sleep(1000)
  597. .deviceKeyEvent(BACK_BUTTON)
  598. .elementById('Apps')
  599. .click()
  600. .then(function () {
  601. return driver
  602. .elementByXPath('//android.widget.Button[@text="OK"]')
  603. .click()
  604. .fail(function () {
  605. // no cling is all right
  606. // it is not a brand new emulator, then
  607. });
  608. })
  609. .elementByAndroidUIAutomator('new UiSelector().text("Gallery")')
  610. .click()
  611. .elementByAndroidUIAutomator('new UiSelector().textContains("Pictures")')
  612. .click()
  613. .then(deleteImage)
  614. .deviceKeyEvent(BACK_BUTTON)
  615. .sleep(1000)
  616. .deviceKeyEvent(BACK_BUTTON)
  617. .sleep(1000)
  618. .deviceKeyEvent(BACK_BUTTON)
  619. .fail(fail)
  620. .finally(done);
  621. }, 3 * MINUTE);
  622. });
  623. });