Taking appium mobile specs out of the canvas-lms project
They now live in their own private repository. Change-Id: Id4e9e3f745c1311fc75f99bb62333af44b90f145 Reviewed-on: https://gerrit.instructure.com/66125 Tested-by: Jenkins Reviewed-by: Taylor Wilson <twilson@instructure.com> Product-Review: Derek Hansen <dhansen@instructure.com> QA-Review: Derek Hansen <dhansen@instructure.com>
This commit is contained in:
parent
6fcdcf6442
commit
b858cba8ca
|
@ -29,7 +29,6 @@ group :test do
|
||||||
gem 'childprocess', '0.5.0', require: false
|
gem 'childprocess', '0.5.0', require: false
|
||||||
gem 'websocket', '1.0.7', require: false
|
gem 'websocket', '1.0.7', require: false
|
||||||
gem 'selinimum', '0.0.1', require: false, path: 'gems/selinimum'
|
gem 'selinimum', '0.0.1', require: false, path: 'gems/selinimum'
|
||||||
gem 'appium_lib', '7.0.0', require: false
|
|
||||||
gem 'test_after_commit', '0.4.0'
|
gem 'test_after_commit', '0.4.0'
|
||||||
gem 'test-unit', '~> 3.0', require: false, platform: :ruby_22
|
gem 'test-unit', '~> 3.0', require: false, platform: :ruby_22
|
||||||
gem 'webmock', '1.16.1', require: false
|
gem 'webmock', '1.16.1', require: false
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
test:
|
|
||||||
host: "localhost"
|
|
||||||
port: 4444
|
|
||||||
browser: "firefox"
|
|
||||||
school_domain:
|
|
||||||
# URL to Canvas environment
|
|
||||||
# "twilson"
|
|
||||||
appium_host_url:
|
|
||||||
# setting path for test server
|
|
||||||
# "10.0.15.242"
|
|
||||||
# iOS device settings
|
|
||||||
ios_version: "8.4"
|
|
||||||
ios_device_name: "iPad"
|
|
||||||
ios_udid: "Put the unique device id here"
|
|
||||||
ios_type: "iPad"
|
|
||||||
ios_app_path: "Put the setting path for icanvas here"
|
|
|
@ -1,67 +0,0 @@
|
||||||
require_relative '../../helpers/android_common'
|
|
||||||
|
|
||||||
def bookmark_original_label
|
|
||||||
'goto_Course_Grades'
|
|
||||||
end
|
|
||||||
|
|
||||||
def bookmark_edited_label
|
|
||||||
'my_Grades'
|
|
||||||
end
|
|
||||||
|
|
||||||
def bookmark_routing_target
|
|
||||||
'Grades'
|
|
||||||
end
|
|
||||||
|
|
||||||
def click_add_bookmark
|
|
||||||
find_ele_by_attr('tag', 'android.widget.ImageView', 'name', /(More options)/).click
|
|
||||||
text_exact('Add Bookmark').click
|
|
||||||
end
|
|
||||||
|
|
||||||
# Multiple elements may exist with the same resource id.
|
|
||||||
# This chooses the element which is vertically aligned with the bookmark text.
|
|
||||||
def get_more_options(bookmark)
|
|
||||||
ids('overflowRipple').each do |more_options|
|
|
||||||
return more_options if more_options.location.y <= bookmark.location.y + bookmark.size.height / 4
|
|
||||||
end
|
|
||||||
raise('Unable to find more options button for bookmark.')
|
|
||||||
end
|
|
||||||
|
|
||||||
# Selects either 'Edit' or 'Delete' option on a given bookmark object
|
|
||||||
def click_bookmark_option(bookmark, option)
|
|
||||||
navigate_to('Bookmarks') unless exists{ text_exact('Bookmarks') }
|
|
||||||
get_more_options(bookmark).click
|
|
||||||
if option == 'Edit' || option == 'Delete'
|
|
||||||
text_exact(option).click
|
|
||||||
else
|
|
||||||
raise('Unsupported bookmark feature')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# When deleting a bookmark, the user may decide to cancel the deletion.
|
|
||||||
# This provides two ways to cancel, press 'back' or tap 'No'
|
|
||||||
def cancel_delete(bookmark, option)
|
|
||||||
click_bookmark_option(bookmark, 'Delete')
|
|
||||||
if option == 'No'
|
|
||||||
text_exact('No').click
|
|
||||||
elsif option == 'Back'
|
|
||||||
back
|
|
||||||
else
|
|
||||||
raise('Unsupported bookmark feature')
|
|
||||||
end
|
|
||||||
expect(exists{ text_exact('Remove Bookmark?') }).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
def verify_bookmark(bookmark_title)
|
|
||||||
navigate_to('Bookmarks')
|
|
||||||
wait_true(timeout: 10, interval: 0.100){ text_exact('Bookmarks') }
|
|
||||||
|
|
||||||
# Check routing
|
|
||||||
find_ele_by_attr('id', 'title', 'text', /(#{bookmark_title})/).click
|
|
||||||
expect(exists(1){ text_exact(bookmark_routing_target) }).to be true
|
|
||||||
expect(exists{ text_exact('Bookmarks') }).to be false
|
|
||||||
|
|
||||||
# Check back-stack, returns to 'Bookmarks'
|
|
||||||
back
|
|
||||||
expect(exists(1){ text_exact(bookmark_routing_target) }).to be false
|
|
||||||
expect(exists{ text_exact('Bookmarks') }).to be true
|
|
||||||
end
|
|
|
@ -1,106 +0,0 @@
|
||||||
require_relative 'bookmarks_common'
|
|
||||||
|
|
||||||
describe 'bookmarks and internal routing' do
|
|
||||||
include_context 'in-process server appium tests'
|
|
||||||
include_context 'appium mobile specs', 'candroid'
|
|
||||||
include_context 'course with a single user', 'student', 'candroid'
|
|
||||||
|
|
||||||
# uses *bookmark_original_label* for creating a new bookmark
|
|
||||||
# uses *bookmark_edited_label* for editing and deleting bookmarks
|
|
||||||
context 'navigated to a page that can be bookmarked' do
|
|
||||||
before(:each) do
|
|
||||||
navigate_to('Course_Grades')
|
|
||||||
click_add_bookmark
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'displays new bookmark view', priority: "1", test_id: 369240 do
|
|
||||||
expect(text_exact('Add Bookmark')).to be_truthy
|
|
||||||
expect(textfield_exact('Label')).to be_truthy
|
|
||||||
expect(text_exact('Cancel')).to be_truthy
|
|
||||||
expect(text_exact('Done')).to be_truthy
|
|
||||||
text_exact('Cancel').click
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not create invalid bookmarks', priority: "1", test_id: 369241 do
|
|
||||||
# A label was not entered; bookmarks without a label are invalid
|
|
||||||
find_element(:id, 'buttonDefaultPositive').click
|
|
||||||
|
|
||||||
# Bookmarks page displays 'Create a bookmark' if user has no bookmarks
|
|
||||||
navigate_to('Bookmarks')
|
|
||||||
expect(text('Create a bookmark')).to be_truthy
|
|
||||||
back # TODO: remove *back* when new navigation framework is complete
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates a new bookmark to course grades', priority: "1", test_id: 208761 do
|
|
||||||
find_element(:id, 'bookmarkEditText').send_keys(bookmark_original_label)
|
|
||||||
find_element(:id, 'buttonDefaultPositive').click
|
|
||||||
verify_bookmark(bookmark_original_label)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'navigated to bookmarks page' do
|
|
||||||
before(:each) do
|
|
||||||
navigate_to('Bookmarks')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'displays bookmark options', priority: "1", test_id: 369242 do
|
|
||||||
# Tap the vertical ellipsis to display 'Edit' and 'Delete' options
|
|
||||||
get_more_options(text_exact(bookmark_original_label)).click
|
|
||||||
expect(text_exact('Edit')).to be_truthy
|
|
||||||
expect(text_exact('Delete')).to be_truthy
|
|
||||||
|
|
||||||
# Close the Bookmark Options by tapping 'Back Button'
|
|
||||||
back
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'displays a bookmark edit view', priority: "1", test_id: 369243 do
|
|
||||||
# Tap the vertical ellipsis to display 'Edit' and 'Delete' options
|
|
||||||
click_bookmark_option(text_exact(bookmark_original_label), 'Edit')
|
|
||||||
expect(find_element(:id, 'title').text).to eq('Edit Bookmark')
|
|
||||||
expect(find_element(:id, 'bookmarkEditText').text).to eq(bookmark_original_label)
|
|
||||||
expect(text_exact('Done')).to be_truthy
|
|
||||||
|
|
||||||
# Close the Bookmark Options by tapping 'Back Button'
|
|
||||||
back
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'displays a bookmark delete view', priority: "1", test_id: 369244 do
|
|
||||||
# Tap the vertical ellipsis to display 'Edit' and 'Delete' options
|
|
||||||
click_bookmark_option(text_exact(bookmark_original_label), 'Delete')
|
|
||||||
expect(find_element(:id, 'title').text).to eq('Remove Bookmark?')
|
|
||||||
expect(find_element(:id, 'content').text).to eq(bookmark_original_label)
|
|
||||||
expect(text_exact('No')).to be_truthy
|
|
||||||
expect(text_exact('Yes')).to be_truthy
|
|
||||||
|
|
||||||
# Close the Bookmark Options by tapping 'Back Button'
|
|
||||||
back
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'edits the bookmark label', priority: "1", test_id: 209406 do
|
|
||||||
click_bookmark_option(text_exact(bookmark_original_label), 'Edit')
|
|
||||||
|
|
||||||
# Make the edit
|
|
||||||
find_element(:id, 'bookmarkEditText').send_keys(bookmark_edited_label)
|
|
||||||
expect(find_element(:id, 'bookmarkEditText').text).to eq(bookmark_edited_label)
|
|
||||||
text_exact('Done').click
|
|
||||||
expect(exists{ text_exact(bookmark_original_label) }).to be false
|
|
||||||
verify_bookmark(bookmark_edited_label)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'closes the \'Remove Bookmark?\' dialogue', priority: "1", test_id: 369245 do
|
|
||||||
cancel_delete(text_exact(bookmark_edited_label), 'No')
|
|
||||||
verify_bookmark(bookmark_edited_label)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'closes the \'Remove Bookmark?\' dialogue when using back button', priority: "1", test_id: 369246 do
|
|
||||||
cancel_delete(text_exact(bookmark_edited_label), 'Back')
|
|
||||||
verify_bookmark(bookmark_edited_label)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'deletes the bookmark', priority: "1", test_id: 209422 do
|
|
||||||
click_bookmark_option(text_exact(bookmark_edited_label), 'Delete')
|
|
||||||
text_exact('Yes').click
|
|
||||||
expect(exists{ text_exact(bookmark_edited_label) }).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,125 +0,0 @@
|
||||||
require_relative '../../helpers/android_common'
|
|
||||||
|
|
||||||
# types a single character until auto-fill generates target recipient
|
|
||||||
# then selects auto-populated recipient
|
|
||||||
def auto_populate_recipient(name)
|
|
||||||
email_field = find_element(:id, 'recipient')
|
|
||||||
substring = ''
|
|
||||||
name.each_char do |char|
|
|
||||||
substring += char
|
|
||||||
email_field.send_keys(substring)
|
|
||||||
|
|
||||||
# auto-populated recipients will appear immediately below the email textfield object
|
|
||||||
# tap the location where recipient will appear
|
|
||||||
action = Appium::TouchAction.new.tap(x: 0.5 * window_size.width,
|
|
||||||
y: (email_field.location.y + email_field.size.height) + (0.5 * email_field.size.height))
|
|
||||||
action.perform
|
|
||||||
|
|
||||||
# if recipient was auto-populated, the tap should have selected them
|
|
||||||
# check if email textfield reflects a successful auto-population; keep trying if unsuccessful
|
|
||||||
if email_field.text =~ recipient_list_matcher
|
|
||||||
# auto-population on last character of input string does not count... fail
|
|
||||||
return true unless char == name[-1, 1]
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
# clicking compose button, for unknown reason, FAILS
|
|
||||||
# this method attempts to open the form several times before giving up
|
|
||||||
# this usually works on the second attempt
|
|
||||||
def click_object(obj)
|
|
||||||
if obj == 'compose'
|
|
||||||
click_compose_button
|
|
||||||
elsif obj == 'sent'
|
|
||||||
click_sent_tab
|
|
||||||
else
|
|
||||||
raise('Unsupported option for click_object. Expecting \'compose\' or \'sent\'')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def click_compose_button
|
|
||||||
attempts = 0
|
|
||||||
loop do
|
|
||||||
if find_ele_by_attr('tag', 'android.widget.ImageButton', 'name', /(Navigate up)/) != nil
|
|
||||||
break
|
|
||||||
elsif (attempts += 1) > 5
|
|
||||||
raise('Unable to open compose message form.')
|
|
||||||
else
|
|
||||||
find_element(:id, 'compose').click
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def click_sent_tab
|
|
||||||
attempts = 0
|
|
||||||
loop do
|
|
||||||
if find_ele_by_attr('tag', 'android.widget.TextView', 'text', /(Sent)/).selected?
|
|
||||||
break
|
|
||||||
elsif (attempts += 1) > 5
|
|
||||||
raise('Unable to open sent tab.')
|
|
||||||
else
|
|
||||||
text_exact('Sent').click
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def enter_subject(subject)
|
|
||||||
subject_field = find_element(:id, 'subject')
|
|
||||||
subject_field.send_keys(subject)
|
|
||||||
end
|
|
||||||
|
|
||||||
def enter_message(message)
|
|
||||||
message_field = find_element(:id, 'message')
|
|
||||||
message_field.send_keys(message)
|
|
||||||
end
|
|
||||||
|
|
||||||
# only matches for a single recipient; does not match multiple recipients
|
|
||||||
def recipient_list_matcher
|
|
||||||
return /(^<#{recipient}>)(,.)$/
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_message(recipient, recipient_role, subject, message)
|
|
||||||
select_recipient_course
|
|
||||||
select_recipient_from_menu(recipient, recipient_role)
|
|
||||||
enter_subject(subject)
|
|
||||||
enter_message(message)
|
|
||||||
find_element(:id, 'menu_send').click
|
|
||||||
end
|
|
||||||
|
|
||||||
def select_recipient_course
|
|
||||||
find_element(:id, 'course_spinner').click
|
|
||||||
text_exact(@course.name).click
|
|
||||||
end
|
|
||||||
|
|
||||||
def select_recipient_from_menu(recipient, recipient_role)
|
|
||||||
# possible recipients are organized by user role
|
|
||||||
find_element(:id, 'menu_choose_recipients').click
|
|
||||||
select_user_group(recipient_role)
|
|
||||||
text_exact(recipient).click
|
|
||||||
find_element(:id, 'menu_done').click
|
|
||||||
end
|
|
||||||
|
|
||||||
def select_user_group(group)
|
|
||||||
case group
|
|
||||||
when 'teacher'
|
|
||||||
text_exact('Teachers').click
|
|
||||||
when 'ta'
|
|
||||||
text_exact('Teaching Assistants').click
|
|
||||||
when 'student'
|
|
||||||
text_exact('Students').click
|
|
||||||
when 'observer'
|
|
||||||
text_exact('Observers').click
|
|
||||||
else
|
|
||||||
raise('Invalid user group selected.')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def student_subject
|
|
||||||
'Final Grades'
|
|
||||||
end
|
|
||||||
|
|
||||||
def student_message
|
|
||||||
'Will I pass this class?'
|
|
||||||
end
|
|
|
@ -1,79 +0,0 @@
|
||||||
require_relative 'conversations_common'
|
|
||||||
|
|
||||||
describe 'conversations inbox' do
|
|
||||||
include_context 'in-process server appium tests'
|
|
||||||
include_context 'appium mobile specs', 'candroid'
|
|
||||||
include_context 'teacher and student users', 'candroid'
|
|
||||||
|
|
||||||
context 'student as the sender' do
|
|
||||||
let(:sender){ @student.primary_pseudonym.unique_id }
|
|
||||||
let(:recipient){ @teacher.primary_pseudonym.unique_id }
|
|
||||||
let(:recipient_role){ 'teacher' }
|
|
||||||
|
|
||||||
before(:all) do
|
|
||||||
android_app_init(@student.primary_pseudonym.unique_id, user_password(@student), @course.name)
|
|
||||||
end
|
|
||||||
|
|
||||||
before(:each) do
|
|
||||||
navigate_to('Inbox')
|
|
||||||
click_object('compose')
|
|
||||||
end
|
|
||||||
|
|
||||||
after(:each) do
|
|
||||||
# use this over *back*; this will clear out previously entered recipients, *back* will not
|
|
||||||
find_ele_by_attr('tag', 'android.widget.ImageButton', 'name', /(Navigate up)/).click
|
|
||||||
end
|
|
||||||
|
|
||||||
after(:all) do
|
|
||||||
logout(false)
|
|
||||||
end
|
|
||||||
|
|
||||||
# taps the compose button (buttom right) and verifies a new message form is displayed
|
|
||||||
it 'has a working compose button', priority: "1", test_id: 18399 do
|
|
||||||
expect(find_ele_by_attr('tag', 'android.widget.ImageButton', 'name', /(Navigate up)/)).to be_displayed
|
|
||||||
expect(text_exact('Compose Message')).to be_displayed
|
|
||||||
expect(find_element(:id, 'menu_send')).to be_displayed
|
|
||||||
expect(text_exact('Select a course')).to be_displayed
|
|
||||||
expect(find_element(:id, 'subject')).to be_displayed
|
|
||||||
expect(text_exact('Compose Message')).to be_displayed
|
|
||||||
end
|
|
||||||
|
|
||||||
# uses course menu dropdown rather than manually typing the course
|
|
||||||
it 'selects a course', priority: "1", test_id: 220022 do
|
|
||||||
expect(exists{ find_element(:id, 'menu_choose_recipients') }).to be false
|
|
||||||
select_recipient_course
|
|
||||||
expect(exists{ find_element(:id, 'menu_choose_recipients') }).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'adds recipient using recipient menu', priority: "1", test_id: 220024 do
|
|
||||||
select_recipient_course
|
|
||||||
select_recipient_from_menu(recipient, recipient_role)
|
|
||||||
|
|
||||||
# actual text of recipient field encapsulates entries with "< >"
|
|
||||||
expect(find_element(:id, 'recipient').text).to match(recipient_list_matcher)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'adds recipient with auto-populated response', priority: "1", test_id: 18400 do
|
|
||||||
select_recipient_course
|
|
||||||
expect(auto_populate_recipient(recipient)).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'enters a subject line', priority: "1", test_id: 18401 do
|
|
||||||
expect(find_element(:id, 'subject').text).to eq('Subject')
|
|
||||||
enter_subject(student_subject)
|
|
||||||
expect(find_element(:id, 'subject').text).to eq(student_subject)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'enters a message body', priority: "1", test_id: 369247 do
|
|
||||||
expect(find_element(:id, 'message').text).to eq('Compose Message')
|
|
||||||
enter_message(student_message)
|
|
||||||
expect(find_element(:id, 'message').text).to eq(student_message)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'sends a message', priority: "1", test_id: 18403 do
|
|
||||||
# sending the email closes the form and displays the inbox view
|
|
||||||
send_message(recipient, recipient_role, student_subject, student_message)
|
|
||||||
expect(find_element(:id, 'compose')).to be_displayed
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,10 +0,0 @@
|
||||||
require_relative '../../../helpers/landing_page_common'
|
|
||||||
|
|
||||||
describe 'candroid landing page' do
|
|
||||||
include_context 'in-process server appium tests'
|
|
||||||
include_context 'appium mobile specs', 'candroid'
|
|
||||||
let(:default_url){ 'Find your school or district' }
|
|
||||||
|
|
||||||
# examples located in: spec/selenium/appium/android/helpers/landing_page_common.rb
|
|
||||||
it_behaves_like 'candroid and speedgrader landing page', 'candroid'
|
|
||||||
end
|
|
|
@ -1,11 +0,0 @@
|
||||||
require_relative '../../../helpers/login_common'
|
|
||||||
|
|
||||||
describe 'candroid login credentials' do
|
|
||||||
include_context 'in-process server appium tests'
|
|
||||||
include_context 'appium mobile specs', 'candroid'
|
|
||||||
let(:app_login_message){ /(Canvas for Android)/ }
|
|
||||||
let(:app_access_message){ /Canvas for Android is requesting access.*/ }
|
|
||||||
|
|
||||||
# examples located in: spec/selenium/appium/android/helpers/login_common.rb
|
|
||||||
it_behaves_like 'login credentials for candroid and speedgrader', 'candroid'
|
|
||||||
end
|
|
|
@ -1,144 +0,0 @@
|
||||||
require_relative '../../mobile_common'
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Log In / Out of Mobile App
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
def android_app_init(username, password, course_name)
|
|
||||||
# check for multi-user access
|
|
||||||
if (userlink = find_ele_by_attr('id', 'name', 'text', /#{username}/)).nil?
|
|
||||||
enter_school
|
|
||||||
login_mobile(username, password)
|
|
||||||
navigate_to_course(course_name)
|
|
||||||
else
|
|
||||||
userlink.click
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def enter_school
|
|
||||||
find_element(:id, 'enterURL').send_keys(@school)
|
|
||||||
find_element(:id, 'connect').click
|
|
||||||
|
|
||||||
# blocks until 1st login view loads
|
|
||||||
wait_true(timeout: 10, interval: 0.250){ button_exact('Log In') }
|
|
||||||
end
|
|
||||||
|
|
||||||
def provide_credentials(username, password)
|
|
||||||
first_textfield.send_keys(username)
|
|
||||||
last_textfield.send_keys(password)
|
|
||||||
button('Log in').click
|
|
||||||
end
|
|
||||||
|
|
||||||
def login_mobile(username, password)
|
|
||||||
# 1st login view
|
|
||||||
provide_credentials(username, password)
|
|
||||||
|
|
||||||
# blocks until 2nd login view loads
|
|
||||||
wait_true(timeout: 10, interval: 0.250){ find_ele_by_attr('tags', 'android.view.View', 'name', /Cancel/) }
|
|
||||||
button('Log In').click
|
|
||||||
skip_tutorial
|
|
||||||
end
|
|
||||||
|
|
||||||
def logout(add_account)
|
|
||||||
candroid_app ? logout_android(add_account) : logout_speedgrader
|
|
||||||
end
|
|
||||||
|
|
||||||
def logout_android(add_account)
|
|
||||||
open_hamburger
|
|
||||||
find_element(:id, 'userNameContainer').click unless exists{ find_element(:id, 'logout') }
|
|
||||||
if add_account
|
|
||||||
find_element(:id, 'addAccount').click
|
|
||||||
else
|
|
||||||
find_element(:id, 'logout').click
|
|
||||||
find_element(:id, 'dialog_custom_confirm').click
|
|
||||||
end
|
|
||||||
wait_true(timeout: 10, interval: 0.100){ find_element(:id, 'enterURL') }
|
|
||||||
end
|
|
||||||
|
|
||||||
def logout_speedgrader
|
|
||||||
find_ele_by_attr('tag', 'android.widget.ImageButton', 'name', /(Open Drawer)/).click
|
|
||||||
find_element(:id, 'logoutText').click
|
|
||||||
end
|
|
||||||
|
|
||||||
def skip_tutorial
|
|
||||||
# Getting Started screen takes a second to animate in and out
|
|
||||||
find_element(:id, 'skip').click if exists(3){ find_element(:id, 'skip') }
|
|
||||||
if candroid_app
|
|
||||||
wait_true(timeout:10, interval: 0.100){ find_element(:id, 'toolbar') }
|
|
||||||
else
|
|
||||||
wait_true(timeout:10, interval: 0.100){ find_element(:id, 'courseSwitcher') }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Navigation
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
# TODO: add support for speedgrader
|
|
||||||
def navigate_to_course(course_name)
|
|
||||||
if candroid_app
|
|
||||||
tags('android.widget.ImageButton')[0].click unless exists{ find_element(:id, 'scrollview') }
|
|
||||||
find_element(:id, 'courses').click
|
|
||||||
text_exact(course_name).click
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def navigate_to(location)
|
|
||||||
case location
|
|
||||||
when 'Bookmarks', 'Grades', 'Inbox'
|
|
||||||
open_hamburger
|
|
||||||
scroll_vertically_in_view(find_element(:id, 'scrollview'), 2000, 'down') unless exists{ text_exact(location) }
|
|
||||||
else
|
|
||||||
# location needs to differentiate between course grades in the navigation menu and grades in scroll view
|
|
||||||
location = 'Grades' if location == 'Course_Grades'
|
|
||||||
wait_true(timeout: 10, interval: 0.100){ find_navigation_indicator.click }
|
|
||||||
list_view = tag('android.widget.ListView')
|
|
||||||
scroll_vertically_in_view(list_view, 2000, 'down') unless exists{ text_exact(location) }
|
|
||||||
end
|
|
||||||
text_exact(location).click
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_navigation_indicator
|
|
||||||
return find_element(:id, 'indicator') if exists{ find_element(:id, 'indicator') }
|
|
||||||
return find_element(:id, 'arrow') if exists{ find_element(:id, 'arrow') }
|
|
||||||
end
|
|
||||||
|
|
||||||
def open_hamburger
|
|
||||||
# hamburger will always be the first image view returned
|
|
||||||
tag('android.widget.ImageButton').click unless exists{ find_element(:id, 'scrollview') }
|
|
||||||
end
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# General
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
def find_ele_by_attr(type, id, attribute, regex)
|
|
||||||
elements = type == 'id' ? ids(id) : tags(id)
|
|
||||||
elements.each do |element|
|
|
||||||
case attribute
|
|
||||||
when 'text'
|
|
||||||
return element if element.text =~ regex
|
|
||||||
when 'name'
|
|
||||||
return element if element.name =~ regex
|
|
||||||
else
|
|
||||||
raise('unsupported option for finding element')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def press_keycodes(text)
|
|
||||||
text.each_byte do |code|
|
|
||||||
if code == 32
|
|
||||||
press_keycode(62) # space
|
|
||||||
else
|
|
||||||
press_keycode(code - 68)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def wait_for_super_panda
|
|
||||||
loop do
|
|
||||||
break unless exists(0.250){ find_element(:id, 'pandaLoading') }
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,174 +0,0 @@
|
||||||
require_relative 'android_common'
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Shared Examples for Candroid and Speedgrader Mobile Apps
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
shared_examples 'candroid and speedgrader landing page' do |app_name|
|
|
||||||
it 'displays a landing page', priority: "1", test_id: pick_test_id_for_app(app_name, 221316, 295284) do
|
|
||||||
# TODO: ask dev team to implement this for speedgrader
|
|
||||||
expect(find_element(:id, 'help_button')).to be_truthy unless @app_name =~ /(speedgrader)/
|
|
||||||
expect(find_element(:id, 'canvas_logo')).to be_truthy
|
|
||||||
expect(find_element(:id, 'enterURL')).to be_truthy
|
|
||||||
expect(find_element(:id, 'enterURL').text).to eq(default_url)
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: ask dev team to implement this
|
|
||||||
it 'routes to canvas guides', priority: "1", test_id: pick_test_id_for_app(app_name, 221317, 295285) do
|
|
||||||
skip('Android SpeedGrader app does not have a Help menu on landing page') if @app_name =~ /(speedgrader)/
|
|
||||||
find_element(:id, 'help_button').click
|
|
||||||
find_element(:id, 'search_guides').click
|
|
||||||
expect(tags('android.widget.ImageButton')[0].name).to eq('Navigate up')
|
|
||||||
expect(text_exact('Canvas Guides')).to be_truthy
|
|
||||||
tags('android.widget.ImageButton')[0].click
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: ask dev team to implement this
|
|
||||||
it 'routes to report a problem', priority: "1", test_id: pick_test_id_for_app(app_name, 221318, 295286) do
|
|
||||||
skip('Android SpeedGrader app does not have a Help menu on landing page') if @app_name =~ /(speedgrader)/
|
|
||||||
find_element(:id, 'help_button').click
|
|
||||||
find_element(:id, 'report_problem').click
|
|
||||||
begin
|
|
||||||
hide_keyboard
|
|
||||||
rescue Selenium::WebDriver::Error::UnknownError => ex # soft keyboard not present, cannot hide keyboard
|
|
||||||
raise unless ex.message == 'Soft keyboard not present, cannot hide keyboard'
|
|
||||||
end
|
|
||||||
|
|
||||||
text_fields_id.each_with_index do |id, index|
|
|
||||||
verify_text_field(scroll_to_text_field(id), index)
|
|
||||||
end
|
|
||||||
|
|
||||||
find_element(:id, 'severitySpinner').click
|
|
||||||
severity_levels = ids('text')
|
|
||||||
verify_severity_levels(severity_levels)
|
|
||||||
severity_levels[4].click
|
|
||||||
scroll_to_severity_spinner
|
|
||||||
expect(find_element(:id, 'text').text).to eq('EXTREME CRITICAL EMERGENCY!!')
|
|
||||||
expect(find_element(:id, 'dialog_custom_cancel').text).to eq('CANCEL')
|
|
||||||
expect(find_element(:id, 'dialog_custom_confirm').text).to eq('SEND')
|
|
||||||
|
|
||||||
find_element(:id, 'dialog_custom_cancel').click
|
|
||||||
|
|
||||||
hide_keyboard unless exists{ find_element(:id, 'help_button') }
|
|
||||||
expect(find_element(:id, 'help_button')).to be_truthy
|
|
||||||
expect(find_element(:id, 'canvas_logo')).to be_truthy
|
|
||||||
expect(find_element(:id, 'enterURL')).to be_truthy
|
|
||||||
expect(find_element(:id, 'enterURL').text).to eq(default_url)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'lists possible schools when entering url and routes to school', priority: "1", test_id: pick_test_id_for_app(app_name, 221319, 295287) do
|
|
||||||
find_element(:id, 'enterURL').send_keys('t')
|
|
||||||
expect(find_element(:id, 'connect')).to be_truthy
|
|
||||||
expect(find_element(:id, 'canvasNetworkHeader')).to be_truthy
|
|
||||||
|
|
||||||
# wait for list of schools to populate
|
|
||||||
sleep(0.100)
|
|
||||||
schools = ids('name')
|
|
||||||
expect(schools.size).to be >= 4
|
|
||||||
school = schools[3]
|
|
||||||
school_name = school.text
|
|
||||||
school.click
|
|
||||||
|
|
||||||
expect(tag('android.webkit.WebView')).to be_truthy
|
|
||||||
expect(find_ele_by_attr('tag', 'android.widget.TextView', 'text', /([a-z]+)(.instructure.com)/))
|
|
||||||
.to be_an_instance_of(Selenium::WebDriver::Element)
|
|
||||||
back
|
|
||||||
|
|
||||||
edit_url_text = find_element(:id,'enterURL')
|
|
||||||
expect(edit_url_text.text).to match(/([a-z]+)(.instructure.com)/)
|
|
||||||
expect(text_exact(school_name)).to be_truthy
|
|
||||||
edit_url_text.clear
|
|
||||||
expect(edit_url_text.text).to eq(default_url)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'routes to school login page when school is typed in', priority: "1", test_id: pick_test_id_for_app(app_name, 221321, 295289) do
|
|
||||||
find_element(:id, 'enterURL').send_keys(@school)
|
|
||||||
find_element(:id, 'connect').click
|
|
||||||
|
|
||||||
# wait for webview to load
|
|
||||||
wait_true(timeout: 10, interval: 0.100){ tag('android.webkit.WebView') }
|
|
||||||
|
|
||||||
wait_true(timeout: 10, interval: 0.100){ first_textfield }
|
|
||||||
wait_true(timeout: 10, interval: 0.100){ last_textfield }
|
|
||||||
wait_true(timeout: 10, interval: 0.100){ button('Log in') }
|
|
||||||
|
|
||||||
reset_password = find_ele_by_attr('tags', 'android.view.View', 'name', /I don't know my password/)
|
|
||||||
expect(reset_password.name).to eq('I don\'t know my password')
|
|
||||||
back
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Helper Methods
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
def scroll_to_severity_spinner
|
|
||||||
scroll_to_element(scroll_view: tag('android.widget.ScrollView'),
|
|
||||||
strategy: 'id',
|
|
||||||
id: 'severitySpinner',
|
|
||||||
time: 500,
|
|
||||||
direction: 'down',
|
|
||||||
attempts: 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
def scroll_to_text_field(id)
|
|
||||||
scroll_to_element(scroll_view: tag('android.widget.ScrollView'),
|
|
||||||
strategy: 'id',
|
|
||||||
id: id,
|
|
||||||
time: 500,
|
|
||||||
direction: 'down',
|
|
||||||
attempts: 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
def text_fields_id
|
|
||||||
%w(
|
|
||||||
dialog_custom_title
|
|
||||||
subject
|
|
||||||
subjectEditText
|
|
||||||
emailAddress
|
|
||||||
emailAddressEditText
|
|
||||||
description
|
|
||||||
descriptionEditText
|
|
||||||
severityPrompt
|
|
||||||
severitySpinner
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def verify_text_field(text_field, index)
|
|
||||||
expect(text_field).to be_an_instance_of(Selenium::WebDriver::Element)
|
|
||||||
case index
|
|
||||||
when 0
|
|
||||||
expect(text_field.text).to eq('Report A Problem')
|
|
||||||
when 1
|
|
||||||
expect(text_field.text).to eq('Subject')
|
|
||||||
when 3
|
|
||||||
expect(text_field.text).to eq('Email Address')
|
|
||||||
when 4
|
|
||||||
expect(text_field.text).to match(/(Enter your email address)/) # '...' has issues with ==
|
|
||||||
when 5
|
|
||||||
expect(text_field.text).to eq('Description')
|
|
||||||
when 6
|
|
||||||
expect(text_field.text).to match(/(Write Something)/) # '...' has issues with ==
|
|
||||||
when 7
|
|
||||||
expect(text_field.text).to eq('How is this affecting you?')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def verify_severity_levels(severity_levels)
|
|
||||||
expect(severity_levels.size).to be(5)
|
|
||||||
severity_levels.each_index do |index|
|
|
||||||
case index
|
|
||||||
when 0
|
|
||||||
# '...' has issues with ==
|
|
||||||
expect(severity_levels[index].text).to match(/(Just a casual question, comment, idea, suggestion…)/)
|
|
||||||
when 1
|
|
||||||
expect(severity_levels[index].text).to eq('I need some help but it\'s not urgent.')
|
|
||||||
when 2
|
|
||||||
expect(severity_levels[index].text).to eq('Something\'s broken but I can work around it to get what I need done.')
|
|
||||||
when 3
|
|
||||||
expect(severity_levels[index].text).to eq('I can\'t get things done until I hear back from you.')
|
|
||||||
when 4
|
|
||||||
expect(severity_levels[index].text).to eq('EXTREME CRITICAL EMERGENCY!!')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,86 +0,0 @@
|
||||||
require_relative 'android_common'
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Shared Examples for Candroid and Speedgrader Mobile Apps
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
shared_examples 'login credentials for candroid and speedgrader' do |app_name|
|
|
||||||
before(:all) do
|
|
||||||
user_with_pseudonym(username: 'teacher1', password: 'teacher')
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'user provides bad credentials' do
|
|
||||||
before(:each) do
|
|
||||||
enter_school
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: write test cases for bad credentials
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'user forgot their password' do
|
|
||||||
before(:each) do
|
|
||||||
enter_school
|
|
||||||
end
|
|
||||||
|
|
||||||
after(:each) do
|
|
||||||
back
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'routes to password reset view', priority: "1", test_id: pick_test_id_for_app(app_name, 221322, 295519) do
|
|
||||||
find_ele_by_attr('tags', 'android.view.View', 'name', /(I don't know my password)/).click
|
|
||||||
|
|
||||||
wait_true(timeout: 10, interval: 0.100){ button_exact('Request Password') }
|
|
||||||
expect(find_ele_by_attr('tags', 'android.view.View', 'name', /(change your password)/).name)
|
|
||||||
.to eq('Enter your Email and we\'ll send you a link to change your password.')
|
|
||||||
expect(first_textfield).to be_displayed # TODO: ask dev team for content descriptor
|
|
||||||
|
|
||||||
back_to_login_view = find_ele_by_attr('tags', 'android.view.View', 'name', /Back to Login/)
|
|
||||||
expect(back_to_login_view.name).to eq('Back to Login')
|
|
||||||
back_to_login_view.click
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'user provides good credentials' do
|
|
||||||
before(:each) do
|
|
||||||
enter_school
|
|
||||||
provide_credentials(@user.primary_pseudonym.unique_id, user_password(@user))
|
|
||||||
end
|
|
||||||
|
|
||||||
after(:each) do
|
|
||||||
logout(false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'passes login and routes to home page', priority: "1", test_id: pick_test_id_for_app(app_name, 221323, 295521) do
|
|
||||||
wait_true(timeout: 10, interval: 0.250){ find_ele_by_attr('tags', 'android.view.View', 'name', app_login_message) }
|
|
||||||
|
|
||||||
expect(find_ele_by_attr('tags', 'android.view.View', 'name', app_access_message))
|
|
||||||
.to be_an_instance_of(Selenium::WebDriver::Element)
|
|
||||||
expect(find_ele_by_attr('tags', 'android.view.View', 'name', /(You are logging into this app as)/))
|
|
||||||
.to be_an_instance_of(Selenium::WebDriver::Element)
|
|
||||||
expect(find_ele_by_attr('tags', 'android.view.View', 'name', /#{(@user.primary_pseudonym.unique_id)}/))
|
|
||||||
.to be_an_instance_of(Selenium::WebDriver::Element)
|
|
||||||
|
|
||||||
expect(find_ele_by_attr('tags', 'android.view.View', 'name', /Cancel/).name).to eq('Cancel')
|
|
||||||
remember_auth = find_ele_by_attr('tags', 'android.widget.CheckBox', 'name', /Remember my authorization for this service/)
|
|
||||||
expect(remember_auth.attribute('checked')).to eq('false')
|
|
||||||
remember_auth.click
|
|
||||||
expect(remember_auth.attribute('checked')).to eq('true')
|
|
||||||
button('Log in').click
|
|
||||||
|
|
||||||
skip_tutorial
|
|
||||||
|
|
||||||
# User avatar displays on Home Page but takes time to load
|
|
||||||
wait_true(timeout:10, interval: 0.250){ candroid_app ? find_element(:id, 'userProfilePic') : find_element(:id, 'courseSwitcher') }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Helper Methods
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
def provide_credentials(username, password)
|
|
||||||
first_textfield.send_keys(username)
|
|
||||||
last_textfield.send_keys(password)
|
|
||||||
button('Log in').click
|
|
||||||
end
|
|
|
@ -1,10 +0,0 @@
|
||||||
require_relative '../../../helpers/landing_page_common'
|
|
||||||
|
|
||||||
describe 'speedgrader for android landing page' do
|
|
||||||
include_context 'in-process server appium tests'
|
|
||||||
include_context 'appium mobile specs', 'speedgrader_android'
|
|
||||||
let(:default_url){ 'myschool.instructure.com' }
|
|
||||||
|
|
||||||
# examples located in: spec/selenium/appium/android/helpers/landing_page_common.rb
|
|
||||||
it_behaves_like 'candroid and speedgrader landing page', 'speedgrader_android'
|
|
||||||
end
|
|
|
@ -1,11 +0,0 @@
|
||||||
require_relative '../../../helpers/login_common'
|
|
||||||
|
|
||||||
describe 'speedgrader login credentials' do
|
|
||||||
include_context 'in-process server appium tests'
|
|
||||||
include_context 'appium mobile specs', 'speedgrader_android'
|
|
||||||
let(:app_login_message){ /(Canvas for Android)/ } # TODO: ask dev team to modify this for Speedgrader
|
|
||||||
let(:app_access_message){ /Canvas for Android is requesting access.*/ } # TODO: ask dev team to modify this for Speedgrader
|
|
||||||
|
|
||||||
# examples located in: spec/selenium/appium/android/helpers/login_common.rb
|
|
||||||
it_behaves_like 'login credentials for candroid and speedgrader', 'speedgrader_android'
|
|
||||||
end
|
|
|
@ -1,271 +0,0 @@
|
||||||
require 'socket'
|
|
||||||
require 'timeout'
|
|
||||||
require_relative '../../spec_helper'
|
|
||||||
require_relative '../test_setup/common_helper_methods/login_and_session_methods'
|
|
||||||
require_relative '../test_setup/common_helper_methods/other_helper_methods'
|
|
||||||
|
|
||||||
module EnvironmentSetup
|
|
||||||
# All test environments must be seeded with this Developer Key. The only attribute allowed
|
|
||||||
# to change is the *redirect_uri* which contains the static IP address of the Canvas-lms
|
|
||||||
# environment, but do not change it; modify the *host_url* method instead.
|
|
||||||
def create_developer_key
|
|
||||||
if @appium_dev_key.nil?
|
|
||||||
truncate_table(DeveloperKey) if @appium_dev_key.nil?
|
|
||||||
@appium_dev_key = DeveloperKey.create!(
|
|
||||||
name: $appium_config[:mv_key_name],
|
|
||||||
tool_id: $appium_config[:mv_key_id],
|
|
||||||
email: $appium_config[:mv_key_email],
|
|
||||||
redirect_uri: "http://#{host_url}",
|
|
||||||
api_key: $appium_config[:mv_key]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
@appium_dev_key
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: update when Appium is integrated with Jenkins, append $server_port
|
|
||||||
def school_domain
|
|
||||||
$appium_config[:school_domain]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Static IP addresses entered into Mobile Verify. Comment/Uncomment to set the url.
|
|
||||||
# Mobile Apps will not connect to local test instances other than these.
|
|
||||||
def host_url
|
|
||||||
$appium_config[:appium_host_url]
|
|
||||||
end
|
|
||||||
|
|
||||||
# This assumes Appium server will be running on the same host as the Canvas-lms.
|
|
||||||
def appium_server_url
|
|
||||||
URI("http://#{host_url}:4723/wd/hub")
|
|
||||||
end
|
|
||||||
|
|
||||||
def android_course_name
|
|
||||||
'QA | Android'
|
|
||||||
end
|
|
||||||
|
|
||||||
def ios_course_name
|
|
||||||
'QA | iOS'
|
|
||||||
end
|
|
||||||
|
|
||||||
# Appium settings are device specific. To list connected devices for Android run:
|
|
||||||
# $ <android_sdk_path>/platform-tools/adb devices
|
|
||||||
def android_device_name
|
|
||||||
$appium_config[:android_udid]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Appium settings are device specific. To get iOS device info:
|
|
||||||
# Settings > General > Version
|
|
||||||
# $ idevice_id -l ### lists connected devices by UDID
|
|
||||||
# $ idevice_id [UDID] ### prints device name
|
|
||||||
# $ idevice_id -l ### lists connected devices by UDID
|
|
||||||
def ios_device
|
|
||||||
{ versionNumber: $appium_config[:ios_version],
|
|
||||||
deviceName: $appium_config[:ios_device_name],
|
|
||||||
udid: $appium_config[:ios_udid],
|
|
||||||
app: $appium_config[:ios_app_path] }
|
|
||||||
end
|
|
||||||
|
|
||||||
def ios_app_path
|
|
||||||
$appium_config[:ios_app_path]
|
|
||||||
end
|
|
||||||
|
|
||||||
def implicit_wait_time
|
|
||||||
3
|
|
||||||
end
|
|
||||||
|
|
||||||
# ====================================================================================================================
|
|
||||||
# Extracted from spec/selenium/test_setup/selenium_driver_setup.rb
|
|
||||||
# Modified for Mobile App automation: all things Selenium removed
|
|
||||||
# ====================================================================================================================
|
|
||||||
|
|
||||||
include I18nUtilities
|
|
||||||
|
|
||||||
$appium_config = ConfigFile.load("appium") || {}
|
|
||||||
SERVER_IP = $appium_config[:server_ip] || UDPSocket.open do |s|
|
|
||||||
s.connect('8.8.8.8', 1)
|
|
||||||
s.addr.last
|
|
||||||
end
|
|
||||||
BIND_ADDRESS = $appium_config[:bind_address] || '0.0.0.0'
|
|
||||||
SECONDS_UNTIL_COUNTDOWN = 5
|
|
||||||
SECONDS_UNTIL_GIVING_UP = 20
|
|
||||||
MAX_SERVER_START_TIME = 60
|
|
||||||
THIS_ENV = ENV['TEST_ENV_NUMBER'].to_i
|
|
||||||
THIS_ENV = 1 if ENV['TEST_ENV_NUMBER'].blank?
|
|
||||||
WEBSERVER = (ENV['WEBSERVER'] || 'thin').freeze
|
|
||||||
|
|
||||||
$server_port = nil
|
|
||||||
$app_host_and_port = nil
|
|
||||||
|
|
||||||
def host_and_port
|
|
||||||
if $appium_config[:host] && $appium_config[:port] && !$appium_config[:host_and_port]
|
|
||||||
$appium_config[:host_and_port] = "#{$appium_config[:host]}:#{$appium_config[:port]}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Runs a port scan unless the appium.yml file defines :server_port
|
|
||||||
def self.setup_host_and_port
|
|
||||||
ENV['CANVAS_CDN_HOST'] = "canvas.instructure.com"
|
|
||||||
if $appium_config[:server_port]
|
|
||||||
$server_port = $appium_config[:server_port]
|
|
||||||
$app_host_and_port = "#{SERVER_IP}:#{$server_port}"
|
|
||||||
return $server_port
|
|
||||||
end
|
|
||||||
|
|
||||||
# find an available socket
|
|
||||||
s = Socket.new(:INET, :STREAM)
|
|
||||||
s.setsockopt(:SOCKET, :REUSEADDR, true)
|
|
||||||
s.bind(Addrinfo.tcp(SERVER_IP, 0))
|
|
||||||
|
|
||||||
$server_port = s.local_address.ip_port
|
|
||||||
if $appium_config[:browser] == 'ie'
|
|
||||||
# makes default URL for selenium the external IP of the box for standalone sel servers
|
|
||||||
server_ip = `curl http://instance-data/latest/meta-data/public-ipv4` # command for aws boxes gets external ip
|
|
||||||
else
|
|
||||||
server_ip = s.local_address.ip_address
|
|
||||||
end
|
|
||||||
|
|
||||||
$app_host_and_port = "#{server_ip}:#{s.local_address.ip_port}"
|
|
||||||
puts "Found available port: #{$app_host_and_port}"
|
|
||||||
|
|
||||||
return $server_port
|
|
||||||
ensure
|
|
||||||
s.close() if s
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.start_webserver(webserver)
|
|
||||||
setup_host_and_port
|
|
||||||
case webserver
|
|
||||||
when 'thin'
|
|
||||||
self.start_in_process_thin_server
|
|
||||||
when 'webrick'
|
|
||||||
self.start_in_process_webrick_server
|
|
||||||
else
|
|
||||||
puts "No web server specified, defaulting to WEBrick"
|
|
||||||
self.start_in_process_webrick_server
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.shutdown_webserver(server)
|
|
||||||
shutdown = lambda do
|
|
||||||
server.shutdown
|
|
||||||
HostUrl.default_host = nil
|
|
||||||
HostUrl.file_host = nil
|
|
||||||
end
|
|
||||||
at_exit { shutdown.call }
|
|
||||||
shutdown
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.rack_app
|
|
||||||
app = Rack::Builder.new do
|
|
||||||
use Rails::Rack::Debugger unless Rails.env.test?
|
|
||||||
run CanvasRails::Application
|
|
||||||
end.to_app
|
|
||||||
|
|
||||||
lambda do |env|
|
|
||||||
nope = [503, {}, [""]]
|
|
||||||
return nope unless allow_requests?
|
|
||||||
|
|
||||||
# wrap request in a mutex so we can ensure it doesn't span spec
|
|
||||||
# boundaries (see clear_requests!)
|
|
||||||
result = request_mutex.synchronize { app.call(env) }
|
|
||||||
|
|
||||||
# check if the spec just finished while we ran, and if so prevent
|
|
||||||
# side effects like redirects (and thus moar requests)
|
|
||||||
if allow_requests?
|
|
||||||
result
|
|
||||||
else
|
|
||||||
# make sure we clean up the body of requests we throw away
|
|
||||||
# https://github.com/rack/rack/issues/658#issuecomment-38476120
|
|
||||||
result.last.close if result.last.respond_to?(:close)
|
|
||||||
nope
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class << self
|
|
||||||
def disallow_requests!
|
|
||||||
# ensure the current in-flight request (if any, AJAX or otherwise)
|
|
||||||
# finishes up its work, and prevent any subsequent requests before the
|
|
||||||
# next spec gets underway. otherwise race conditions can cause sadness
|
|
||||||
# with our shared conn and transactional fixtures (e.g. special
|
|
||||||
# accounts and their caching)
|
|
||||||
@allow_requests = false
|
|
||||||
request_mutex.synchronize { }
|
|
||||||
end
|
|
||||||
|
|
||||||
def allow_requests!
|
|
||||||
@allow_requests = true
|
|
||||||
end
|
|
||||||
|
|
||||||
def allow_requests?
|
|
||||||
@allow_requests
|
|
||||||
end
|
|
||||||
|
|
||||||
def request_mutex
|
|
||||||
@request_mutex ||= Mutex.new
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.start_in_process_thin_server
|
|
||||||
require_relative '../test_setup/servers/thin_server'
|
|
||||||
SpecFriendlyThinServer.run(self.rack_app, BindAddress: BIND_ADDRESS, Port: $server_port, AccessLog: [])
|
|
||||||
self.shutdown_webserver(SpecFriendlyThinServer)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.start_in_process_webrick_server
|
|
||||||
require_relative '../test_setup/servers/webrick_server'
|
|
||||||
SpecFriendlyWEBrickServer.run(self.rack_app, BindAddress: BIND_ADDRESS, Port: $server_port, AccessLog: [])
|
|
||||||
self.shutdown_webserver(SpecFriendlyWEBrickServer)
|
|
||||||
end
|
|
||||||
|
|
||||||
# ====================================================================================================================
|
|
||||||
# Extracted from spec/selenium/common.rb
|
|
||||||
# Modified for Mobile App automation: all things Selenium removed
|
|
||||||
# ====================================================================================================================
|
|
||||||
|
|
||||||
shared_context 'in-process server appium tests' do
|
|
||||||
include OtherHelperMethods
|
|
||||||
include LoginAndSessionMethods
|
|
||||||
|
|
||||||
# set up so you can use rails urls helpers in your selenium tests
|
|
||||||
include Rails.application.routes.url_helpers
|
|
||||||
|
|
||||||
prepend_before :all do
|
|
||||||
$in_proc_webserver_shutdown ||= EnvironmentSetup.start_webserver(WEBSERVER)
|
|
||||||
end
|
|
||||||
|
|
||||||
# tricksy tricksy. grab the current connection, and then always return the same one
|
|
||||||
# (even if on a different thread - i.e. the server's thread), so that it will be in
|
|
||||||
# the same transaction and see the same data
|
|
||||||
before do
|
|
||||||
if self.use_transactional_fixtures
|
|
||||||
@db_connection = ActiveRecord::Base.connection
|
|
||||||
@dj_connection = Delayed::Backend::ActiveRecord::Job.connection
|
|
||||||
|
|
||||||
# synchronize db connection methods for a modicum of thread safety
|
|
||||||
methods_to_sync = %w{execute exec_cache exec_no_cache query}
|
|
||||||
[@db_connection, @dj_connection].each do |conn|
|
|
||||||
methods_to_sync.each do |method_name|
|
|
||||||
if conn.respond_to?(method_name, true) && !conn.respond_to?("#{method_name}_with_synchronization", true)
|
|
||||||
conn.class.class_eval <<-RUBY
|
|
||||||
def #{method_name}_with_synchronization(*args)
|
|
||||||
@mutex ||= Mutex.new
|
|
||||||
@mutex.synchronize { #{method_name}_without_synchronization(*args) }
|
|
||||||
end
|
|
||||||
alias_method_chain :#{method_name}, :synchronization
|
|
||||||
RUBY
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
ActiveRecord::ConnectionAdapters::ConnectionPool.any_instance.stubs(:connection).returns(@db_connection)
|
|
||||||
Delayed::Backend::ActiveRecord::Job.stubs(:connection).returns(@dj_connection)
|
|
||||||
Delayed::Backend::ActiveRecord::Job::Failed.stubs(:connection).returns(@dj_connection)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
after(:each) do
|
|
||||||
EnvironmentSetup.disallow_requests!
|
|
||||||
truncate_all_tables unless self.use_transactional_fixtures
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,140 +0,0 @@
|
||||||
require_relative '../../mobile_common'
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Log In / Out of Mobile App
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
def icanvas_init(username, password, course_name)
|
|
||||||
enter_school
|
|
||||||
login_mobile(username, password)
|
|
||||||
dismiss_user_polling
|
|
||||||
navigate_to_course(course_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
def enter_school
|
|
||||||
find_ele_by_attr('UIATextField', 'value', 'Find your school or district').send_keys(@school)
|
|
||||||
visible_buttons = buttons
|
|
||||||
if visible_buttons.size == 1
|
|
||||||
visible_buttons[0].click
|
|
||||||
else
|
|
||||||
visible_buttons[1].click
|
|
||||||
end
|
|
||||||
wait_true(timeout: 10, interval: 0.100){ tag('UIASecureTextField') }
|
|
||||||
end
|
|
||||||
|
|
||||||
def enter_username(username)
|
|
||||||
email = tag('UIATextField')
|
|
||||||
email.click
|
|
||||||
email.send_keys(username)
|
|
||||||
end
|
|
||||||
|
|
||||||
def enter_password(password)
|
|
||||||
pw = tag('UIASecureTextField')
|
|
||||||
pw.click
|
|
||||||
# bug found in iOS Instruments / Appium: when using Simulator, *send_keys* has undefined behavior
|
|
||||||
# *send_keys* may clear out the previously selected text field
|
|
||||||
sleep(1) # <--- Waiting to send keys after element is selected appears to be a stable temporary fix
|
|
||||||
pw.send_keys(password)
|
|
||||||
end
|
|
||||||
|
|
||||||
def provide_credentials(username, password)
|
|
||||||
enter_username(username)
|
|
||||||
enter_password(password)
|
|
||||||
button_exact('Log In').click
|
|
||||||
end
|
|
||||||
|
|
||||||
def login_mobile(username, password)
|
|
||||||
provide_credentials(username, password)
|
|
||||||
wait_true(timeout: 10, interval: 0.250){ text_exact('Canvas for iOS') }
|
|
||||||
button_exact('Log In').click
|
|
||||||
end
|
|
||||||
|
|
||||||
# ==== Parameters
|
|
||||||
# change_user: App settings for multi-user access must be enabled if change_user is true
|
|
||||||
# If multi-user is disabled, change_user must be false
|
|
||||||
def logout(change_user)
|
|
||||||
if icanvas_app
|
|
||||||
navigate_to('Logout')
|
|
||||||
if change_user
|
|
||||||
find_element(:id, 'Change User').click
|
|
||||||
else
|
|
||||||
find_elements(:id, 'Logout')[1].click
|
|
||||||
end
|
|
||||||
else # speedgrader
|
|
||||||
first_button.click # hamburger
|
|
||||||
find_element(:id, 'Logout').click
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# This assumes the app is on the landing page.
|
|
||||||
def logout_all_users
|
|
||||||
while exists{ find_element(:id, 'icon x delete') }
|
|
||||||
find_element(:id, 'icon x delete').click
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Navigation
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
def goto_courses_root
|
|
||||||
# tap twice to guarantee root view
|
|
||||||
button_exact('Courses').click
|
|
||||||
button_exact('Courses').click
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: add support for Speedgrader
|
|
||||||
def navigate_to_course(course_name)
|
|
||||||
if icanvas_app
|
|
||||||
# Double tap should navigate to root courses view
|
|
||||||
wait_true(timeout: 10, interval: 0.250){ button_exact('Courses') }
|
|
||||||
goto_courses_root
|
|
||||||
scroll_to_element(
|
|
||||||
scroll_view: tag('UIACollectionView'),
|
|
||||||
id: course_name,
|
|
||||||
time: 1000,
|
|
||||||
direction: 'down',
|
|
||||||
attempts: 2
|
|
||||||
).click
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def navigate_to(location, opts = {course_name: ios_course_name})
|
|
||||||
case location
|
|
||||||
when 'My Files', 'About', 'Help', 'Logout'
|
|
||||||
goto_courses_root
|
|
||||||
find_element(:name, 'Profile').click
|
|
||||||
find_element(:name, location).click
|
|
||||||
when 'Calendar', 'To Do List', 'Notifications', 'Messages'
|
|
||||||
button_exact(location).click
|
|
||||||
else
|
|
||||||
navigate_to_course(opts[:course_name])
|
|
||||||
find_element(:name, location).click
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# General
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
def double_tap(element)
|
|
||||||
element.click
|
|
||||||
element.click
|
|
||||||
end
|
|
||||||
|
|
||||||
def device_is_iphone
|
|
||||||
$appium_config[:ios_type] == 'iPhone'
|
|
||||||
end
|
|
||||||
|
|
||||||
def device_is_ipad
|
|
||||||
$appium_config[:ios_type] == 'iPad'
|
|
||||||
end
|
|
||||||
|
|
||||||
def dismiss_user_polling
|
|
||||||
begin
|
|
||||||
find_element(:id, 'How do you like Canvas?')
|
|
||||||
find_element(:id, 'Don\'t ask me again').click
|
|
||||||
rescue => ex
|
|
||||||
raise ex unless ex.is_a?(Selenium::WebDriver::Error::NoSuchElementError)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,106 +0,0 @@
|
||||||
require_relative 'ios_common'
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Shared Examples for iCanvas and Speedgrader Mobile Apps
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
shared_examples 'icanvas and speedgrader landing page' do |app_name|
|
|
||||||
before(:all) do
|
|
||||||
logout_all_users
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'displays a landing page', priority: "1", test_id: pick_test_id_for_app(app_name, 9779, 303715) do
|
|
||||||
expect(find_ele_by_attr('UIATextField', 'value', 'Find your school or district')).to be_displayed
|
|
||||||
|
|
||||||
help_button = open_help_menu(app_name)
|
|
||||||
verify_help_menu(true)
|
|
||||||
close_help_menu(help_button)
|
|
||||||
verify_help_menu(false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'routes to find school domain help page', priority: "1", test_id: pick_test_id_for_app(app_name, 235571, 303716) do
|
|
||||||
open_help_menu(app_name)
|
|
||||||
verify_help_menu(true)
|
|
||||||
button_exact('Find School Domain').click
|
|
||||||
verify_help_menu(false)
|
|
||||||
|
|
||||||
expect(find_element(:id, 'Back')).not_to be_displayed
|
|
||||||
expect(find_element(:id, 'Help')).to be_displayed
|
|
||||||
expect(find_element(:id, 'Done')).to be_displayed
|
|
||||||
|
|
||||||
wait_true(timeout: 10, interval: 0.100){ text_exact('How do I find my institution\'s URL to access Canvas apps on my mobile device?') }
|
|
||||||
|
|
||||||
button_exact('Done').click
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'entering school url' do
|
|
||||||
let(:school_name){ 'Utah Education Network'}
|
|
||||||
let(:school_url){ 'uen.instructure.com'}
|
|
||||||
let(:minimum_list_size){ 4 }
|
|
||||||
|
|
||||||
after(:each) do
|
|
||||||
find_element(:id, 'Cancel').click
|
|
||||||
wait_true(timeout: 10, interval: 0.250){ expect(find_ele_by_attr('UIATextField', 'value', 'Find your school or district')).to be_displayed }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'lists possible schools when entering url and routes to school', priority: "1", test_id: pick_test_id_for_app(app_name, 235572, 303717) do
|
|
||||||
find_school_text = find_ele_by_attr('UIATextField', 'value', 'Find your school or district')
|
|
||||||
expect(tags('UIATableCell').size).to be 0
|
|
||||||
|
|
||||||
# Sending any key should generate a list of possible school
|
|
||||||
find_school_text.send_keys(school_name.split[0])
|
|
||||||
school_list = tags('UIATableCell')
|
|
||||||
expect(school_list.size).to be > minimum_list_size
|
|
||||||
|
|
||||||
# Click on auto-generated text
|
|
||||||
text_exact(school_name).click
|
|
||||||
wait_true(timeout: 10, interval: 0.250){ find_element(:id, school_url) }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'routes to school login page when school is typed in', priority: "1", test_id: pick_test_id_for_app(app_name, 235573, 303718) do
|
|
||||||
enter_school
|
|
||||||
verify_empty_login_view
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Helper Methods
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
# TODO: fix when SpeedGrader Landing Page is updated
|
|
||||||
def open_help_menu(app_name)
|
|
||||||
if app_name == 'icanvas'
|
|
||||||
help_button = find_element(:id, 'Open help menu.')
|
|
||||||
else
|
|
||||||
help_button = buttons[0]
|
|
||||||
end
|
|
||||||
help_button.click
|
|
||||||
help_button
|
|
||||||
end
|
|
||||||
|
|
||||||
# Help menu closes differently between iPhone and iPad devices
|
|
||||||
def close_help_menu(help_button)
|
|
||||||
if device_is_iphone
|
|
||||||
button_exact('Cancel').click
|
|
||||||
else
|
|
||||||
Appium::TouchAction.new.tap(x: help_button.location.x, y: help_button.location.y).perform
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def verify_help_menu(displayed)
|
|
||||||
expect(exists{ text_exact('Help Menu') }).to be displayed
|
|
||||||
expect(exists{ button_exact('Report a Problem') }).to be displayed
|
|
||||||
expect(exists{ button_exact('Request a Feature') }).to be displayed
|
|
||||||
expect(exists{ button_exact('Find School Domain') }).to be displayed
|
|
||||||
expect(exists{ button_exact('Cancel') }).to be displayed if device_is_iphone # not displayed on iPads
|
|
||||||
end
|
|
||||||
|
|
||||||
def verify_empty_login_view
|
|
||||||
wait(timeout: 10, interval: 0.250){ tag('UIASecureTextField') }
|
|
||||||
expect(tag('UIATextField')).to be_displayed
|
|
||||||
expect(tag('UIASecureTextField')).to be_displayed
|
|
||||||
expect(button_exact('Log In')).to be_displayed
|
|
||||||
expect(text_exact('I don\'t know my password')).to be_displayed
|
|
||||||
expect(find_element(:id, 'Cancel')).to be_displayed
|
|
||||||
end
|
|
|
@ -1,147 +0,0 @@
|
||||||
require_relative 'ios_common'
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Shared Examples for iCanvas and Speedgrader Mobile Apps
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
shared_context 'icanvas and speedgrader login credentials' do |app_name|
|
|
||||||
before(:all) do
|
|
||||||
user_with_pseudonym(username: 'teacher1', password: 'teacher')
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'displays login view' do
|
|
||||||
# using *mobiledev* as school rather than test instance because test instance will not have *.instructure.com*
|
|
||||||
let(:school_name){ 'mobiledev' }
|
|
||||||
let(:school_url){ school_name + '.instructure.com' }
|
|
||||||
|
|
||||||
before(:each) do
|
|
||||||
find_ele_by_attr('UIATextField', 'value', 'Find your school or district').send_keys(school_name)
|
|
||||||
visible_buttons = buttons
|
|
||||||
visible_buttons.size == 1 ? visible_buttons[0].click : visible_buttons[1].click
|
|
||||||
end
|
|
||||||
|
|
||||||
after(:each) do
|
|
||||||
find_element(:id, 'Cancel').click if exists{ find_element(:id, 'Cancel') }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'displays correct url for selected school', priority: "1", test_id: pick_test_id_for_app(app_name, 14040, 303727) do
|
|
||||||
wait_true(timeout: 10, interval: 0.250){ expect(find_element(:id, school_url)).to be_displayed }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'displays cancel button which returns to landing page', priority: "1", test_id: pick_test_id_for_app(app_name, 251028, 303728) do
|
|
||||||
expect(find_element(:id, 'Cancel')).to be_displayed
|
|
||||||
find_element(:id, 'Cancel').click
|
|
||||||
expect(find_ele_by_attr('UIATextField', 'value', 'Find your school or district')).to be_displayed
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'user provides bad credentials' do
|
|
||||||
before(:all) do
|
|
||||||
enter_school
|
|
||||||
end
|
|
||||||
|
|
||||||
# Clear Email field between runs
|
|
||||||
after(:each) do
|
|
||||||
tag('UIATextField').clear
|
|
||||||
end
|
|
||||||
|
|
||||||
# Return to 'Find your school or district'
|
|
||||||
after(:all) do
|
|
||||||
button_exact('Cancel').click
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'fails login with incorrect username', priority: "1", test_id: pick_test_id_for_app(app_name, 14042, 303723) do
|
|
||||||
provide_credentials('Chester Copperpot', user_password(@user))
|
|
||||||
verify_login_view('Chester Copperpot', 'Incorrect username and/or password')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'fails login when password omitted', priority: "1", test_id: pick_test_id_for_app(app_name, 238142, 303726) do
|
|
||||||
provide_credentials('Chester Copperpot', '')
|
|
||||||
verify_login_view('Chester Copperpot', 'No password was given')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'fails login with incorrect password', priority: "1", test_id: pick_test_id_for_app(app_name, 14043, 303724) do
|
|
||||||
provide_credentials(@user.primary_pseudonym.unique_id, '1234')
|
|
||||||
verify_login_view(@user.primary_pseudonym.unique_id, 'Incorrect username and/or password')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'user forgot their password' do
|
|
||||||
before(:each) do
|
|
||||||
enter_school
|
|
||||||
end
|
|
||||||
|
|
||||||
# Return to 'Find your school or district'
|
|
||||||
after(:each) do
|
|
||||||
button_exact('Cancel').click
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'routes to password reset view', priority: "1", test_id: pick_test_id_for_app(app_name, 235575, 303729) do
|
|
||||||
text_exact('I don\'t know my password').click
|
|
||||||
|
|
||||||
message = 'Enter your Email and we\'ll send you a link to change your password.'
|
|
||||||
email = tag('UIATextField')
|
|
||||||
expect(text_exact(message)).to be_displayed
|
|
||||||
expect(email).to be_displayed
|
|
||||||
expect(email.name).to eq(message)
|
|
||||||
expect(email.text).to eq('Email')
|
|
||||||
expect(button_exact('Request Password')).to be_displayed
|
|
||||||
expect(text_exact('Back to Login')).to be_displayed
|
|
||||||
|
|
||||||
# return to Login view
|
|
||||||
text_exact('Back to Login').click
|
|
||||||
expect(tag('UIASecureTextField')).to be_displayed
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'user provides good credentials' do
|
|
||||||
before(:each) do
|
|
||||||
enter_school
|
|
||||||
provide_credentials(@user.primary_pseudonym.unique_id, user_password(@user))
|
|
||||||
end
|
|
||||||
|
|
||||||
after(:each) do
|
|
||||||
logout(false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'passes login and routes to home page', priority: "1", test_id: pick_test_id_for_app(app_name, 14041, 303730) do
|
|
||||||
# App requests access to Canvas account; need to wait for next WebView to load
|
|
||||||
wait_true(timeout: 10, interval: 0.250){ text_exact(app_login_message) }
|
|
||||||
expect(text_exact(app_access_message)).to be_displayed
|
|
||||||
|
|
||||||
# Verify paragraph text includes username
|
|
||||||
links = tags('UIALink')
|
|
||||||
expect(text_exact('You are logging into this app as')).to be_displayed
|
|
||||||
expect(links[0].name).to eq(@user.primary_pseudonym.unique_id)
|
|
||||||
expect(text_exact('.')).to be_displayed
|
|
||||||
expect(button_exact('Log In')).to be_displayed
|
|
||||||
expect(links[1].name).to eq('Cancel')
|
|
||||||
expect(text_exact('Cancel')).to be_displayed
|
|
||||||
expect(text_exact('Remember my authorization for this service')).to be_displayed
|
|
||||||
|
|
||||||
# Toggle switch and click
|
|
||||||
auth_switch = tag('UIASwitch')
|
|
||||||
expect(auth_switch).to be_displayed
|
|
||||||
expect(auth_switch.name).to eq('Remember my authorization for this service')
|
|
||||||
auth_switch.click
|
|
||||||
button_exact('Log In').click
|
|
||||||
|
|
||||||
# User is occasionally polled here
|
|
||||||
dismiss_user_polling
|
|
||||||
wait_true(timeout: 10, interval: 0.250){ find_element(:id, app_login_success) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Helper Methods
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
def verify_login_view(username, error_message)
|
|
||||||
wait_true(timeout: 10, interval: 0.250){ expect(tag('UIATextField').text).to eq(username) }
|
|
||||||
expect(tag('UIASecureTextField').text).to eq('Password')
|
|
||||||
find_ele_by_attr('UIAStaticText', 'name', error_message)
|
|
||||||
expect(button_exact('Log In')).to be_displayed
|
|
||||||
expect(text_exact('I don\'t know my password')).to be_displayed
|
|
||||||
expect(find_element(:id, 'Cancel')).to be_displayed
|
|
||||||
end
|
|
|
@ -1,8 +0,0 @@
|
||||||
require_relative '../../../helpers/landing_page_common'
|
|
||||||
|
|
||||||
describe 'icanvas landing page' do
|
|
||||||
include_context 'in-process server appium tests'
|
|
||||||
include_context 'appium mobile specs', 'icanvas'
|
|
||||||
|
|
||||||
it_behaves_like 'icanvas and speedgrader landing page', 'icanvas'
|
|
||||||
end
|
|
|
@ -1,12 +0,0 @@
|
||||||
require_relative '../../../helpers/login_common'
|
|
||||||
|
|
||||||
describe 'user logging into icanvas app' do
|
|
||||||
include_context 'in-process server appium tests'
|
|
||||||
include_context 'appium mobile specs', 'icanvas'
|
|
||||||
let(:app_login_message){ 'Canvas for iOS' }
|
|
||||||
let(:app_access_message){ 'Canvas for iOS is requesting access to your account.' }
|
|
||||||
let(:app_login_success){ 'Profile' }
|
|
||||||
|
|
||||||
# examples located in: spec/selenium/appium/ios/helpers/login_common.rb
|
|
||||||
it_behaves_like 'icanvas and speedgrader login credentials', 'icanvas'
|
|
||||||
end
|
|
|
@ -1,8 +0,0 @@
|
||||||
require_relative '../../../helpers/landing_page_common'
|
|
||||||
|
|
||||||
describe 'speedgrader for ios landing page' do
|
|
||||||
include_context 'in-process server appium tests'
|
|
||||||
include_context 'appium mobile specs', 'speedgrader_ios'
|
|
||||||
|
|
||||||
it_behaves_like 'icanvas and speedgrader landing page', 'speedgrader_ios'
|
|
||||||
end
|
|
|
@ -1,12 +0,0 @@
|
||||||
require_relative '../../../helpers/login_common'
|
|
||||||
|
|
||||||
describe 'user logging into speedgrader app' do
|
|
||||||
include_context 'in-process server appium tests'
|
|
||||||
include_context 'appium mobile specs', 'speedgrader_ios'
|
|
||||||
let(:app_login_message){ 'SpeedGrader' }
|
|
||||||
let(:app_access_message){ 'SpeedGrader is requesting access to your account.' }
|
|
||||||
let(:app_login_success){ 'CSGSlideMenuView' }
|
|
||||||
|
|
||||||
# examples located in: spec/selenium/appium/ios/helpers/login_common.rb
|
|
||||||
it_behaves_like 'icanvas and speedgrader login credentials', 'speedgrader_ios'
|
|
||||||
end
|
|
|
@ -1,233 +0,0 @@
|
||||||
require 'appium_lib'
|
|
||||||
require_relative 'environment_setup'
|
|
||||||
|
|
||||||
include EnvironmentSetup
|
|
||||||
|
|
||||||
# Mobile canvas and speedgrader apps have features that behave the same in both apps.
|
|
||||||
# Because each app still has its own test case in TestRails, this method chooses
|
|
||||||
# the proper test_id for examples that can be shared between app specs.
|
|
||||||
def pick_test_id_for_app(app_name, canvas, speedgrader)
|
|
||||||
app_name =~ /(speedgrader)/ ? speedgrader : canvas
|
|
||||||
end
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Shared Contexts and Helper Methods for Canvas Test Environment
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
shared_context 'appium mobile specs' do |app_name|
|
|
||||||
before(:all) do
|
|
||||||
@app_name = app_name
|
|
||||||
# appium_init(app_name) # TODO: uncomment to run Appium tests
|
|
||||||
skip('Appium not yet integrated with Jenkins') # TODO: removed when Appium is integrated with Jenkins
|
|
||||||
create_developer_key
|
|
||||||
toggle_fail_fast(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
after(:all) do
|
|
||||||
toggle_fail_fast(false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
shared_context 'teacher and student users' do |app_name|
|
|
||||||
before(:all) do
|
|
||||||
course(course_name: app_name =~ /(android)/ ? android_course_name : ios_course_name)
|
|
||||||
@course.offer
|
|
||||||
@teacher = user_with_pseudonym(username: 'teacher1', unique_id: 'teacher1', password: 'teacher', active_user: true)
|
|
||||||
@student = user_with_pseudonym(username: 'student1', unique_id: 'student1', password: 'student', active_user: true)
|
|
||||||
@course.enroll_user(@teacher, 'TeacherEnrollment').accept!
|
|
||||||
@course.enroll_user(@student).accept!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
shared_context 'course with all user groups' do |app_name|
|
|
||||||
before(:all) do
|
|
||||||
course(course_name: app_name =~ /(android)/ ? android_course_name : ios_course_name)
|
|
||||||
@course.offer
|
|
||||||
@teacher = user_with_pseudonym(username: 'teacher1', unique_id: 'teacher1', password: 'teacher', active_user: true)
|
|
||||||
@ta = user_with_pseudonym(username: 'assistant1', unique_id: 'assistant1', password: 'assistant', active_user: true)
|
|
||||||
@students = []
|
|
||||||
@observers = []
|
|
||||||
5.times do |i|
|
|
||||||
@students << user_with_pseudonym(username: "student#{i+1}", unique_id: "student#{i+1}", password: 'student', active_user: true)
|
|
||||||
@observers << user_with_pseudonym(username: "observer#{i+1}", unique_id: "observer#{i+1}", password: 'observer', active_user: true)
|
|
||||||
@course.enroll_user(@students[i]).accept!
|
|
||||||
@course.enroll_user(@observers[i], 'ObserverEnrollment').accept!
|
|
||||||
end
|
|
||||||
@course.enroll_user(@teacher, 'TeacherEnrollment').accept!
|
|
||||||
@course.enroll_user(@ta, 'TaEnrollment').accept!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
shared_context 'course with a single user' do |role, app_name|
|
|
||||||
before(:all) do
|
|
||||||
basic_course_setup(role, app_name)
|
|
||||||
mobile_app_init(app_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
after(:all) do
|
|
||||||
logout(false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def basic_course_setup(role, app_name)
|
|
||||||
case role
|
|
||||||
when 'teacher'
|
|
||||||
course_with_teacher(course_arguments(role, app_name))
|
|
||||||
when 'student'
|
|
||||||
course_with_student(course_arguments(role, app_name))
|
|
||||||
else
|
|
||||||
raise('Unsupported role for custom user shared context. Additional roles coming soon...')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def course_arguments(role, app_name)
|
|
||||||
{ course_name: app_name =~ /(android)/ ? android_course_name : ios_course_name,
|
|
||||||
user: user_with_pseudonym(username: role + '1', unique_id: role + '1', password: role, active_user: true),
|
|
||||||
active_all: true }
|
|
||||||
end
|
|
||||||
|
|
||||||
def mobile_app_init(app_name)
|
|
||||||
case app_name
|
|
||||||
when 'candroid', 'speedgrader_android'
|
|
||||||
android_app_init(@user.primary_pseudonym.unique_id, user_password(@user), @course.name)
|
|
||||||
when 'icanvas', 'speedgrader_ios'
|
|
||||||
icanvas_init(@user.primary_pseudonym.unique_id, user_password(@user), @course.name)
|
|
||||||
else
|
|
||||||
raise('Unsupported mobile application.')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def user_password(user)
|
|
||||||
user.primary_pseudonym.unique_id =~ /[a-z]+1/ ? user.primary_pseudonym.unique_id.sub(/[0-9]/, '') : user.primary_pseudonym.unique_id
|
|
||||||
end
|
|
||||||
|
|
||||||
def candroid_app
|
|
||||||
@app_name == 'candroid'
|
|
||||||
end
|
|
||||||
|
|
||||||
def icanvas_app
|
|
||||||
@app_name == 'icanvas'
|
|
||||||
end
|
|
||||||
|
|
||||||
def toggle_fail_fast(flag)
|
|
||||||
# this tells rspec to not run remaining tests in the spec if a test fails
|
|
||||||
# with mobile we can't guarantee the app is navigated to a specific location, so we fail quickly to not waste time
|
|
||||||
RSpec.configure do |c|
|
|
||||||
c.fail_fast = flag
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Appium
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
def start_appium_driver
|
|
||||||
Appium::Driver.new(caps: @capabilities, appium_lib: @appium_lib).start_driver
|
|
||||||
Appium.promote_appium_methods(RSpec::Core::ExampleGroup)
|
|
||||||
set_wait(implicit_wait_time)
|
|
||||||
end
|
|
||||||
|
|
||||||
def appium_init_android
|
|
||||||
@capabilities = {
|
|
||||||
platformName: 'Android',
|
|
||||||
deviceName: android_device_name
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def appium_init_ios
|
|
||||||
device = ios_device
|
|
||||||
@capabilities = {
|
|
||||||
platformName: 'iOS',
|
|
||||||
versionNumber: device[:versionNumber],
|
|
||||||
deviceName: device[:deviceName],
|
|
||||||
udid: device[:udid],
|
|
||||||
app: device[:app],
|
|
||||||
autoAcceptAlerts: true,
|
|
||||||
# sendKeysStrategy: 'setValue',
|
|
||||||
waitForAppScript: '$.delay(7500); $.acceptAlert();' # auto-accepts device alerts when app launches, and prompts
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def appium_init(app_name)
|
|
||||||
@school = school_domain
|
|
||||||
@appium_lib = { server_url: appium_server_url }
|
|
||||||
case app_name
|
|
||||||
when 'candroid', 'speedgrader_android'
|
|
||||||
appium_init_android
|
|
||||||
when 'icanvas', 'speedgrader_ios'
|
|
||||||
appium_init_ios
|
|
||||||
else
|
|
||||||
raise('unsupported mobile platform')
|
|
||||||
end
|
|
||||||
start_appium_driver
|
|
||||||
end
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Scrolling
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
def scroll_within_y_coordinates(direction, top, bottom)
|
|
||||||
x = window_size.width / 2
|
|
||||||
if direction == 'up'
|
|
||||||
start_y = top * 1.2
|
|
||||||
end_y = bottom * 0.9
|
|
||||||
else
|
|
||||||
start_y = bottom * 0.9
|
|
||||||
end_y = top * 1.2
|
|
||||||
end
|
|
||||||
|
|
||||||
action = Appium::TouchAction.new.press(x: x, y: start_y).wait(2000).move_to(x: x, y: end_y).release
|
|
||||||
action.perform
|
|
||||||
end
|
|
||||||
|
|
||||||
def scroll_to_element(opts)
|
|
||||||
count = 0
|
|
||||||
begin
|
|
||||||
scroll_to_element_locator(opts)
|
|
||||||
rescue Selenium::WebDriver::Error::NoSuchElementError
|
|
||||||
scroll_vertically_in_view(opts[:scroll_view], opts[:time], opts[:direction])
|
|
||||||
retry unless (count += 1) > opts[:attempts]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def scroll_to_element_locator(opts)
|
|
||||||
case opts[:strategy]
|
|
||||||
when 'id'
|
|
||||||
return find_element(:id, opts[:id])
|
|
||||||
when 'tag'
|
|
||||||
return tag(opts[:tag])
|
|
||||||
when 'text_exact'
|
|
||||||
return text_exact(opts[:text_exact])
|
|
||||||
else
|
|
||||||
raise('Unsupported locator strategy for scroll_to_element.')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Time is in milliseconds, so unless you want this to be a click send 1000 rather than 1
|
|
||||||
def scroll_vertically_in_view(scroll_view, time, direction)
|
|
||||||
x = scroll_view.location.x + (0.5 * scroll_view.size.width)
|
|
||||||
|
|
||||||
if direction == 'up'
|
|
||||||
start_y = scroll_view.location.y + (0.1 * scroll_view.size.height)
|
|
||||||
end_y = scroll_view.location.y + (0.9 * scroll_view.size.height)
|
|
||||||
else
|
|
||||||
start_y = scroll_view.location.y + (0.9 * scroll_view.size.height)
|
|
||||||
end_y = scroll_view.location.y + (0.1 * scroll_view.size.height)
|
|
||||||
end
|
|
||||||
|
|
||||||
action = Appium::TouchAction.new.press(x: x, y: start_y).wait(time).move_to(x: x, y: end_y).release
|
|
||||||
action.perform
|
|
||||||
end
|
|
||||||
|
|
||||||
def refresh_view(view)
|
|
||||||
scroll_vertically_in_view(view, 2, 'up')
|
|
||||||
end
|
|
||||||
|
|
||||||
# ======================================================================================================================
|
|
||||||
# Regex
|
|
||||||
# ======================================================================================================================
|
|
||||||
|
|
||||||
# returns regex which matches 12 and 24 hours time formats
|
|
||||||
def time_format
|
|
||||||
/(([1]{1}[0-2]{1}|[0-9]{1}):[0-5]{1}[0-9]{1}\s(AM|PM))|(([1-2]{1}[0-9]{1}|[0]?[0-9]{1}):[0-5]{1}[0-9]{1})/
|
|
||||||
end
|
|
|
@ -229,9 +229,6 @@ def truncate_table(model)
|
||||||
begin
|
begin
|
||||||
old_proc = model.connection.raw_connection.set_notice_processor {}
|
old_proc = model.connection.raw_connection.set_notice_processor {}
|
||||||
model.connection.execute("TRUNCATE TABLE #{model.connection.quote_table_name(model.table_name)} CASCADE")
|
model.connection.execute("TRUNCATE TABLE #{model.connection.quote_table_name(model.table_name)} CASCADE")
|
||||||
|
|
||||||
# mobile verify expects specific id seq for developer key, this forces the sequence to always start at 101
|
|
||||||
model.connection.execute("SELECT setval('developer_keys_id_seq', 100);") if model == DeveloperKey
|
|
||||||
ensure
|
ensure
|
||||||
model.connection.raw_connection.set_notice_processor(&old_proc)
|
model.connection.raw_connection.set_notice_processor(&old_proc)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue