Upgrade to Selenium 4

Switches from standalone containers to explicit node+hub config

Selenium 4 has some differences in handling stale elements that we
should be aware of moving forward

closes OUT-4988
flag=none
[skip-stages=Flakey Spec Catcher]

Test-plan:
- make sure screenshots can happen for failures
- retrigger a few times and make sure things pass
- verify build summaries are intact
- verify FSC can still run seleniums
- verify local selenium running still works
  - firefox / chrome / edge where applicable
- verify docker selenium running still works
  - firefox / chrome / edge where applicable

Change-Id: I8f2fe5a34d712b5ccd7191bae7a9aeeb6f1f473d
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/284811
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: James Butters <jbutters@instructure.com>
QA-Review: Robin Kuss <rkuss@instructure.com>
Product-Review: Brian Watson <bwatson@instructure.com>
This commit is contained in:
Brian Watson 2022-02-09 15:53:07 -07:00
parent df7130ba93
commit 2f2a27d91b
37 changed files with 180 additions and 201 deletions

View File

@ -41,9 +41,9 @@ group :test do
gem "once-ler", "2.0.0" gem "once-ler", "2.0.0"
gem "sauce_whisk", "0.2.2" gem "sauce_whisk", "0.2.2"
gem "selenium-webdriver", "3.142.7", require: false gem "selenium-webdriver", "~> 4.1.0", require: false
gem "childprocess", "3.0.0", require: false gem "childprocess", "3.0.0", require: false
gem "webdrivers", "4.2.0", require: false gem "webdrivers", "5.0.0", require: false
gem "testrailtagging", "0.3.8.7", require: false gem "testrailtagging", "0.3.8.7", require: false
gem "webmock", "3.8.2", require: false gem "webmock", "3.8.2", require: false

View File

