android.spec.js 26 KB

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