diff --git a/.github/workflows/appiumV2.yml b/.github/workflows/appiumV2_Android.yml similarity index 97% rename from .github/workflows/appiumV2.yml rename to .github/workflows/appiumV2_Android.yml index 50ea3a935..d0b92b182 100644 --- a/.github/workflows/appiumV2.yml +++ b/.github/workflows/appiumV2_Android.yml @@ -1,4 +1,4 @@ -name: Appium V2 Tests +name: Appium V2 Tests - Android on: push: diff --git a/.github/workflows/appiumV2_iOS.yml b/.github/workflows/appiumV2_iOS.yml new file mode 100644 index 000000000..3dbe3aca1 --- /dev/null +++ b/.github/workflows/appiumV2_iOS.yml @@ -0,0 +1,59 @@ +name: Appium V2 Tests - iOS + +on: + push: + branches: + - 3.x + - appium-v1-deprecation + +env: + CI: true + # Force terminal colors. @see https://www.npmjs.com/package/colors + FORCE_COLOR: 1 + +jobs: + appium1: + runs-on: ubuntu-20.04 + + strategy: + matrix: + node-version: [16.x] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: npm install --legacy-peer-deps + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true + - run: 'npm run test:ios:appium-quick' + env: # Or as an environment variable + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + + + appium2: + + runs-on: ubuntu-20.04 + + strategy: + matrix: + node-version: [16.x] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: npm install --legacy-peer-deps + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true + - run: 'npm run test:ios:appium-other' + env: # Or as an environment variable + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} diff --git a/package.json b/package.json index 7e2749809..010d04a92 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,8 @@ "test": "npm run test:unit && npm run test:runner", "test:appium-quick": "mocha test/helper/AppiumV2_test.js --grep 'quick'", "test:appium-other": "mocha test/helper/AppiumV2_test.js --grep 'second'", + "test:ios:appium-quick": "mocha test/helper/AppiumV2_ios_test.js --grep 'quick'", + "test:ios:appium-other": "mocha test/helper/AppiumV2_ios_test.js --grep 'second'", "test-app:start": "php -S 127.0.0.1:8000 -t test/data/app", "test-app:stop": "kill -9 $(lsof -t -i:8000)", "test:unit:webbapi:playwright": "mocha test/helper/Playwright_test.js", diff --git a/test/helper/AppiumV2_ios_test.js b/test/helper/AppiumV2_ios_test.js new file mode 100644 index 000000000..5595cc8e6 --- /dev/null +++ b/test/helper/AppiumV2_ios_test.js @@ -0,0 +1,202 @@ +const assert = require('assert'); +const path = require('path'); + +const Appium = require('../../lib/helper/Appium'); +const AssertionFailedError = require('../../lib/assert/error'); +const fileExists = require('../../lib/utils').fileExists; +global.codeceptjs = require('../../lib'); + +let app; +// iOS test app is built from https://github.com/appium/ios-test-app and uploaded to Saucelabs +const apk_path = 'storage:filename=TestApp-iphonesimulator.zip'; +const smallWait = 3; + +describe('Appium iOS Tests', function () { + this.timeout(0); + + before(async () => { + global.codecept_dir = path.join(__dirname, '/../data'); + app = new Appium({ + app: apk_path, + appiumV2: true, + desiredCapabilities: { + 'sauce:options': { + appiumVersion: '2.0.0', + }, + browserName: '', + recordVideo: 'false', + recordScreenshots: 'false', + platformName: 'iOS', + platformVersion: '12.2', + deviceName: 'iPhone 8 Simulator', + androidInstallTimeout: 90000, + appWaitDuration: 300000, + }, + restart: true, + protocol: 'http', + host: 'ondemand.saucelabs.com', + port: 80, + user: process.env.SAUCE_USERNAME, + key: process.env.SAUCE_ACCESS_KEY, + }); + await app._beforeSuite(); + app.isWeb = false; + await app._before(); + }); + + after(async () => { + await app._after(); + }); + + describe('app installation : #removeApp', () => { + describe( + '#grabAllContexts, #grabContext, #grabOrientation, #grabSettings', + () => { + it('should grab all available contexts for screen', async () => { + await app.resetApp(); + const val = await app.grabAllContexts(); + assert.deepEqual(val, ['NATIVE_APP']); + }); + + it('should grab current context', async () => { + const val = await app.grabContext(); + assert.equal(val, 'NATIVE_APP'); + }); + + it('should grab custom settings', async () => { + const val = await app.grabSettings(); + assert.deepEqual(val, { imageElementTapStrategy: 'w3cActions' }); + }); + }, + ); + }); + + describe('device orientation : #seeOrientationIs #setOrientation', () => { + it('should return correct status about device orientation', async () => { + await app.seeOrientationIs('PORTRAIT'); + try { + await app.seeOrientationIs('LANDSCAPE'); + } catch (e) { + e.should.be.instanceOf(AssertionFailedError); + e.inspect().should.include('expected orientation to be LANDSCAPE'); + } + }); + }); + + describe('#hideDeviceKeyboard', () => { + it('should hide device Keyboard @quick', async () => { + await app.resetApp(); + await app.click('~IntegerA'); + try { + await app.click('~locationStatus'); + } catch (e) { + e.message.should.include('element'); + } + await app.hideDeviceKeyboard('pressKey', 'Done'); + await app.click('~locationStatus'); + }); + + it('should assert if no keyboard', async () => { + try { + await app.hideDeviceKeyboard('pressKey', 'Done'); + } catch (e) { + e.message.should.include('An unknown server-side error occurred while processing the command. Original error: Soft keyboard not present, cannot hide keyboard'); + } + }); + }); + + describe('see text : #see', () => { + it('should work inside elements @second', async () => { + await app.resetApp(); + await app.see('Compute Sum', '~ComputeSumButton'); + }); + }); + + describe('#appendField', () => { + it('should be able to send special keys to element @second', async () => { + await app.resetApp(); + await app.waitForElement('~IntegerA', smallWait); + await app.click('~IntegerA'); + await app.appendField('~IntegerA', '1'); + await app.hideDeviceKeyboard('pressKey', 'Done'); + await app.see('1', '~IntegerA'); + }); + }); + + describe('#waitForText', () => { + it('should return error if not present', async () => { + try { + await app.waitForText('Nothing here', 1, '~IntegerA'); + } catch (e) { + e.message.should.contain('element (~IntegerA) is not in DOM or there is no element(~IntegerA) with text "Nothing here" after 1 sec'); + } + }); + }); + + describe('#seeNumberOfElements @second', () => { + it('should return 1 as count', async () => { + await app.resetApp(); + await app.seeNumberOfElements('~IntegerA', 1); + }); + }); + + describe('see element : #seeElement, #dontSeeElement', () => { + it('should check visible elements on page @quick', async () => { + await app.resetApp(); + await app.seeElement('~IntegerA'); + await app.dontSeeElement('#something-beyond'); + await app.dontSeeElement('//input[@id="something-beyond"]'); + }); + }); + + describe('#click @quick', () => { + it('should click by accessibility id', async () => { + await app.resetApp(); + await app.tap('~ComputeSumButton'); + await app.see('0'); + }); + }); + + describe('#fillField @second', () => { + it('should fill field by accessibility id', async () => { + await app.resetApp(); + await app.waitForElement('~IntegerA', smallWait); + await app.click('~IntegerA'); + await app.fillField('~IntegerA', '1'); + await app.hideDeviceKeyboard('pressKey', 'Done'); + await app.see('1', '~IntegerA'); + }); + }); + + describe('#grabTextFrom, #grabValueFrom, #grabAttributeFrom @quick', () => { + it('should grab text from page', async () => { + await app.resetApp(); + const val = await app.grabTextFrom('~ComputeSumButton'); + assert.equal(val, 'Compute Sum'); + }); + + it('should grab attribute from element', async () => { + await app.resetApp(); + const val = await app.grabAttributeFrom('~ComputeSumButton', 'label'); + assert.equal(val, 'Compute Sum'); + }); + + it('should be able to grab elements', async () => { + await app.resetApp(); + const id = await app.grabNumberOfVisibleElements('~ComputeSumButton'); + assert.strictEqual(1, id); + }); + }); + + describe('#saveScreenshot', () => { + beforeEach(() => { + global.output_dir = path.join(global.codecept_dir, 'output'); + }); + + it('should create a screenshot file in output dir', async () => { + const sec = (new Date()).getUTCMilliseconds(); + await app.saveScreenshot(`screenshot_${sec}.png`); + assert.ok(fileExists(path.join(global.output_dir, `screenshot_${sec}.png`)), null, 'file does not exists'); + }); + }); +});