@ -68,7 +68,6 @@ pipeline {
RSPECQ_UPDATE_TIMINGS = "${env.GERRIT_EVENT_TYPE == 'change-merged' ? '1' : '0'}" RSPECQ_UPDATE_TIMINGS = "${env.GERRIT_EVENT_TYPE == 'change-merged' ? '1' : '0'}"
ENABLE_AXE_SELENIUM = "${env.ENABLE_AXE_SELENIUM}" ENABLE_AXE_SELENIUM = "${env.ENABLE_AXE_SELENIUM}"
POSTGRES_PASSWORD = 'sekret' POSTGRES_PASSWORD = 'sekret'
SELENIUM_VERSION = '3.141.59-20210929'
RSPECQ_REDIS_URL = redisUrl() RSPECQ_REDIS_URL = redisUrl()
PATCHSET_TAG = getPatchsetTag() PATCHSET_TAG = getPatchsetTag()
CANVAS_ZEITWERK = '1' CANVAS_ZEITWERK = '1'
@ -103,18 +102,18 @@ pipeline {
projectName: env.JOB_NAME, projectName: env.JOB_NAME,
selector: specific(env.BUILD_NUMBER), selector: specific(env.BUILD_NUMBER),
) )
withEnv(['COMPOSE_FILE=docker-compose.new-jenkins.yml']) { withEnv(['COMPOSE_FILE=docker-compose.new-jenkins.yml']) {
sh """ sh """
docker-compose run -v \$(pwd)/\$LOCAL_WORKDIR/tmp/coverage/:/tmp/coverage \ docker-compose run -v \$(pwd)/\$LOCAL_WORKDIR/tmp/coverage/:/tmp/coverage \
--name coverage-collator canvas bash -c \ --name coverage-collator canvas bash -c \
"bundle install; bundle exec rake coverage:report['/tmp/coverage/canvas__*/**']" "bundle install; bundle exec rake coverage:report['/tmp/coverage/canvas__*/**']"
""" """
sh 'docker cp coverage-collator:/usr/src/app/coverage/ coverage' sh 'docker cp coverage-collator:/usr/src/app/coverage/ coverage'
archiveArtifacts allowEmptyArchive: true, artifacts: 'coverage/**' archiveArtifacts allowEmptyArchive: true, artifacts: 'coverage/**'
publishHTML target: [ publishHTML target: [
allowMissing: false, allowMissing: false,
alwaysLinkToLastBuild: false, alwaysLinkToLastBuild: false,
@ -123,7 +122,7 @@ pipeline {
reportFiles: 'index.html', reportFiles: 'index.html',
reportName: 'Ruby Coverage Report' reportName: 'Ruby Coverage Report'
] ]
uploadCoverage([ uploadCoverage([
uploadSource: '/coverage', uploadSource: '/coverage',
uploadDest: env.COVERAGE_LOCATION uploadDest: env.COVERAGE_LOCATION

View File

@ -168,7 +168,6 @@ pipeline {
RSPECQ_UPDATE_TIMINGS = "${env.GERRIT_EVENT_TYPE == 'change-merged' ? '1' : '0'}" RSPECQ_UPDATE_TIMINGS = "${env.GERRIT_EVENT_TYPE == 'change-merged' ? '1' : '0'}"
ENABLE_AXE_SELENIUM = "${env.ENABLE_AXE_SELENIUM}" ENABLE_AXE_SELENIUM = "${env.ENABLE_AXE_SELENIUM}"
POSTGRES_PASSWORD = 'sekret' POSTGRES_PASSWORD = 'sekret'
SELENIUM_VERSION = '3.141.59-20210929'
RSPECQ_REDIS_URL = redisUrl() RSPECQ_REDIS_URL = redisUrl()
CANVAS_ZEITWERK = '1' CANVAS_ZEITWERK = '1'
} }

View File

@ -189,7 +189,6 @@ pipeline {
// until we figure out how to run them, we should ignore them // until we figure out how to run them, we should ignore them
FSC_IGNORE_FILES = 'gems/.*/spec/,spec/contracts/,engines/.*/spec/,spec/selenium/performance/' FSC_IGNORE_FILES = 'gems/.*/spec/,spec/contracts/,engines/.*/spec/,spec/selenium/performance/'
POSTGRES_PASSWORD = 'sekret' POSTGRES_PASSWORD = 'sekret'
SELENIUM_VERSION = '3.141.59-20210929'
// Targeting 10 minutes / node, each node runs RSPEC_PROCESSES threads and // Targeting 10 minutes / node, each node runs RSPEC_PROCESSES threads and
// repeats each test FSC_REPEAT_FACTOR times. // repeats each test FSC_REPEAT_FACTOR times.

View File

@ -37,7 +37,6 @@ pipeline {
RUBY = configuration.ruby() RUBY = configuration.ruby()
RERUNS_RETRY = 0 // no reruns RERUNS_RETRY = 0 // no reruns
POSTGRES_PASSWORD = 'sekret' POSTGRES_PASSWORD = 'sekret'
SELENIUM_VERSION = '3.141.59-20210929'
CASSANDRA_IMAGE_TAG = imageTag.cassandra() CASSANDRA_IMAGE_TAG = imageTag.cassandra()
DYNAMODB_IMAGE_TAG = imageTag.dynamodb() DYNAMODB_IMAGE_TAG = imageTag.dynamodb()

View File

@ -1,5 +1,4 @@
ARG SELENIUM_VERSION=3.141.59-20210929 FROM selenium/node-chrome:98.0
FROM selenium/standalone-chrome-debug:$SELENIUM_VERSION
COPY entry_point.sh /opt/bin/custom_entry_point.sh COPY entry_point.sh /opt/bin/custom_entry_point.sh
USER root USER root

View File

@ -17,7 +17,8 @@ DOCKER_IMAGES=(
$POSTGRES_IMAGE_TAG $POSTGRES_IMAGE_TAG
$REGISTRY_BASE/canvas-rce-api $REGISTRY_BASE/canvas-rce-api
$REGISTRY_BASE/redis:alpine $REGISTRY_BASE/redis:alpine
$REGISTRY_BASE/selenium-chrome:"${SELENIUM_VERSION:-3.141.59-20210929}" $REGISTRY_BASE/selenium-node-chrome:"${CHROME_VERSION:-98.0}"
$REGISTRY_BASE/selenium-hub:4.1.2-20220217
) )
echo "${DOCKER_IMAGES[@]}" | xargs -P0 -n1 ./build/new-jenkins/docker-with-flakey-network-protection.sh pull & echo "${DOCKER_IMAGES[@]}" | xargs -P0 -n1 ./build/new-jenkins/docker-with-flakey-network-protection.sh pull &

View File

@ -29,8 +29,7 @@ def createDistribution(nestedStages) {
def baseEnvVars = [ def baseEnvVars = [
"ENABLE_AXE_SELENIUM=${env.ENABLE_AXE_SELENIUM}", "ENABLE_AXE_SELENIUM=${env.ENABLE_AXE_SELENIUM}",
'POSTGRES_PASSWORD=sekret', 'POSTGRES_PASSWORD=sekret'
'SELENIUM_VERSION=3.141.59-20210929'
] ]
def rspecqEnvVars = baseEnvVars + [ def rspecqEnvVars = baseEnvVars + [
@ -70,8 +69,7 @@ def createDistribution(nestedStages) {
def createLegacyDistribution(nestedStages) { def createLegacyDistribution(nestedStages) {
def setupNodeHook = this.&setupNode def setupNodeHook = this.&setupNode
def baseEnvVars = [ def baseEnvVars = [
'POSTGRES_PASSWORD=sekret', 'POSTGRES_PASSWORD=sekret'
'SELENIUM_VERSION=3.141.59-20210929'
] ]
// Used only for crystalball map generation // Used only for crystalball map generation

View File

@ -245,9 +245,11 @@ To enable Selenium: Add `docker-compose/selenium.override.yml` to your `COMPOSE_
The container used to run the selenium browser is only started when spinning up The container used to run the selenium browser is only started when spinning up
all docker-compose containers, or when specified explicitly. The selenium all docker-compose containers, or when specified explicitly. The selenium
container needs to be started before running any specs that require selenium. container needs to be started before running any specs that require selenium.
Select a browser to run in selenium through config/selenium.yml and then ensure
that only the corresponding browser is configured in selenium.override.yml.
```sh ```sh
docker-compose up selenium-firefox # or selenium-chrome or selenium-edge docker-compose up -d selenium-hub
``` ```
With the container running, you should be able to open a VNC session: With the container running, you should be able to open a VNC session:

View File

@ -6,20 +6,13 @@ services:
- selenium-chrome - selenium-chrome
- canvasrceapi - canvasrceapi
environment: environment:
remote_url: http://selenium-chrome:4444/wd/hub remote_url: http://selenium-hub:4444/wd/hub
browser: chrome browser: chrome
RCE_HOST: "http://canvasrceapi" RCE_HOST: "http://canvasrceapi"
# these are so we can use prod compiled assets in test environment # these are so we can use prod compiled assets in test environment
USE_OPTIMIZED_JS: 'true' USE_OPTIMIZED_JS: 'true'
SASS_STYLE: 'compressed' SASS_STYLE: 'compressed'
selenium-chrome:
image: starlord.inscloudgate.net/jenkins/selenium-chrome:$SELENIUM_VERSION
environment:
SCREEN_WIDTH: 1680
SCREEN_HEIGHT: 1050
init: true
canvasrceapi: canvasrceapi:
image: starlord.inscloudgate.net/jenkins/canvas-rce-api image: starlord.inscloudgate.net/jenkins/canvas-rce-api
environment: environment:
@ -33,3 +26,53 @@ services:
STATSD_HOST: 127.0.0.1 STATSD_HOST: 127.0.0.1
STATSD_PORT: 8125 STATSD_PORT: 8125
init: true init: true
selenium-hub:
image: starlord.inscloudgate.net/jenkins/selenium-hub:4.1.2-20220217
environment:
GRID_MAX_SESSION: 64
GRID_BROWSER_TIMEOUT: 3000
selenium-chrome: &NODE_CHROME
image: starlord.inscloudgate.net/jenkins/selenium-node-chrome:98.0
environment: &NODE_CHROME_ENV
SE_EVENT_BUS_HOST: selenium-hub
SE_EVENT_BUS_PUBLISH_PORT: 4442
SE_EVENT_BUS_SUBSCRIBE_PORT: 4443
HUB_PORT_4444_TCP_ADDR: selenium-hub
HUB_PORT_4444_TCP_PORT: 4444
SE_NODE_HOST: selenium-chrome
JAVA_OPTS: '-Dwebdriver.chrome.whitelistedIps='
init: true
links:
- selenium-hub
selenium-chrome2:
<<: *NODE_CHROME
environment:
<<: *NODE_CHROME_ENV
SE_NODE_HOST: selenium-chrome2
selenium-chrome3:
<<: *NODE_CHROME
environment:
<<: *NODE_CHROME_ENV
SE_NODE_HOST: selenium-chrome3
selenium-chrome4:
<<: *NODE_CHROME
environment:
<<: *NODE_CHROME_ENV
SE_NODE_HOST: selenium-chrome4
selenium-chrome5:
<<: *NODE_CHROME
environment:
<<: *NODE_CHROME_ENV
SE_NODE_HOST: selenium-chrome5
selenium-chrome6:
<<: *NODE_CHROME
environment:
<<: *NODE_CHROME_ENV
SE_NODE_HOST: selenium-chrome6

View File

@ -1,6 +1,6 @@
test: test:
remote_url_firefox: http://selenium-firefox:4444/wd/hub remote_url_firefox: http://selenium-hub:4444/wd/hub
remote_url_chrome: http://selenium-chrome:4444/wd/hub remote_url_chrome: http://selenium-hub:4444/wd/hub
remote_url_edge: http://selenium-edge:4444/wd/hub remote_url_edge: http://selenium-hub:4444/wd/hub
browser: chrome browser: chrome
# auto_open_devtools: true # auto_open_devtools: true

View File

@ -1,5 +1,4 @@
ARG SELENIUM_VERSION=3.141.59-20210929 FROM selenium/node-chrome:98.0
FROM selenium/standalone-chrome-debug:$SELENIUM_VERSION
COPY entry_point.sh /opt/bin/custom_entry_point.sh COPY entry_point.sh /opt/bin/custom_entry_point.sh
USER root USER root

View File

@ -1,4 +1,4 @@
FROM selenium/standalone-edge:96.0 FROM selenium/node-edge:98.0
COPY entry_point.sh /opt/bin/custom_entry_point.sh COPY entry_point.sh /opt/bin/custom_entry_point.sh
USER root USER root

View File

@ -1,5 +1,5 @@
# Keep this image version tag synced with Gemfile.d/test.rb # Keep this image version tag synced with Gemfile.d/test.rb
FROM selenium/standalone-firefox-debug:3.12.0-americium FROM selenium/node-firefox:96.0
COPY entry_point.sh /opt/bin/custom_entry_point.sh COPY entry_point.sh /opt/bin/custom_entry_point.sh
USER root USER root

View File

@ -5,35 +5,38 @@ version: '2.3'
services: services:
web: web:
links: links:
- selenium-chrome - selenium-hub
#- selenium-firefox
#- selenium-edge
selenium-chrome: ## We list all of the different standalone containers as `selenium-hub` since Jenkins
build: ./docker-compose/selenium-chrome ## will use the actual hub + node configuration instead of a standalone for performance
## reasons. Listing all of them as selenium-hub saves a great deal of configuration issues
## Chrome
selenium-hub:
image: selenium/standalone-chrome
environment:
SE_NODE_GRID_URL: selenium-hub:4444/wd/hub
VIRTUAL_HOST: seleniumch.docker
init: true
ports: ports:
- 5901:5900 - 5901:5900
environment:
VIRTUAL_HOST: seleniumch.docker
remote_url: http://seleniumch.docker/wd/hub
browser: chrome
# selenium-firefox: ## Firefox
# build: ./docker-compose/selenium-firefox # selenium-hub:
# image: selenium/standalone-firefox
# environment:
# SE_NODE_GRID_URL: selenium-hub:4444/wd/hub
# VIRTUAL_HOST: seleniumff.docker
# init: true
# ports: # ports:
# - 5900:5900 # - 5900:5900
# environment:
# VIRTUAL_HOST: seleniumff.docker
# remote_url: http://seleniumff.docker/wd/hub
# browser: firefox
# selenium-edge: ## Edge
# build: ./docker-compose/selenium-edge # selenium-hub:
# image: selenium/standalone-edge
# environment:
# SE_NODE_GRID_URL: selenium-hub:4444/wd/hub
# VIRTUAL_HOST: seleniumedge.docker
# init: true
# ports: # ports:
# - 5902:5900 # - 5902:5900
# environment:
# VIRTUAL_HOST: seleniumedge.docker
# remote_url: http://seleniumedge.docker/wd/hub
# browser: edge
# shm_size: 2gb

View File

@ -120,7 +120,7 @@ function display_next_steps {
echo ':docker-compose/selenium.override.yml' >> .env echo ':docker-compose/selenium.override.yml' >> .env
build the selenium container build the selenium container
${DOCKER_COMMAND} build selenium-chrome ${DOCKER_COMMAND} build selenium-hub
run selenium run selenium
${DOCKER_COMMAND} run --rm web bundle exec rspec spec/selenium ${DOCKER_COMMAND} run --rm web bundle exec rspec spec/selenium

View File

@ -29,26 +29,22 @@ describe "account authentication" do
end end
describe "sso settings" do describe "sso settings" do
let(:login_handle_name) { f("#sso_settings_login_handle_name") }
let(:change_password_url) { f("#sso_settings_change_password_url") }
let(:auth_discovery_url) { f("#sso_settings_auth_discovery_url") }
it "saves", priority: "1" do it "saves", priority: "1" do
add_sso_config add_sso_config
expect(login_handle_name).to have_value "login" expect(f("#sso_settings_login_handle_name")).to have_value "login"
expect(change_password_url).to have_value "http://test.example.com" expect(f("#sso_settings_change_password_url")).to have_value "http://test.example.com"
expect(auth_discovery_url).to have_value "http://test.example.com" expect(f("#sso_settings_auth_discovery_url")).to have_value "http://test.example.com"
end end
it "updates", priority: "1" do it "updates", priority: "1" do
add_sso_config add_sso_config
login_handle_name.clear f("#sso_settings_login_handle_name").clear
change_password_url.clear f("#sso_settings_change_password_url").clear
auth_discovery_url.clear f("#sso_settings_auth_discovery_url").clear
f("#edit_sso_settings button[type='submit']").click f("#edit_sso_settings button[type='submit']").click
expect(login_handle_name).not_to have_value "login" expect(f("#sso_settings_login_handle_name")).not_to have_value "login"
expect(change_password_url).not_to have_value "http://test.example.com" expect(f("#sso_settings_change_password_url")).not_to have_value "http://test.example.com"
expect(auth_discovery_url).not_to have_value "http://test.example.com" expect(f("#sso_settings_auth_discovery_url")).not_to have_value "http://test.example.com"
end end
end end

View File

@ -43,10 +43,6 @@ describe "new account user search" do
user_session(@user) user_session(@user)
end end
def wait_for_loading_to_disappear
expect(f('[data-automation="users list"]')).not_to contain_css("tr:nth-child(2)")
end
describe "with default page visit" do describe "with default page visit" do
before do before do
@user.update_attribute(:name, "Test User") @user.update_attribute(:name, "Test User")
@ -95,7 +91,9 @@ describe "new account user search" do
user_with_pseudonym(account: @account, name: "diffrient user") user_with_pseudonym(account: @account, name: "diffrient user")
refresh_page refresh_page
user_search_box.send_keys("Test") user_search_box.send_keys("Test")
wait_for_loading_to_disappear wait_for_ajaximations
wait_for(method: nil, timeout: 0.5) { fj("title:contains('Loading')").displayed? }
wait_for_no_such_element { fj("title:contains('Loading')") }
expect(results_rows.count).to eq 1 expect(results_rows.count).to eq 1
expect(results_rows.first).to include_text("Test") expect(results_rows.first).to include_text("Test")
end end

View File

@ -92,7 +92,7 @@ module AssignmentsIndexPage
end end
def bulk_edit_tr_rows def bulk_edit_tr_rows
ff("#bulkEditRoot tbody tr") ff("#bulkEditRoot [role='table'] [role='row']")
end end
def bulk_edit_loading_spinner def bulk_edit_loading_spinner

View File

@ -31,7 +31,7 @@ describe "browser" do
get("/login") get("/login")
driver.execute_script("window.console.log('#{sample_msg}')") driver.execute_script("window.console.log('#{sample_msg}')")
browser_logs = driver.manage.logs.get(:browser) browser_logs = driver.logs.get(:browser)
expect(browser_logs.map(&:message)).to include(a_string_matching(sample_msg)) expect(browser_logs.map(&:message)).to include(a_string_matching(sample_msg))
end end

View File

@ -192,7 +192,7 @@ shared_context "in-process server selenium tests" do
example.metadata[:page_html] = document.to_html example.metadata[:page_html] = document.to_html
end end
browser_logs = driver.manage.logs.get(:browser) rescue nil browser_logs = driver.logs.get(:browser) rescue nil
# log INSTUI deprecation warnings # log INSTUI deprecation warnings
if browser_logs.present? if browser_logs.present?

View File

@ -126,8 +126,8 @@ describe "native canvas conditional release" do
assignment = assignment_model(course: @course, points_possible: 100) assignment = assignment_model(course: @course, points_possible: 100)
get "/courses/#{@course.id}/assignments/#{assignment.id}/edit" get "/courses/#{@course.id}/assignments/#{assignment.id}/edit"
ConditionalReleaseObjects.conditional_release_link.click ConditionalReleaseObjects.conditional_release_link.click
replace_content(ConditionalReleaseObjects.division_cutoff1, "72") ConditionalReleaseObjects.replace_mastery_path_scores(ConditionalReleaseObjects.division_cutoff1, "70", "72")
replace_content(ConditionalReleaseObjects.division_cutoff2, "47") ConditionalReleaseObjects.replace_mastery_path_scores(ConditionalReleaseObjects.division_cutoff2, "40", "47")
ConditionalReleaseObjects.division_cutoff2.send_keys :tab ConditionalReleaseObjects.division_cutoff2.send_keys :tab
expect(ConditionalReleaseObjects.division_cutoff1.attribute("value")).to eq("72 pts") expect(ConditionalReleaseObjects.division_cutoff1.attribute("value")).to eq("72 pts")
@ -188,10 +188,10 @@ describe "native canvas conditional release" do
get "/courses/#{@course.id}/assignments/#{assignment.id}/edit" get "/courses/#{@course.id}/assignments/#{assignment.id}/edit"
ConditionalReleaseObjects.conditional_release_link.click ConditionalReleaseObjects.conditional_release_link.click
replace_content(ConditionalReleaseObjects.division_cutoff1, "") ConditionalReleaseObjects.replace_mastery_path_scores(ConditionalReleaseObjects.division_cutoff1, "70", "")
expect(ConditionalReleaseObjects.must_not_be_empty_exists?).to eq(true) expect(ConditionalReleaseObjects.must_not_be_empty_exists?).to eq(true)
replace_content(ConditionalReleaseObjects.division_cutoff1, "35") ConditionalReleaseObjects.replace_mastery_path_scores(ConditionalReleaseObjects.division_cutoff1, "", "35")
expect(ConditionalReleaseObjects.these_scores_are_out_of_order_exists?).to eq(true) expect(ConditionalReleaseObjects.these_scores_are_out_of_order_exists?).to eq(true)
end end

View File

@ -26,6 +26,11 @@ class ConditionalReleaseObjects
element_exists?("#conditional_content") element_exists?("#conditional_content")
end end
def replace_mastery_path_scores(element, current_value, new_value)
current_value.length.times { element.send_keys(:backspace) }
element.send_keys(new_value)
end
# Assignment Index Page # Assignment Index Page
def assignment_kebob(page_title) def assignment_kebob(page_title)

View File

@ -611,7 +611,8 @@ describe "content migrations", :non_parallel do
ff("[name=selective_import]")[0].click ff("[name=selective_import]")[0].click
submit submit
run_jobs run_jobs
expect(f(".migrationProgressItem .progressStatus")).to include_text("Completed") # Wait until the item is imported on the back-end, otherwise the selenium tools will fail the test due to runtime
keep_trying_until { ContentMigration.last.workflow_state == "imported" }
@course.reload @course.reload
expect(@course.announcements.last.locked).to be_truthy expect(@course.announcements.last.locked).to be_truthy
expect(@course.lock_all_announcements).to be_truthy expect(@course.lock_all_announcements).to be_truthy
@ -631,7 +632,8 @@ describe "content migrations", :non_parallel do
ff("[name=selective_import]")[0].click ff("[name=selective_import]")[0].click
submit submit
run_jobs run_jobs
expect(f(".migrationProgressItem .progressStatus")).to include_text("Completed") # Wait until the item is imported on the back-end, otherwise the selenium tools will fail the test due to runtime
keep_trying_until { ContentMigration.last.workflow_state == "imported" }
@course.reload @course.reload
expect(@course.discussion_topics.last.allow_rating).to be_truthy expect(@course.discussion_topics.last.allow_rating).to be_truthy
end end

View File

@ -30,6 +30,11 @@ describe "course copy" do
expect(header.text).to eq @course.course_code expect(header.text).to eq @course.course_code
end end
def wait_for_migration_to_complete
completed_status = fj("div.progressStatus:contains('Completed')")
keep_trying_until(5) { completed_status.displayed? == true }
end
it "copies the course" do it "copies the course" do
course_with_admin_logged_in course_with_admin_logged_in
@course.syllabus_body = "<p>haha</p>" @course.syllabus_body = "<p>haha</p>"
@ -37,11 +42,12 @@ describe "course copy" do
@course.default_view = "modules" @course.default_view = "modules"
@course.wiki_pages.create!(title: "hi", body: "Whatever") @course.wiki_pages.create!(title: "hi", body: "Whatever")
@course.save! @course.save!
get "/courses/#{@course.id}/copy" get "/courses/#{@course.id}/copy"
expect_new_page_load { f('button[type="submit"]').click } expect_new_page_load { f('button[type="submit"]').click }
expect(f("div.progressStatus").text.include?("Queued")).to eq(true)
run_jobs run_jobs
expect(f("div.progressStatus span")).to include_text "Completed" wait_for_ajaximations
wait_for_migration_to_complete
@new_course = Course.last @new_course = Course.last
expect(@new_course.syllabus_body).to eq @course.syllabus_body expect(@new_course.syllabus_body).to eq @course.syllabus_body
@ -123,12 +129,11 @@ describe "course copy" do
get "/courses/#{@course.id}/settings" get "/courses/#{@course.id}/settings"
link = f(".copy_course_link") link = f(".copy_course_link")
expect(link).to be_displayed expect(link).to be_displayed
expect_new_page_load { link.click } expect_new_page_load { link.click }
expect_new_page_load { f('button[type="submit"]').click } expect_new_page_load { f('button[type="submit"]').click }
run_jobs run_jobs
expect(f("div.progressStatus span")).to include_text "Completed" wait_for_ajaximations
wait_for_migration_to_complete
@new_course = subaccount.courses.where("id <>?", @course.id).last @new_course = subaccount.courses.where("id <>?", @course.id).last
expect(@new_course.syllabus_body).to eq @course.syllabus_body expect(@new_course.syllabus_body).to eq @course.syllabus_body

View File

@ -589,6 +589,7 @@ describe "context modules" do
expect(button.text).to eq("Expand All") expect(button.text).to eq("Expand All")
refresh_page refresh_page
assert_collapsed assert_collapsed
button = f("button#expand_collapse_all")
button.click button.click
wait_for_ajaximations wait_for_ajaximations
assert_expanded assert_expanded

View File

@ -49,7 +49,7 @@ describe "cross-listing" do
# crosslist a valid course # crosslist a valid course
course_id.click course_id.click
course_id.clear course_id.clear
course_id.send_keys([:control, "a"], @course2.id.to_s, "\n") course_id.send_keys(@course2.id.to_s, "\n")
expect(course_name).to include_text(@course2.name) expect(course_name).to include_text(@course2.name)
expect(form.find_element(:id, "course_autocomplete_id")).to have_attribute(:value, @course.id.to_s) expect(form.find_element(:id, "course_autocomplete_id")).to have_attribute(:value, @course.id.to_s)
expect(submit_btn).not_to have_class("disabled") expect(submit_btn).not_to have_class("disabled")
@ -107,7 +107,7 @@ describe "cross-listing" do
# k, let's crosslist to the other course # k, let's crosslist to the other course
form.find_element(:css, "#course_id").click form.find_element(:css, "#course_id").click
form.find_element(:css, "#course_id").clear form.find_element(:css, "#course_id").clear
form.find_element(:css, "#course_id").send_keys([:control, "a"], other_course.id.to_s, "\n") form.find_element(:css, "#course_id").send_keys(other_course.id.to_s, "\n")
expect(f("#course_autocomplete_name")).to include_text other_course.name expect(f("#course_autocomplete_name")).to include_text other_course.name
expect(form.find_element(:css, "#course_autocomplete_id")).to have_attribute(:value, other_course.id.to_s) expect(form.find_element(:css, "#course_autocomplete_id")).to have_attribute(:value, other_course.id.to_s)

View File

@ -104,8 +104,8 @@ describe "discussions" do
wait_for_ajaximations wait_for_ajaximations
fj(".ic-tokeninput-option:visible:first").click fj(".ic-tokeninput-option:visible:first").click
wait_for_ajaximations wait_for_ajaximations
fj(".datePickerDateField[data-date-type='due_at']:first").send_keys(format_date_for_view(due_at1)) fj(".datePickerDateField[data-date-type='due_at']:first").send_keys(format_date_for_view(due_at1), :tab)
wait_for_ajaximations
f("#add_due_date").click f("#add_due_date").click
wait_for_ajaximations wait_for_ajaximations

View File

@ -68,7 +68,7 @@ describe "Gradebook" do
Gradebook::Cells.open_tray(@student_1, group_assignment) Gradebook::Cells.open_tray(@student_1, group_assignment)
Gradebook::GradeDetailTray.add_new_comment(@comment_text) Gradebook::GradeDetailTray.add_new_comment(@comment_text)
Gradebook::GradeDetailTray.close_tray_button.click Gradebook::GradeDetailTray.click_close_tray_button
# make sure it's on the other student's submission # make sure it's on the other student's submission
Gradebook::Cells.open_tray(@student_2, group_assignment) Gradebook::Cells.open_tray(@student_2, group_assignment)

View File

@ -412,6 +412,8 @@ describe "Gradebook" do
grading_cell.click grading_cell.click
Gradebook::Cells.edit_grade(student, essay_quiz.assignment, 10) Gradebook::Cells.edit_grade(student, essay_quiz.assignment, 10)
# Re-select element in case it's gone stale
grading_cell = Gradebook::Cells.grading_cell(student, essay_quiz.assignment)
expect(grading_cell).not_to contain_css(".icon-not-graded") expect(grading_cell).not_to contain_css(".icon-not-graded")
end end
end end

View File

@ -31,8 +31,8 @@ module Gradebook
f("#SubmissionTray__Content") f("#SubmissionTray__Content")
end end
def self.close_tray_button def self.click_close_tray_button
fj("button:contains('Close submission tray')") force_click("button:contains('Close submission tray')")
end end
def self.avatar def self.avatar

View File

@ -129,6 +129,7 @@ module AssignmentOverridesSeleniumHelper
last_due_at_element.send_keys(opts.fetch(:due_at, Time.zone.now.advance(days: 5))) last_due_at_element.send_keys(opts.fetch(:due_at, Time.zone.now.advance(days: 5)))
last_unlock_at_element.send_keys(opts.fetch(:unlock_at, Time.zone.now.advance(days: -1))) last_unlock_at_element.send_keys(opts.fetch(:unlock_at, Time.zone.now.advance(days: -1)))
last_lock_at_element.send_keys(opts.fetch(:lock_at, Time.zone.now.advance(days: 5))) last_lock_at_element.send_keys(opts.fetch(:lock_at, Time.zone.now.advance(days: 5)))
last_lock_at_element.send_keys(:tab)
end end
def find_vdd_time(override_context) def find_vdd_time(override_context)

View File

@ -221,21 +221,8 @@ describe "RCE next tests", ignore_js_errors: true do
body: "<p id='para'><a id='lnk' href='http://example.com'>delete me</a></p>" body: "<p id='para'><a id='lnk' href='http://example.com'>delete me</a></p>"
) )
visit_existing_wiki_edit(@course, "title") visit_existing_wiki_edit(@course, "title")
f("##{rce_page_body_ifr_id}").click f("##{rce_page_body_ifr_id}").click
f("##{rce_page_body_ifr_id}").send_keys( f("##{rce_page_body_ifr_id}").send_keys([:control, "a"], :backspace)
%i[shift arrow_left],
%i[shift arrow_left],
%i[shift arrow_left],
%i[shift arrow_left],
%i[shift arrow_left],
%i[shift arrow_left],
%i[shift arrow_left],
%i[shift arrow_left],
%i[shift arrow_left],
%i[shift arrow_left]
)
f("##{rce_page_body_ifr_id}").send_keys(:enter)
in_frame rce_page_body_ifr_id do in_frame rce_page_body_ifr_id do
expect(f("#para").text).to eql "" expect(f("#para").text).to eql ""
@ -1175,7 +1162,7 @@ describe "RCE next tests", ignore_js_errors: true do
it "opens keyboard shortcut modal with alt-f8" do it "opens keyboard shortcut modal with alt-f8" do
visit_front_page_edit(@course) visit_front_page_edit(@course)
rce = f(".tox-edit-area__iframe") rce = f(".tox-edit-area__iframe")
rce.send_keys %i[alt f8] rce.send_keys(:alt, :f8)
expect(keyboard_shortcut_modal).to be_displayed expect(keyboard_shortcut_modal).to be_displayed
end end
@ -1184,7 +1171,7 @@ describe "RCE next tests", ignore_js_errors: true do
visit_front_page_edit(@course) visit_front_page_edit(@course)
rce = f(".tox-edit-area__iframe") rce = f(".tox-edit-area__iframe")
expect(f(".tox-menubar")).to be_displayed # always show menubar now expect(f(".tox-menubar")).to be_displayed # always show menubar now
rce.send_keys %i[alt f9] rce.send_keys(:alt, :f9)
expect(f(".tox-menubar")).to be_displayed expect(f(".tox-menubar")).to be_displayed
expect(fj('.tox-menubar button:contains("Edit")')).to eq(driver.switch_to.active_element) expect(fj('.tox-menubar button:contains("Edit")')).to eq(driver.switch_to.active_element)
@ -1193,7 +1180,7 @@ describe "RCE next tests", ignore_js_errors: true do
it "focuses the toolbar with alt-f10" do it "focuses the toolbar with alt-f10" do
visit_front_page_edit(@course) visit_front_page_edit(@course)
rce = f(".tox-edit-area__iframe") rce = f(".tox-edit-area__iframe")
rce.send_keys %i[alt f10] rce.send_keys(:alt, :f10)
expect(fj('.tox-toolbar__primary button:contains("12pt")')).to eq( expect(fj('.tox-toolbar__primary button:contains("12pt")')).to eq(
driver.switch_to.active_element driver.switch_to.active_element

View File

@ -339,8 +339,7 @@ module CustomSeleniumActions
tinymce_element = f("body") tinymce_element = f("body")
until tinymce_element.text.empty? until tinymce_element.text.empty?
tinymce_element.click tinymce_element.click
tinymce_element.send_keys(Array.new(100, :backspace)) tinymce_element.send_keys([:control, "a"], :backspace)
tinymce_element = f("body")
end end
end end
else else
@ -515,14 +514,6 @@ module CustomSeleniumActions
keys = value.to_s.empty? ? [:backspace] : [] keys = value.to_s.empty? ? [:backspace] : []
keys << value keys << value
el.send_keys(*keys) el.send_keys(*keys)
count = 0
until el["value"] == value.to_s
break if count > 1
count += 1
driver.execute_script("arguments[0].select();", el)
el.send_keys(*keys)
end
end end
el.send_keys(:tab) if options[:tab_out] el.send_keys(:tab) if options[:tab_out]

View File

@ -22,20 +22,15 @@ require "selenium-webdriver"
module Selenium module Selenium
module WebDriver module WebDriver
module Remote module Remote
module W3C class Bridge
class Bridge def log(type)
COMMANDS = remove_const(:COMMANDS).dup command(:get_log, "session/:session_id/log", :post)
COMMANDS[:get_log] = [:post, "session/:session_id/log"] data = execute :get_log, {}, { type: type.to_s }
COMMANDS.freeze
def log(type) Array(data).map do |l|
data = execute :get_log, {}, { type: type.to_s } LogEntry.new l.fetch("level", "UNKNOWN"), l.fetch("timestamp"), l.fetch("message")
rescue KeyError
Array(data).map do |l| next
LogEntry.new l.fetch("level", "UNKNOWN"), l.fetch("timestamp"), l.fetch("message")
rescue KeyError
next
end
end end
end end
end end

View File

@ -23,20 +23,6 @@ require_relative "common_helper_methods/custom_alert_actions"
require_relative "common_helper_methods/custom_screen_actions" require_relative "common_helper_methods/custom_screen_actions"
require_relative "patches/selenium/webdriver/remote/w3c/bridge" require_relative "patches/selenium/webdriver/remote/w3c/bridge"
# WebDriver uses port 7054 (the "locking port") as a mutex to ensure
# that we don't launch two Firefox instances at the same time. Each
# new instance you create will wait for the mutex before starting
# the browser, then release it as soon as the browser is open.
#
# The default port mutex wait timeout is 45 seconds.
# Bump it to 90 seconds as a stopgap for the recent flood of:
# `unable to bind to locking port 7054 within 45 seconds`
#
# TODO: Investigate why it's taking so long to launch Firefox, or
# what process is hogging port 7054.
Selenium::WebDriver::Firefox::Launcher.send :remove_const, :SOCKET_LOCK_TIMEOUT
Selenium::WebDriver::Firefox::Launcher::SOCKET_LOCK_TIMEOUT = 90
module SeleniumDriverSetup module SeleniumDriverSetup
CONFIG = ConfigFile.load("selenium") || {}.freeze CONFIG = ConfigFile.load("selenium") || {}.freeze
SECONDS_UNTIL_GIVING_UP = 10 SECONDS_UNTIL_GIVING_UP = 10
@ -275,9 +261,8 @@ module SeleniumDriverSetup
# by modifying 'chromedriver_version: <version>' for the version you want. # by modifying 'chromedriver_version: <version>' for the version you want.
# otherwise this will use the default version matching what is used in docker. # otherwise this will use the default version matching what is used in docker.
Webdrivers::Chromedriver.required_version = CONFIG[:chromedriver_version] Webdrivers::Chromedriver.required_version = CONFIG[:chromedriver_version]
chrome_options = Selenium::WebDriver::Chrome::Options.new
Selenium::WebDriver.for :chrome, desired_capabilities: desired_capabilities, options: chrome_options Selenium::WebDriver.for :chrome, capabilities: desired_capabilities
end end
def ruby_safari_driver def ruby_safari_driver
@ -287,16 +272,16 @@ module SeleniumDriverSetup
def ruby_edge_driver def ruby_edge_driver
puts "Thread: provisioning local edge driver" puts "Thread: provisioning local edge driver"
edge_options = Selenium::WebDriver::Edge::Options.new Selenium::WebDriver.for :edge, capabilities: desired_capabilities
Selenium::WebDriver.for :edge, desired_capabilities: desired_capabilities, options: edge_options
end end
def selenium_remote_driver def selenium_remote_driver
puts "Thread: provisioning remote #{browser} driver" puts "Thread: provisioning remote #{browser} driver"
puts "Selenium_Url: #{selenium_url}"
driver = Selenium::WebDriver.for( driver = Selenium::WebDriver.for(
:remote, :remote,
url: selenium_url, url: selenium_url,
desired_capabilities: desired_capabilities capabilities: desired_capabilities
) )
driver.file_detector = lambda do |args| driver.file_detector = lambda do |args|
@ -311,37 +296,36 @@ module SeleniumDriverSetup
def desired_capabilities def desired_capabilities
case browser case browser
when :firefox when :firefox
caps = Selenium::WebDriver::Remote::Capabilities.firefox options = Selenium::WebDriver::Options.firefox
options.log_level = :debug
when :chrome when :chrome
caps = Selenium::WebDriver::Remote::Capabilities.chrome options = Selenium::WebDriver::Options.chrome
caps["goog:chromeOptions"] = { options.add_argument("no-sandbox")
args: %w[disable-dev-shm-usage no-sandbox start-maximized] options.add_argument("start-maximized")
} options.add_argument("disable-dev-shm-usage")
caps["goog:loggingPrefs"] = { options.logging_prefs = {
browser: "ALL" browser: "ALL"
} }
# put `auto_open_devtools: true` in your selenium.yml if you want to have # put `auto_open_devtools: true` in your selenium.yml if you want to have
# the chrome dev tools open by default by selenium # the chrome dev tools open by default by selenium
if CONFIG[:auto_open_devtools] if CONFIG[:auto_open_devtools]
caps["goog:chromeOptions"][:args].append("auto-open-devtools-for-tabs") options.add_argument("auto-open-devtools-for-tabs")
end end
# put `headless: true` and `window_size: "<x>,<y>"` in your selenium.yml # put `headless: true` and `window_size: "<x>,<y>"` in your selenium.yml
# if you want to run against headless chrome # if you want to run against headless chrome
if CONFIG[:headless] if CONFIG[:headless]
caps["goog:chromeOptions"][:args].append("headless") options.add_argument("headless")
end end
if CONFIG[:window_size].present?
caps["goog:chromeOptions"][:args].append("window-size=#{CONFIG[:window_size]}")
end
caps["unexpectedAlertBehaviour"] = "ignore"
when :edge when :edge
caps = Selenium::WebDriver::Remote::Capabilities.edge options = Selenium::WebDriver::Options.edge
options.add_argument("disable-dev-shm-usage")
when :safari when :safari
# TODO: options for safari driver # TODO: options for safari driver
else else
raise "unsupported browser #{browser}" raise "unsupported browser #{browser}"
end end
caps options.unhandled_prompt_behavior = "ignore"
options
end end
def selenium_url def selenium_url
@ -362,24 +346,7 @@ module SeleniumDriverSetup
def ruby_firefox_driver def ruby_firefox_driver
puts "Thread: provisioning local firefox driver" puts "Thread: provisioning local firefox driver"
Selenium::WebDriver.for(:firefox, Selenium::WebDriver.for(:firefox,
profile: firefox_profile, capabilities: desired_capabilities)
desired_capabilities: desired_capabilities)
end
def firefox_profile
if CONFIG[:firefox_path].present?
Selenium::WebDriver::Firefox::Binary.path = (CONFIG[:firefox_path]).to_s
end
profile = Selenium::WebDriver::Firefox::Profile.new
profile.add_extension Rails.root.join("spec/selenium/test_setup/JSErrorCollector.xpi")
profile.log_file = "/dev/stdout"
# firefox randomly reloads if/when it decides to download the OpenH264 codec, so don't let it
profile["media.gmp-manager.url"] = ""
if CONFIG[:firefox_profile].present?
profile = Selenium::WebDriver::Firefox::Profile.from_name(CONFIG[:firefox_profile])
end
profile
end end
def_delegator :driver_capabilities, :browser_name def_delegator :driver_capabilities, :browser_name
@ -517,18 +484,6 @@ module SeleniumDriverSetup
end end
end end
# get some extra verbose logging from firefox for when things go wrong
Selenium::WebDriver::Firefox::Binary.class_eval do
def execute(*extra_args)
args = [self.class.path, "-no-remote"] + extra_args
SeleniumDriverSetup.browser_process = @process = ChildProcess.build(*args)
SeleniumDriverSetup.browser_log = @process.io.stdout = @process.io.stderr = Tempfile.new("firefox")
$DEBUG = true
@process.start
$DEBUG = nil
end
end
# make Wait play nicely with Timecop # make Wait play nicely with Timecop
module Selenium::WebDriver::Wait::Time module Selenium::WebDriver::Wait::Time
def self.now def self.now

View File

@ -256,4 +256,4 @@ Selenium::WebDriver::Element.prepend(SeleniumExtensions::FinderWaiting)
Selenium::WebDriver::Element.prepend(SeleniumExtensions::UnexpectedPageReloadProtectionElement) Selenium::WebDriver::Element.prepend(SeleniumExtensions::UnexpectedPageReloadProtectionElement)
Selenium::WebDriver::Driver.prepend(SeleniumExtensions::PreventEarlyInteraction) Selenium::WebDriver::Driver.prepend(SeleniumExtensions::PreventEarlyInteraction)
Selenium::WebDriver::Driver.prepend(SeleniumExtensions::FinderWaiting) Selenium::WebDriver::Driver.prepend(SeleniumExtensions::FinderWaiting)
Selenium::WebDriver::W3CActionBuilder.prepend(SeleniumExtensions::UnexpectedPageReloadProtectionActionBuilder) Selenium::WebDriver::ActionBuilder.prepend(SeleniumExtensions::UnexpectedPageReloadProtectionActionBuilder)