diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/MultimediaViewer/tests |
first commit
Diffstat (limited to 'www/wiki/extensions/MultimediaViewer/tests')
67 files changed, 9114 insertions, 0 deletions
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/LocalSettings.php b/www/wiki/extensions/MultimediaViewer/tests/browser/LocalSettings.php new file mode 100644 index 00000000..acd153c6 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/LocalSettings.php @@ -0,0 +1,2 @@ +<?php + $wgUseInstantCommons = true; diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/README.md b/www/wiki/extensions/MultimediaViewer/tests/browser/README.md new file mode 100644 index 00000000..36319498 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/README.md @@ -0,0 +1 @@ +Please see https://github.com/wikimedia/mediawiki-selenium for instructions on how to run tests. diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/ci.yml b/www/wiki/extensions/MultimediaViewer/tests/browser/ci.yml new file mode 100644 index 00000000..b95ed6f9 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/ci.yml @@ -0,0 +1,28 @@ +BROWSER: + - chrome + - firefox + - safari + +MEDIAWIKI_ENVIRONMENT: + - beta + - mediawiki + +PLATFORM: + - Linux + - OS X 10.9 + +exclude: + - BROWSER: chrome + MEDIAWIKI_ENVIRONMENT: mediawiki + + - BROWSER: chrome + PLATFORM: Linux + + - BROWSER: firefox + PLATFORM: OS X 10.9 + + - BROWSER: safari + MEDIAWIKI_ENVIRONMENT: mediawiki + + - BROWSER: safari + PLATFORM: Linux diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/environments.yml b/www/wiki/extensions/MultimediaViewer/tests/browser/environments.yml new file mode 100644 index 00000000..caa5168e --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/environments.yml @@ -0,0 +1,42 @@ +# Customize this configuration as necessary to provide defaults for various +# test environments. +# +# The set of defaults to use is determined by the MEDIAWIKI_ENVIRONMENT +# environment variable. +# +# export MEDIAWIKI_ENVIRONMENT=mw-vagrant-host +# bundle exec cucumber +# +# Additional variables set by the environment will override the corresponding +# defaults defined here. +# +# export MEDIAWIKI_ENVIRONMENT=mw-vagrant-host +# export MEDIAWIKI_USER=Selenium_user2 +# bundle exec cucumber +# +mw-vagrant-host: &default + browser_useragent: test-user-agent + user_factory: true + mediawiki_url: http://127.0.0.1:8080/wiki/ + +mw-vagrant-guest: + user_factory: true + mediawiki_url: http://127.0.0.1/wiki/ + +beta: + browser_useragent: test-user-agent + mediawiki_url: https://en.wikipedia.beta.wmflabs.org/wiki/ + mediawiki_user: Selenium_user + # mediawiki_password: SET THIS IN THE ENVIRONMENT! + +mediawiki: + browser_useragent: test-user-agent + mediawiki_url: https://www.mediawiki.org/wiki/ + mediawiki_user: Selenium_user + # mediawiki_password: SET THIS IN THE ENVIRONMENT! + +integration: + user_factory: true + # mediawiki_url: THIS WILL BE SET BY JENKINS + +default: *default diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.download.feature b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.download.feature new file mode 100644 index 00000000..fac69f34 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.download.feature @@ -0,0 +1,59 @@ +@chrome @en.wikipedia.beta.wmflabs.org @firefox @integration @safari @test2.wikipedia.org +Feature: Download menu + + Background: + Given I am viewing an image using MMV + + Scenario: Download menu can be opened + When I click the download icon + Then the download menu should appear + + Scenario: Clicking the image closes the download menu + When I click the download icon + And the download menu appears + And I click the image + Then the download menu should disappear + + Scenario: Image size defaults to original + When I click the download icon + Then the original beginning download image size label should be "4000 × 3000 px jpg" + And the download links should be the original image + + Scenario: Attribution area is collapsed by default + When I click the download icon + Then the attribution area should be collapsed + + Scenario: Attribution area can be opened + When I click the download icon + And I click on the attribution area + Then the attribution area should be open + + Scenario: Attribution area can be closed + When I click the download icon + And I click on the attribution area + And I click on the attribution area close icon + Then the attribution area should be collapsed + + Scenario: The small download option has the correct information + When I open the download dropdown + And the download size options appear + And I click the small download size + And the download size options disappears + Then the download image size label should be "193 × 145 px jpg" + And the download links should be the 193 thumbnail + + Scenario: The medium download option has the correct information + When I open the download dropdown + And the download size options appear + And I click the medium download size + And the download size options disappears + Then the download image size label should be "640 × 480 px jpg" + And the download links should be the 640 thumbnail + + Scenario: The large download option has the correct information + When I open the download dropdown + And the download size options appear + And I click the large download size + And the download size options disappears + Then the download image size label should be "1200 × 900 px jpg" + And the download links should be the 1200 thumbnail diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.navigation.feature b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.navigation.feature new file mode 100644 index 00000000..39fae447 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.navigation.feature @@ -0,0 +1,23 @@ +@chrome @en.wikipedia.beta.wmflabs.org @firefox @integration @test2.wikipedia.org +Feature: Navigation + + Background: + Given I am viewing an image using MMV + + Scenario: Clicking the next arrow takes me to the next image + When I click the next arrow + Then the image and metadata of the next image should appear + + Scenario: Clicking the previous arrow takes me to the previous image + When I click the previous arrow + Then the image and metadata of the previous image should appear + + Scenario: Closing MMV restores the scroll position + When I close MMV + Then I should be navigated back to the original wiki article + And the wiki article should be scrolled to the same position as before opening MMV + + Scenario: Browsing back to close MMV restores the scroll position + When I press the browser back button + Then I should be navigated back to the original wiki article + And the wiki article should be scrolled to the same position as before opening MMV diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.options.feature b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.options.feature new file mode 100644 index 00000000..85c6826d --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.options.feature @@ -0,0 +1,44 @@ +@chrome @en.wikipedia.beta.wmflabs.org @firefox @integration @test2.wikipedia.org +Feature: Options + + Background: + Given I am viewing an image using MMV + + Scenario: Clicking the X icon on the enable confirmation closes the options menu + Given I reenable MMV + When I click the enable X icon + Then the options menu should disappear + + Scenario: Clicking the X icon on the disable confirmation closes the options menu + Given I disable MMV + When I click the disable X icon + Then the options menu should disappear + + Scenario: Clicking the image closes the options menu + Given I click the options icon + When I click the image + Then the options menu should disappear + + Scenario: Clicking cancel closes the options menu + Given I click the options icon + When I click the disable cancel button + Then the options menu should disappear + + Scenario: Clicking the options icon brings up the options menu + When I click the options icon + Then the options menu should appear with the prompt to disable + + Scenario: Clicking enable shows the confirmation + Given I click the options icon with MMV disabled + When I click the enable button + Then the enable confirmation should appear + + Scenario: Clicking disable shows the confirmation + Given I click the options icon + When I click the disable button + Then the disable confirmation should appear + + Scenario: Disabling media viewer makes the next thumbnail click go to the file page + Given I disable and close MMV + When I click on the first image in the article + Then I am taken to the file page diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.performance.feature b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.performance.feature new file mode 100644 index 00000000..37e0c658 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.performance.feature @@ -0,0 +1,42 @@ +@en.wikipedia.beta.wmflabs.org @firefox @www.mediawiki.org @test2.wikipedia.org +Feature: Multimedia Viewer performance + + Background: + Given I am using a custom user agent + And I am at a wiki article with at least two embedded pictures + + Scenario: Commons with warm cache + Given I visit an unrelated Commons page to warm up the browser cache + And I visit the Commons page + Then the File: page image is loaded + + Scenario: MMV with warm cache and small browser window + Given I have a small browser window + When I click on an unrelated image in the article to warm up the browser cache + And I close MMV + And I click on the first image in the article + Then the MMV image is loaded in 125 percent of the time with a warm cache and an average browser window + + Scenario: MMV with cold cache and average browser window + Given I have an average browser window + When I click on the first image in the article + Then the MMV image is loaded in 210 percent of the time with a cold cache and an average browser window + + Scenario: MMV with warm cache and average browser window + Given I have an average browser window + When I click on an unrelated image in the article to warm up the browser cache + And I close MMV + And I click on the first image in the article + Then the MMV image is loaded in 125 percent of the time with a warm cache and an average browser window + + Scenario: MMV with cold cache and large browser window + Given I have a large browser window + When I click on the first image in the article + Then the MMV image is loaded in 240 percent of the time with a cold cache and a large browser window + + Scenario: MMV with warm cache and large browser window + Given I have a large browser window + When I click on an unrelated image in the article to warm up the browser cache + And I close MMV + And I click on the first image in the article + Then the MMV image is loaded in 125 percent of the time with a warm cache and a large browser window diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_download_steps.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_download_steps.rb new file mode 100644 index 00000000..1d94a804 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_download_steps.rb @@ -0,0 +1,101 @@ +# encoding: utf-8 + +When /^I open the download dropdown$/ do + step 'I click the download icon' + step 'I click the download down arrow icon' +end + +When /^I click the download icon$/ do + on(E2ETestPage).mmv_download_icon_element.when_present.click +end + +When /^I click the download down arrow icon$/ do + sleep 1 + on(E2ETestPage).mmv_download_down_arrow_icon_element.when_present(10).click +end + +When /^I click on the attribution area$/ do + on(E2ETestPage).mmv_download_attribution_area_element.when_present(10).click +end + +When /^I click on the attribution area close icon$/ do + on(E2ETestPage).mmv_download_attribution_area_close_icon_element.click +end + +When /^I click the (.*) download size$/ do |size_option| + on(E2ETestPage) do |page| + case size_option + when 'small' + @index = 1 + when 'medium' + @index = 2 + when 'large' + @index = 3 + else + @index = 0 + end + + page.mmv_download_size_options_elements[@index].click + end +end + +When /^the download size options appear$/ do + on(E2ETestPage).mmv_download_size_menu_element.when_present +end + +When /^the download size options disappears$/ do + on(E2ETestPage).mmv_download_size_menu_element.when_not_present +end + +When /^the download menu appears$/ do + on(E2ETestPage).mmv_download_menu_element.when_present(10) +end + +Then /^the download menu should appear$/ do + expect(on(E2ETestPage).mmv_download_menu_element.when_present(10)).to be_visible +end + +Then /^the download menu should disappear$/ do + expect(on(E2ETestPage).mmv_download_menu_element).not_to be_visible +end + +Then /^the original beginning download image size label should be "(.*)"$/ do |size_in_pixels| + expect(on(E2ETestPage).mmv_download_size_label_element.when_present(10).text).to eq size_in_pixels +end + +Then /^the download image size label should be "(.*)"$/ do |size_in_pixels| + on(E2ETestPage) do |page| + page.mmv_download_size_options_elements[0].when_not_present + expect(page.mmv_download_size_label_element.when_present.text).to eq size_in_pixels + end +end + +Then /^the download size options should appear$/ do + expect(on(E2ETestPage).mmv_download_size_menu_element.when_present).to be_visible +end + +Then /^the download links should be the original image$/ do + on(E2ETestPage) do |page| + expect(page.mmv_download_link_element.attribute('href')).to match /^?download$/ + expect(page.mmv_download_preview_link_element.attribute('href')).not_to match /^?download$/ + expect(page.mmv_download_link_element.attribute('href')).not_to match %r{/thumb/} + expect(page.mmv_download_preview_link_element.attribute('href')).not_to match %r{/thumb/} + end +end + +Then /^the download links should be the (\d+) thumbnail$/ do |thumb_size| + on(E2ETestPage) do |page| + page.wait_until { page.mmv_download_link_element.attribute('href').match thumb_size } + expect(page.mmv_download_link_element.attribute('href')).to match /^?download$/ + expect(page.mmv_download_preview_link_element.attribute('href')).not_to match /^?download$/ + expect(page.mmv_download_preview_link_element.attribute('href')).to match thumb_size + end +end + +Then /^the attribution area should be collapsed$/ do + expect(on(E2ETestPage).mmv_download_attribution_area_element.when_present(10).attribute('class')).to match 'mw-mmv-download-attribution-collapsed' +end + +Then /^the attribution area should be open$/ do + expect(on(E2ETestPage).mmv_download_attribution_area_element.when_present.attribute('class')).not_to match 'mw-mmv-download-attribution-collapsed' +end diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_navigation_steps.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_navigation_steps.rb new file mode 100644 index 00000000..bd73d899 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_navigation_steps.rb @@ -0,0 +1,44 @@ +# encoding: utf-8 + +When /^I click the next arrow$/ do + on(E2ETestPage).mmv_next_button_element.when_present.click +end + +When /^I click the previous arrow$/ do + on(E2ETestPage).mmv_previous_button_element.when_present.click +end + +When /^I press the browser back button$/ do + # $browser.back doesn't work for Safari. This is a workaround for https://code.google.com/p/selenium/issues/detail?id=3771 + on(E2ETestPage).execute_script('window.history.back();') +end + +Then /^the image and metadata of the next image should appear$/ do + on(E2ETestPage) do |page| + # MMV was launched, article is not visible yet + expect(page.image1_in_article_element).not_to be_visible + check_elements_in_viewer_for_image3 page + end +end + +Then /^the image and metadata of the previous image should appear$/ do + on(E2ETestPage) do |page| + # MMV was launched, article is not visible yet + expect(page.image1_in_article_element).not_to be_visible + check_elements_in_viewer_for_image1 page + end +end + +Then /^the wiki article should be scrolled to the same position as before opening MMV$/ do + on(E2ETestPage) do |page| + scroll_difference = page.execute_script('return $(window).scrollTop();') - @article_scroll_top + expect(scroll_difference.abs).to be < 2 + end +end + +Then /^I should be navigated back to the original wiki article$/ do + on(E2ETestPage) do |page| + expect(page.image1_in_article_element).to be_visible + expect(page.mmv_wrapper_element).not_to be_visible + end +end diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_options_steps.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_options_steps.rb new file mode 100644 index 00000000..db5c380c --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_options_steps.rb @@ -0,0 +1,69 @@ +# encoding: utf-8 + +When /^I click the options icon$/ do + on(E2ETestPage).mmv_options_icon_element.click +end + +Then /^the options menu should appear with the prompt to disable$/ do + on(E2ETestPage).mmv_options_menu_disable_element.should be_visible +end + +Then /^the options menu should disappear$/ do + on(E2ETestPage).mmv_options_menu_disable_element.should_not be_visible +end + +When /^I click the enable button$/ do + on(E2ETestPage).mmv_options_enable_button_element.click +end + +When /^I click the disable button$/ do + on(E2ETestPage).mmv_options_disable_button_element.click +end + +When /^I click the disable cancel button$/ do + on(E2ETestPage).mmv_options_disable_cancel_button_element.click +end + +When /^I click the enable X icon$/ do + on(E2ETestPage).mmv_options_enable_x_icon_element.click +end + +When /^I click the disable X icon$/ do + on(E2ETestPage).mmv_options_disable_x_icon_element.click +end + +When /^I disable MMV$/ do + step 'I click the options icon' + step 'I click the disable button' +end + +When /^I reenable MMV$/ do + step 'I disable MMV' + step 'I click the options icon' + step 'I click the enable button' +end + +When /^I click the options icon with MMV disabled$/ do + step 'I disable MMV' + step 'I click the options icon' +end + +When /^I disable and close MMV$/ do + step 'I disable MMV' + step 'I close MMV' +end + +Then /^the disable confirmation should appear$/ do + on(E2ETestPage).mmv_options_disable_confirmation_element.should be_visible +end + +Then /^the enable confirmation should appear$/ do + on(E2ETestPage).mmv_options_enable_confirmation_element.should be_visible +end + +Then /^I am taken to the file page$/ do + on(E2ETestPage) do |page| + page.current_url.should match %r{/File:} + page.current_url.should_not match %r{#/media} + end +end diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_performance_steps.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_performance_steps.rb new file mode 100644 index 00000000..c81ee4e1 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_performance_steps.rb @@ -0,0 +1,48 @@ +When /^I click on an unrelated image in the article to warm up the browser cache$/ do + on(E2ETestPage).other_image_in_article +end + +Given /^I visit the Commons page$/ do + @commons_open_time = Time.now.getutc + browser.goto 'https://commons.wikimedia.org/wiki/File:Sunrise_over_fishing_boats_in_Kerala.jpg' +end + +Given /^I visit an unrelated Commons page to warm up the browser cache$/ do + browser.goto 'https://commons.wikimedia.org/wiki/File:Wikimedia_Foundation_2013_All_Hands_Offsite_-_Day_2_-_Photo_16.jpg' +end + +Given /^I have a small browser window$/ do + browser.window.resize_to 900, 700 +end + +Given /^I have an average browser window$/ do + browser.window.resize_to 1366, 768 +end + +Given /^I have a large browser window$/ do + browser.window.resize_to 1920, 1080 +end + +Given /^I am using a custom user agent$/ do + browser_factory.override(browser_user_agent: env[:browser_useragent]) +end + +Then /^the File: page image is loaded$/ do + on(CommonsPage) do |page| + page.wait_for_image_load '.fullImageLink img' + # Has to be a global variable, otherwise it doesn't survive between scenarios + $commons_time = Time.now.getutc - @commons_open_time + page.log_performance type: 'file-page', duration: $commons_time * 1000 + end +end + +Then /^the MMV image is loaded in (\d+) percent of the time with a (.*) cache and an? (.*) browser window$/ do |percentage, cache, window_size| + on(E2ETestPage) do |page| + page.wait_for_image_load '.mw-mmv-image img' + mmv_time = Time.now.getutc - @image_click_time + page.log_performance type: 'mmv', duration: mmv_time * 1000, cache: cache, windowSize: window_size + + expected_time = $commons_time * (percentage.to_f / 100.0) + expect(mmv_time).to be < expected_time + end +end diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_steps.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_steps.rb new file mode 100644 index 00000000..3e4590bc --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_steps.rb @@ -0,0 +1,174 @@ +# encoding: utf-8 + +Given /^I am at a wiki article with at least two embedded pictures$/ do + api.create_page 'MediaViewerE2ETest', File.read(File.join(__dir__, '../../samples/MediaViewerE2ETest.wikitext')) + visit(E2ETestPage) + on(E2ETestPage).image1_in_article_element.when_present.should be_visible +end + +Given /^the MMV has loaded$/ do + on(E2ETestPage) do |page| + page.wait_until do + # Wait for JS to hijack standard link + # TODO: If this approach works well, we should implement general + # `wait_for_resource` and `resource_ready?` helper methods in + # mw-selenium, and document this pattern on mw.org + browser.execute_script("return mw.loader.getState('mmv.bootstrap') === 'ready'") + end + end +end + +Given /^I am viewing an image using MMV$/ do + step 'I am at a wiki article with at least two embedded pictures' + step 'the MMV has loaded' + step 'I click on the second image in the article' + step 'the image metadata and the image itself should be there' +end + +When /^I click on the first image in the article$/ do + on(E2ETestPage) do |page| + # We store the offset of the image as the scroll position and scroll to it, because cucumber/selenium + # sometimes automatically scrolls to it when we ask it to click on it (seems to depend on timing) + @article_scroll_top = page.execute_script("var scrollTop = Math.round($('a[href$=\"File:Sunrise_over_fishing_boats_in_Kerala.jpg\"]').first().find('img').offset().top); window.scrollTo(0, scrollTop); return scrollTop;") + # Scrolls to the image and clicks on it + page.image1_in_article + # This is a global variable that can be used to measure performance + @image_click_time = Time.now.getutc + end +end + +When /^I click on the second image in the article$/ do + on(E2ETestPage) do |page| + # We store the offset of the image as the scroll position and scroll to it, because cucumber/selenium + # sometimes automatically scrolls to it when we ask it to click on it (seems to depend on timing) + @article_scroll_top = page.execute_script("var scrollTop = Math.round($('a[href$=\"File:Wikimedia_Foundation_2013_All_Hands_Offsite_-_Day_2_-_Photo_24.jpg\"]').first().find('img').offset().top); window.scrollTo(0, scrollTop); return scrollTop;") + # Scrolls to the image and clicks on it + page.image2_in_article + # This is a global variable that can be used to measure performance + @image_click_time = Time.now.getutc + end +end + +When /^I close MMV$/ do + on(E2ETestPage).mmv_close_button_element.when_present(30).click +end + +When /^I click the image$/ do + on(E2ETestPage) do + # Clicking the top-left corner of the image is necessary for the test to work on IE + # A plain click on the image element ends up hitting the dialog, which means it won't close + begin + browser.driver.action.move_to(browser.driver.find_element(:class, 'mw-mmv-image'), 10, 10).click.perform + rescue + # Plain click for web drivers that don't support mouse moves (Safari, currently) + on(E2ETestPage).mmv_image_div_element.when_present.click + end + end +end + +Then /^the image metadata and the image itself should be there$/ do + on(E2ETestPage) do |page| + # MMV was launched, article is not visible now + page.image1_in_article_element.should_not be_visible + check_elements_in_viewer_for_image2 page + end +end + +# Helper function that verifies the presence of various elements in viewer +# while looking at image1 (Kerala) +def check_elements_in_viewer_for_image1(page) + # Check basic MMV elements are present + expect(page.mmv_overlay_element.when_present).to be_visible + expect(page.mmv_wrapper_element.when_present).to be_visible + expect(page.mmv_image_div_element).to be_visible + + # Check image content + expect(page.mmv_final_image_element.when_present.attribute('src')).to match /Kerala/ + + # Check basic metadata is present + + # Title + expect(page.mmv_metadata_title_element.when_present.text).to match /^Sunrise over fishing boats$/ + # License + expect(page.mmv_metadata_license_element.when_present.attribute('href')).to match %r{^https?://creativecommons.org/licenses/by-sa/3.0$} + expect(page.mmv_metadata_license_element.when_present.text).to match 'CC BY-SA 3.0' + # Credit + expect(page.mmv_metadata_credit_element.when_present).to be_visible + expect(page.mmv_metadata_source_element.when_present.text).to match 'Own work' + + # Image metadata + expect(page.mmv_image_metadata_wrapper_element.when_present).to be_visible + # Description + expect(page.mmv_image_metadata_desc_element.when_present.text).to match 'Sunrise over fishing boats on the beach south of Kovalam' + # Image metadata links + expect(page.mmv_image_metadata_links_wrapper_element.when_present).to be_visible + # Details link + expect(page.mmv_details_page_link_element.when_present.text).to match 'More details' + expect(page.mmv_details_page_link_element.when_present.attribute('href')).to match /boats_in_Kerala.jpg$/ +end + +# Helper function that verifies the presence of various elements in viewer +# while looking at image2 (Aquarium) +def check_elements_in_viewer_for_image2(page) + # Check basic MMV elements are present + expect(page.mmv_overlay_element.when_present).to be_visible + expect(page.mmv_wrapper_element.when_present).to be_visible + expect(page.mmv_image_div_element).to be_visible + + # Check image content + expect(page.mmv_final_image_element.when_present(30).attribute('src')).to match 'Offsite' + + # Check basic metadata is present + + # Title + expect(page.mmv_metadata_title_element.when_present.text).to match /^Tropical Fish Aquarium$/ + # License + expect(page.mmv_metadata_license_element.when_present(10).attribute('href')).to match %r{^https?://creativecommons.org/licenses/by-sa/3.0$} + expect(page.mmv_metadata_license_element.when_present.text).to match 'CC BY-SA 3.0' + # Credit + expect(page.mmv_metadata_credit_element.when_present).to be_visible + expect(page.mmv_metadata_source_element.when_present.text).to match 'Wikimedia Foundation' + + # Image metadata + expect(page.mmv_image_metadata_wrapper_element.when_present).to be_visible + # Description + expect(page.mmv_image_metadata_desc_element.when_present.text).to match 'Photo from Wikimedia Foundation' + # Image metadata links + expect(page.mmv_image_metadata_links_wrapper_element.when_present).to be_visible + # Details link + expect(page.mmv_details_page_link_element.when_present.text).to match 'More details' + expect(page.mmv_details_page_link_element.when_present.attribute('href')).to match /All_Hands_Offsite.*\.jpg$/ +end + +# Helper function that verifies the presence of various elements in viewer +# while looking at image3 (Hong Kong) +def check_elements_in_viewer_for_image3(page) + # Check basic MMV elements are present + expect(page.mmv_overlay_element.when_present).to be_visible + expect(page.mmv_wrapper_element.when_present).to be_visible + expect(page.mmv_image_div_element).to be_visible + + # Check image content + expect(page.mmv_image_div_element.image_element.attribute('src')).to match 'Hong_Kong' + + # Check basic metadata is present + + # Title + expect(page.mmv_metadata_title_element.when_present.text).to match /^Hong Kong Harbor at night$/ + # License + expect(page.mmv_metadata_license_element.when_present.attribute('href')).to match %r{^https?://creativecommons.org/licenses/by-sa/3.0$} + expect(page.mmv_metadata_license_element.when_present.text).to match 'CC BY-SA 3.0' + # Credit + expect(page.mmv_metadata_credit_element.when_present).to be_visible + expect(page.mmv_metadata_source_element.when_present.text).to match 'Wikimedia Foundation' + + # Image metadata + expect(page.mmv_image_metadata_wrapper_element.when_present).to be_visible + # Description + expect(page.mmv_image_metadata_desc_element.when_present.text).to match /Photos from our product team's talks at Wikimania 2013 in Hong Kong./ + # Image metadata links + expect(page.mmv_image_metadata_links_wrapper_element.when_present).to be_visible + # Details link + expect(page.mmv_details_page_link_element.when_present.text).to match 'More details' + expect(page.mmv_details_page_link_element.when_present.attribute('href')).to match /Wikimania_2013_-_Hong_Kong_-_Photo_090\.jpg$/ +end diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/env.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/env.rb new file mode 100644 index 00000000..c1072b26 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/env.rb @@ -0,0 +1,3 @@ +require 'mediawiki_selenium/cucumber' +require 'mediawiki_selenium/pages' +require 'mediawiki_selenium/step_definitions' diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/commons_page.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/commons_page.rb new file mode 100644 index 00000000..0923e354 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/commons_page.rb @@ -0,0 +1,51 @@ +require 'json' + +class CommonsPage + include PageObject + + page_url 'File:Sunrise_over_fishing_boats_in_Kerala.jpg' + + img(:commons_image, src: /Kerala\.jpg$/) + div(:mmv_image_loaded_cucumber, class: 'mw-mmv-image-loaded-cucumber') + + def wait_for_image_load(selector) + browser.execute_script <<-end_script + function wait_for_image() { + var $img = $( #{selector.to_json} ); + if ( $img.length + && $img.attr( 'src' ).match(/Kerala/) + && !$img.attr( 'src' ).match(/\\/220px-/) // Blurry placeholder + && $img.prop( 'complete' ) ) { + $( 'body' ).append( '<div class=\"mw-mmv-image-loaded-cucumber\"/>' ); + } else { + setTimeout( wait_for_image, 10 ); + } + } + wait_for_image(); + end_script + + wait_until { mmv_image_loaded_cucumber_element.exists? } + end + + def log_performance(stats) + stats = stats.reject { |_name, value| value.nil? || value.to_s.empty? } + stats[:duration] = stats[:duration].floor + + browser.execute_script <<-end_script + mediaWiki.eventLog.declareSchema( 'MultimediaViewerVersusPageFilePerformance', + { schema: + { title: 'MultimediaViewerVersusPageFilePerformance', + properties: { + type: { type: 'string', required: true, enum: [ 'mmv', 'file-page' ] }, + duration: { type: 'integer', required: true }, + cache: { type: 'string', required: false, enum: [ 'cold', 'warm' ] }, + windowSize: { type: 'string', required: false, enum: [ 'average', 'large'] } + } + }, + revision: 7907636 + }); + + mw.eventLog.logEvent( 'MultimediaViewerVersusPageFilePerformance', #{stats.to_json} ); + end_script + end +end diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/e2e_test_page.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/e2e_test_page.rb new file mode 100644 index 00000000..c0f04077 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/e2e_test_page.rb @@ -0,0 +1,87 @@ +class E2ETestPage < CommonsPage + include PageObject + + page_url 'MediaViewerE2ETest' + + # Tag page elements that we will need. + + # First image in lightbox demo page + a(:image1_in_article, class: 'image', href: /Kerala\.jpg$/) + a(:image2_in_article, class: 'image', href: /Wikimedia_Foundation_2013_All_Hands_Offsite_-_Day_2_-_Photo_24\.jpg$/) + + a(:other_image_in_article, href: /Academy_of_Sciences\.jpg$/) + + # Black overlay + div(:mmv_overlay, class: 'mw-mmv-overlay') + + # Wrapper div for all mmv elements + div(:mmv_wrapper, class: 'mw-mmv-wrapper') + + # Wrapper div for image + div(:mmv_image_div, class: 'mw-mmv-image') + + # Actual image + image(:mmv_final_image, class: 'mw-mmv-final-image') + + # Metadata elements + span(:mmv_metadata_title, class: 'mw-mmv-title') + a(:mmv_metadata_license, class: 'mw-mmv-license') + p(:mmv_metadata_credit, class: 'mw-mmv-credit') + span(:mmv_metadata_source, class: 'mw-mmv-source') + + div(:mmv_image_metadata_wrapper, class: 'mw-mmv-image-metadata') + p(:mmv_image_metadata_desc, class: 'mw-mmv-image-desc') + + ul(:mmv_image_metadata_links_wrapper, class: 'mw-mmv-image-links') + a(:mmv_details_page_link, class: 'mw-mmv-description-page-button') + + # Controls + button(:mmv_next_button, class: 'mw-mmv-next-image') + button(:mmv_previous_button, class: 'mw-mmv-prev-image') + button(:mmv_close_button, class: 'mw-mmv-close') + div(:mmv_image_loaded_cucumber, class: 'mw-mmv-image-loaded-cucumber') + + # Download + button(:mmv_download_icon, class: 'mw-mmv-download-button') + div(:mmv_download_menu, class: 'mw-mmv-download-dialog') + span(:mmv_download_size_label, class: 'mw-mmv-download-image-size') + span(:mmv_download_down_arrow_icon, class: 'mw-mmv-download-select-menu') + div(:mmv_download_size_menu_container, class: 'mw-mmv-download-size') + div(:mmv_download_size_menu) do |page| + page.mmv_download_size_menu_container_element.div_element(class: 'oo-ui-selectWidget') + end + divs(:mmv_download_size_options, class: 'oo-ui-menuOptionWidget') + a(:mmv_download_link, class: 'mw-mmv-download-go-button') + a(:mmv_download_preview_link, class: 'mw-mmv-download-preview-link') + div(:mmv_download_attribution_area, class: 'mw-mmv-download-attribution') + p(:mmv_download_attribution_area_close_icon, class: 'mw-mmv-download-attribution-close-button') + div(:mmv_download_attribution_area_input_container, class: 'mw-mmv-download-attr-input') + text_field(:mmv_download_attribution_area_input) do |page| + page.mmv_download_attribution_area_input_container_element.text_field_element + end + + # Options + button(:mmv_options_icon, class: 'mw-mmv-options-button') + div(:mmv_options_menu_disable, class: 'mw-mmv-options-disable') + div(:mmv_options_menu_enable, class: 'mw-mmv-options-enable') + button(:mmv_options_enable_button) do |page| + page.mmv_options_menu_enable_element.div_element(class: 'mw-mmv-options-submit').button_element(class: 'mw-mmv-options-submit-button') + end + button(:mmv_options_disable_button) do |page| + page.mmv_options_menu_disable_element.div_element(class: 'mw-mmv-options-submit').button_element(class: 'mw-mmv-options-submit-button') + end + button(:mmv_options_enable_cancel_button) do |page| + page.mmv_options_menu_enable_element.div_element(class: 'mw-mmv-options-submit').button_element(class: 'mw-mmv-options-cancel-button') + end + button(:mmv_options_disable_cancel_button) do |page| + page.mmv_options_menu_disable_element.div_element(class: 'mw-mmv-options-submit').button_element(class: 'mw-mmv-options-cancel-button') + end + div(:mmv_options_disable_confirmation, class: 'mw-mmv-disable-confirmation') + div(:mmv_options_disable_x_icon) do |page| + page.mmv_options_disable_confirmation_element.div_element(class: 'mw-mmv-confirmation-close') + end + div(:mmv_options_enable_confirmation, class: 'mw-mmv-enable-confirmation') + div(:mmv_options_enable_x_icon) do |page| + page.mmv_options_enable_confirmation_element.div_element(class: 'mw-mmv-confirmation-close') + end +end diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/samples/MediaViewerE2ETest.wikitext b/www/wiki/extensions/MultimediaViewer/tests/browser/samples/MediaViewerE2ETest.wikitext new file mode 100644 index 00000000..a920e77f --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/browser/samples/MediaViewerE2ETest.wikitext @@ -0,0 +1,14 @@ +<big>PLEASE DO NOT EDIT THIS PAGE! IT NEEDS TO STAY THE SAME FOR THE PURPOSE OF AUTOMATED TESTING</big> + +==Test Images== +Here are some sample images for testing different features of Media Viewer. + +[[File:Sunrise over fishing boats in Kerala.jpg|thumb|left|Sunrise over fishing boats]] [[File:Wikimedia_Foundation_2013_All_Hands_Offsite_-_Day_2_-_Photo_24.jpg|thumb|Tropical Fish Aquarium]] [[File:Wikimania 2013 - Hong Kong - Photo 090.jpg|thumb|center|Hong Kong Harbor at night]] +<br clear="all"/> + +[[File:Wikimedia_Foundation_2013_All_Hands_Offsite_-_Day_2_-_Photo_16.jpg|thumb|left|Nautilus Shell at California Academy of Sciences]] [[File:Multimedia_Team_-_Wikimedia_Foundation.jpg|thumb|center|Multimedia Team]] [[File:Zonotrichia atricapilla -British Columbia, Canada-8.jpg|thumb|Golden-crowned Sparrow]] +<br clear="all"/> + +[[File:Multimedia Roundtable 5 Photo 2.jpg|thumb|left|Multimedia Roundtable]] [[File:Wikimedia Foundation - Team 1 - California Academy of Sciences.jpg|thumb|Wikimedia Team]] + +<br clear="all"/> diff --git a/www/wiki/extensions/MultimediaViewer/tests/phan/config.php b/www/wiki/extensions/MultimediaViewer/tests/phan/config.php new file mode 100644 index 00000000..0cfe0c1a --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/phan/config.php @@ -0,0 +1,19 @@ +<?php + +$cfg = require __DIR__ . '/../../vendor/mediawiki/mediawiki-phan-config/src/config.php'; + +$cfg['directory_list'] = array_merge( + $cfg['directory_list'], + [ + './../../extensions/BetaFeatures', + ] +); + +$cfg['exclude_analysis_directory_list'] = array_merge( + $cfg['exclude_analysis_directory_list'], + [ + './../../extensions/BetaFeatures', + ] +); + +return $cfg; diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ActionLogger.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ActionLogger.test.js new file mode 100644 index 00000000..9a1808c4 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ActionLogger.test.js @@ -0,0 +1,48 @@ +( function ( mw, $ ) { + QUnit.module( 'mmv.logging.ActionLogger', QUnit.newMwEnvironment() ); + + QUnit.test( 'log()', function ( assert ) { + var fakeEventLog = { logEvent: this.sandbox.stub() }, + logger = new mw.mmv.logging.ActionLogger(), + action1key = 'test-1', + action1value = 'Test', + action2key = 'test-2', + action2value = 'Foo $1 $2 bar', + unknownAction = 'test-3', + clock = this.sandbox.useFakeTimers(); + + this.sandbox.stub( logger, 'loadDependencies' ).returns( $.Deferred().resolve() ); + this.sandbox.stub( mw, 'log' ); + + logger.samplingFactorMap = { 'default': 1 }; + logger.setEventLog( fakeEventLog ); + logger.logActions = {}; + logger.logActions[ action1key ] = action1value; + logger.logActions[ action2key ] = action2value; + + logger.log( unknownAction ); + clock.tick( 10 ); + + assert.strictEqual( mw.log.lastCall.args[ 0 ], unknownAction, 'Log message defaults to unknown key' ); + assert.ok( fakeEventLog.logEvent.called, 'event log has been recorded' ); + + mw.log.reset(); + fakeEventLog.logEvent.reset(); + logger.log( action1key ); + clock.tick( 10 ); + + assert.strictEqual( mw.log.lastCall.args[ 0 ], action1value, 'Log message is translated to its text' ); + assert.ok( fakeEventLog.logEvent.called, 'event log has been recorded' ); + + mw.log.reset(); + fakeEventLog.logEvent.reset(); + logger.samplingFactorMap = { 'default': 0 }; + logger.log( action1key, true ); + clock.tick( 10 ); + + assert.ok( !mw.log.called, 'No logging when disabled' ); + assert.ok( fakeEventLog.logEvent.called, 'event log has been recorded' ); + + clock.restore(); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.AttributionLogger.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.AttributionLogger.test.js new file mode 100644 index 00000000..e62d8109 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.AttributionLogger.test.js @@ -0,0 +1,22 @@ +( function ( mw, $ ) { + QUnit.module( 'mmv.logging.AttributionLogger', QUnit.newMwEnvironment() ); + + QUnit.test( 'log()', function ( assert ) { + var fakeEventLog = { logEvent: this.sandbox.stub() }, + logger = new mw.mmv.logging.AttributionLogger(), + image = { author: 'foo', source: 'bar', license: {} }, + emptyImage = {}; + + this.sandbox.stub( logger, 'loadDependencies' ).returns( $.Deferred().resolve() ); + this.sandbox.stub( mw, 'log' ); + + logger.samplingFactor = 1; + logger.setEventLog( fakeEventLog ); + + logger.logAttribution( image ); + assert.ok( true, 'logDimensions() did not throw errors' ); + + logger.logAttribution( emptyImage ); + assert.ok( true, 'logDimensions() did not throw errors for empty image' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DimensionLogger.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DimensionLogger.test.js new file mode 100644 index 00000000..0df8c02f --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DimensionLogger.test.js @@ -0,0 +1,17 @@ +( function ( mw, $ ) { + QUnit.module( 'mmv.logging.DimensionLogger', QUnit.newMwEnvironment() ); + + QUnit.test( 'log()', function ( assert ) { + var fakeEventLog = { logEvent: this.sandbox.stub() }, + logger = new mw.mmv.logging.DimensionLogger(); + + this.sandbox.stub( logger, 'loadDependencies' ).returns( $.Deferred().resolve() ); + this.sandbox.stub( mw, 'log' ); + + logger.samplingFactor = 1; + logger.setEventLog( fakeEventLog ); + + logger.logDimensions( 640, 480, 200, 'resize' ); + assert.ok( true, 'logDimensions() did not throw errors' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DurationLogger.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DurationLogger.test.js new file mode 100644 index 00000000..474fc08c --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DurationLogger.test.js @@ -0,0 +1,218 @@ +( function ( mw, $ ) { + QUnit.module( 'mmv.logging.DurationLogger', QUnit.newMwEnvironment( { + setup: function () { + this.clock = this.sandbox.useFakeTimers(); + + // since jQuery 2/3, $.now will capture a reference to Date.now + // before above fake timer gets a chance to override it, so I'll + // override that new behavior in order to run these tests... + // @see https://github.com/sinonjs/lolex/issues/76 + this.oldNow = $.now; + $.now = function () { return +( new Date() ); }; + }, + + teardown: function () { + $.now = this.oldNow; + this.clock.restore(); + } + } ) ); + + QUnit.test( 'start()', function ( assert ) { + var durationLogger = new mw.mmv.durationLogger.constructor(); + durationLogger.samplingFactor = 1; + + try { + durationLogger.start(); + } catch ( e ) { + assert.ok( true, 'Exception raised when calling start() without parameters' ); + } + assert.ok( $.isEmptyObject( durationLogger.starts ), 'No events saved by DurationLogger' ); + + durationLogger.start( 'foo' ); + assert.strictEqual( durationLogger.starts.foo, 0, 'Event start saved' ); + + this.clock.tick( 1000 ); + durationLogger.start( 'bar' ); + assert.strictEqual( durationLogger.starts.bar, 1000, 'Later event start saved' ); + + durationLogger.start( 'foo' ); + assert.strictEqual( durationLogger.starts.foo, 0, 'Event start not overritten' ); + + this.clock.tick( 666 ); + durationLogger.start( [ 'baz', 'bob', 'bar' ] ); + assert.strictEqual( durationLogger.starts.baz, 1666, 'First simultaneous event start saved' ); + assert.strictEqual( durationLogger.starts.bob, 1666, 'Second simultaneous event start saved' ); + assert.strictEqual( durationLogger.starts.bar, 1000, 'Third simultaneous event start not overwritten' ); + } ); + + QUnit.test( 'stop()', function ( assert ) { + var durationLogger = new mw.mmv.durationLogger.constructor(); + + try { + durationLogger.stop(); + } catch ( e ) { + assert.ok( true, 'Exception raised when calling stop() without parameters' ); + } + + durationLogger.stop( 'foo' ); + + assert.strictEqual( durationLogger.stops.foo, 0, 'Event stop saved' ); + + this.clock.tick( 1000 ); + durationLogger.stop( 'foo' ); + + assert.strictEqual( durationLogger.stops.foo, 0, 'Event stop not overwritten' ); + + durationLogger.stop( 'foo', 1 ); + + assert.strictEqual( durationLogger.starts.foo, 1, 'Event start saved' ); + + durationLogger.stop( 'foo', 2 ); + + assert.strictEqual( durationLogger.starts.foo, 1, 'Event start not overwritten' ); + } ); + + QUnit.test( 'record()', function ( assert ) { + var dependenciesDeferred = $.Deferred(), + fakeEventLog = { logEvent: this.sandbox.stub() }, + durationLogger = new mw.mmv.durationLogger.constructor(); + + durationLogger.samplingFactor = 1; + durationLogger.schemaSupportsCountry = this.sandbox.stub().returns( true ); + + this.sandbox.stub( mw.user, 'isAnon' ).returns( false ); + this.sandbox.stub( durationLogger, 'loadDependencies' ).returns( dependenciesDeferred.promise() ); + + try { + durationLogger.record(); + } catch ( e ) { + assert.ok( true, 'Exception raised when calling record() without parameters' ); + } + + durationLogger.setEventLog( fakeEventLog ); + + durationLogger.start( 'bar' ); + this.clock.tick( 1000 ); + durationLogger.stop( 'bar' ); + durationLogger.record( 'bar' ); + + assert.ok( !fakeEventLog.logEvent.called, 'Event queued if dependencies not loaded' ); + + // Queue a second item + + durationLogger.start( 'bob' ); + this.clock.tick( 4000 ); + durationLogger.stop( 'bob' ); + durationLogger.record( 'bob' ); + + assert.ok( !fakeEventLog.logEvent.called, 'Event queued if dependencies not loaded' ); + + dependenciesDeferred.resolve(); + this.clock.tick( 10 ); + + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 0 ], 'MultimediaViewerDuration', 'EventLogging schema is correct' ); + assert.deepEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ], { type: 'bar', duration: 1000, loggedIn: true, samplingFactor: 1 }, + 'EventLogging data is correct' ); + + assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 0 ], 'MultimediaViewerDuration', 'EventLogging schema is correct' ); + assert.deepEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 1 ], { type: 'bob', duration: 4000, loggedIn: true, samplingFactor: 1 }, + 'EventLogging data is correct' ); + + assert.strictEqual( fakeEventLog.logEvent.callCount, 2, 'logEvent called when processing the queue' ); + + durationLogger.start( 'foo' ); + this.clock.tick( 3000 ); + durationLogger.stop( 'foo' ); + durationLogger.record( 'foo' ); + this.clock.tick( 10 ); + + assert.strictEqual( fakeEventLog.logEvent.getCall( 2 ).args[ 0 ], 'MultimediaViewerDuration', 'EventLogging schema is correct' ); + assert.deepEqual( fakeEventLog.logEvent.getCall( 2 ).args[ 1 ], { type: 'foo', duration: 3000, loggedIn: true, samplingFactor: 1 }, + 'EventLogging data is correct' ); + + assert.strictEqual( durationLogger.starts.bar, undefined, 'Start value deleted after record' ); + assert.strictEqual( durationLogger.stops.bar, undefined, 'Stop value deleted after record' ); + + durationLogger.setGeo( { country: 'FR' } ); + mw.user.isAnon.returns( true ); + + durationLogger.start( 'baz' ); + this.clock.tick( 2000 ); + durationLogger.stop( 'baz' ); + durationLogger.record( 'baz' ); + this.clock.tick( 10 ); + + assert.strictEqual( fakeEventLog.logEvent.getCall( 3 ).args[ 0 ], 'MultimediaViewerDuration', 'EventLogging schema is correct' ); + assert.deepEqual( fakeEventLog.logEvent.getCall( 3 ).args[ 1 ], { type: 'baz', duration: 2000, loggedIn: false, country: 'FR', samplingFactor: 1 }, + 'EventLogging data is correct' ); + + assert.strictEqual( durationLogger.starts.bar, undefined, 'Start value deleted after record' ); + assert.strictEqual( durationLogger.stops.bar, undefined, 'Stop value deleted after record' ); + + durationLogger.stop( 'fooz', $.now() - 9000 ); + durationLogger.record( 'fooz' ); + this.clock.tick( 10 ); + + assert.deepEqual( fakeEventLog.logEvent.getCall( 4 ).args[ 1 ], { type: 'fooz', duration: 9000, loggedIn: false, country: 'FR', samplingFactor: 1 }, + 'EventLogging data is correct' ); + + assert.strictEqual( fakeEventLog.logEvent.callCount, 5, 'logEvent has been called fives times at this point in the test' ); + + durationLogger.stop( 'foo' ); + durationLogger.record( 'foo' ); + this.clock.tick( 10 ); + + assert.strictEqual( fakeEventLog.logEvent.callCount, 5, 'Record without a start doesn\'t get logged' ); + + durationLogger.start( 'foofoo' ); + durationLogger.record( 'foofoo' ); + this.clock.tick( 10 ); + + assert.strictEqual( fakeEventLog.logEvent.callCount, 5, 'Record without a stop doesn\'t get logged' ); + + durationLogger.start( 'extra' ); + this.clock.tick( 5000 ); + durationLogger.stop( 'extra' ); + durationLogger.record( 'extra', { bim: 'bam' } ); + this.clock.tick( 10 ); + + assert.deepEqual( fakeEventLog.logEvent.getCall( 5 ).args[ 1 ], { type: 'extra', duration: 5000, loggedIn: false, country: 'FR', samplingFactor: 1, bim: 'bam' }, + 'EventLogging data is correct' ); + } ); + + QUnit.test( 'loadDependencies()', function ( assert ) { + var promise, + durationLogger = new mw.mmv.durationLogger.constructor(); + + this.sandbox.stub( mw.loader, 'using' ); + + mw.loader.using.withArgs( [ 'ext.eventLogging', 'schema.MultimediaViewerDuration' ] ).throwsException( 'EventLogging is missing' ); + + promise = durationLogger.loadDependencies(); + this.clock.tick( 10 ); + + assert.strictEqual( promise.state(), 'rejected', 'Promise is rejected' ); + + // It's necessary to reset the stub, otherwise the original withArgs keeps running alongside the new one + mw.loader.using.restore(); + this.sandbox.stub( mw.loader, 'using' ); + + mw.loader.using.withArgs( [ 'ext.eventLogging', 'schema.MultimediaViewerDuration' ] ).throwsException( 'EventLogging is missing' ); + + promise = durationLogger.loadDependencies(); + this.clock.tick( 10 ); + + assert.strictEqual( promise.state(), 'rejected', 'Promise is rejected' ); + + // It's necessary to reset the stub, otherwise the original withArgs keeps running alongside the new one + mw.loader.using.restore(); + this.sandbox.stub( mw.loader, 'using' ); + + mw.loader.using.withArgs( [ 'ext.eventLogging', 'schema.MultimediaViewerDuration' ] ).callsArg( 1 ); + + promise = durationLogger.loadDependencies(); + this.clock.tick( 10 ); + + assert.strictEqual( promise.state(), 'resolved', 'Promise is resolved' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.PerformanceLogger.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.PerformanceLogger.test.js new file mode 100644 index 00000000..81a621f7 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.PerformanceLogger.test.js @@ -0,0 +1,341 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MultimediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.logging.PerformanceLogger', QUnit.newMwEnvironment() ); + + function createFakeXHR( response ) { + return { + readyState: 0, + open: $.noop, + send: function () { + var xhr = this; + + setTimeout( function () { + xhr.readyState = 4; + xhr.response = response; + if ( $.isFunction( xhr.onreadystatechange ) ) { + xhr.onreadystatechange(); + } + }, 0 ); + } + }; + } + + QUnit.test( 'recordEntry: basic', function ( assert ) { + var performance = new mw.mmv.logging.PerformanceLogger(), + fakeEventLog = { logEvent: this.sandbox.stub() }, + type = 'gender', + total = 100, + // we'll be waiting for 4 promises to complete + asyncs = [ assert.async(), assert.async(), assert.async(), assert.async() ]; + + this.sandbox.stub( performance, 'loadDependencies' ).returns( $.Deferred().resolve() ); + this.sandbox.stub( performance, 'isInSample' ); + performance.setEventLog( fakeEventLog ); + + performance.isInSample.returns( false ); + + performance.recordEntry( type, total ).then( null, function () { + assert.strictEqual( fakeEventLog.logEvent.callCount, 0, 'No stats should be logged if not in sample' ); + asyncs.pop()(); + } ); + + performance.isInSample.returns( true ); + + performance.recordEntry( type, total ).then( null, function () { + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 0 ], 'MultimediaViewerNetworkPerformance', 'EventLogging schema is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].type, type, 'type is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].total, total, 'total is correct' ); + assert.strictEqual( fakeEventLog.logEvent.callCount, 1, 'Stats should be logged' ); + asyncs.pop()(); + } ); + + performance.recordEntry( type, total, 'URL' ).then( null, function () { + assert.strictEqual( fakeEventLog.logEvent.callCount, 2, 'Stats should be logged' ); + asyncs.pop()(); + } ); + + performance.recordEntry( type, total, 'URL' ).then( null, function () { + assert.strictEqual( fakeEventLog.logEvent.callCount, 2, 'Stats should not be logged a second time for the same URL' ); + asyncs.pop()(); + } ); + } ); + + QUnit.test( 'recordEntry: with Navigation Timing data', function ( assert ) { + var fakeRequest, + varnish1 = 'cp1061', + varnish2 = 'cp3006', + varnish3 = 'cp3005', + varnish1hits = 0, + varnish2hits = 2, + varnish3hits = 1, + xvarnish = '1754811951 1283049064, 1511828531, 1511828573 1511828528', + xcache = varnish1 + ' miss (0), ' + varnish2 + ' miss (2), ' + varnish3 + ' frontend hit (1), malformed(5)', + age = '12345', + contentLength = '23456', + urlHost = 'fail', + date = 'Tue, 04 Feb 2014 11:11:50 GMT', + timestamp = 1391512310, + url = 'https://' + urlHost + '/balls.jpg', + redirect = 500, + dns = 2, + tcp = 10, + request = 25, + response = 50, + cache = 1, + perfData = { + initiatorType: 'xmlhttprequest', + name: url, + duration: 12345, + redirectStart: 1000, + redirectEnd: 1500, + domainLookupStart: 2, + domainLookupEnd: 4, + connectStart: 50, + connectEnd: 60, + requestStart: 125, + responseStart: 150, + responseEnd: 200, + fetchStart: 1 + }, + country = 'FR', + type = 'image', + performance = new mw.mmv.logging.PerformanceLogger(), + status = 200, + metered = true, + bandwidth = 45.67, + fakeEventLog = { logEvent: this.sandbox.stub() }, + done = assert.async(); + + this.sandbox.stub( performance, 'loadDependencies' ).returns( $.Deferred().resolve() ); + performance.setEventLog( fakeEventLog ); + performance.schemaSupportsCountry = this.sandbox.stub().returns( true ); + + this.sandbox.stub( performance, 'getWindowPerformance' ).returns( { + getEntriesByName: function () { + return [ perfData, { + initiatorType: 'bogus', + duration: 1234, + name: url + } ]; + } + } ); + + this.sandbox.stub( performance, 'getNavigatorConnection' ).returns( { metered: metered, bandwidth: bandwidth } ); + this.sandbox.stub( performance, 'isInSample' ).returns( true ); + + fakeRequest = { + getResponseHeader: function ( header ) { + switch ( header ) { + case 'X-Cache': + return xcache; + case 'X-Varnish': + return xvarnish; + case 'Age': + return age; + case 'Content-Length': + return contentLength; + case 'Date': + return date; + } + }, + status: status + }; + + performance.setGeo( { country: country } ); + + performance.recordEntry( type, 100, url, fakeRequest ).then( null, function () { + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 0 ], 'MultimediaViewerNetworkPerformance', 'EventLogging schema is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].type, type, 'type is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish1, varnish1, 'varnish1 is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish2, varnish2, 'varnish2 is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish3, varnish3, 'varnish3 is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish4, undefined, 'varnish4 is undefined' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish1hits, varnish1hits, 'varnish1hits is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish2hits, varnish2hits, 'varnish2hits is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish3hits, varnish3hits, 'varnish3hits is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish4hits, undefined, 'varnish4hits is undefined' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].XVarnish, xvarnish, 'XVarnish is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].XCache, xcache, 'XCache is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].age, parseInt( age, 10 ), 'age is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].contentLength, parseInt( contentLength, 10 ), 'contentLength is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].contentHost, window.location.host, 'contentHost is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].urlHost, urlHost, 'urlHost is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].timestamp, timestamp, 'timestamp is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].total, perfData.duration, 'total is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].redirect, redirect, 'redirect is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].dns, dns, 'dns is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].tcp, tcp, 'tcp is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].request, request, 'request is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].response, response, 'response is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].cache, cache, 'cache is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].country, country, 'country is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].isHttps, true, 'isHttps is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].status, status, 'status is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].metered, metered, 'metered is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].bandwidth, Math.round( bandwidth ), 'bandwidth is correct' ); + done(); + } ); + } ); + + QUnit.test( 'recordEntry: with async extra stats', function ( assert ) { + var performance = new mw.mmv.logging.PerformanceLogger(), + fakeEventLog = { logEvent: this.sandbox.stub() }, + type = 'gender', + total = 100, + overriddenType = 'image', + foo = 'bar', + extraStatsPromise = $.Deferred(), + clock = this.sandbox.useFakeTimers(); + + this.sandbox.stub( performance, 'loadDependencies' ).returns( $.Deferred().resolve() ); + this.sandbox.stub( performance, 'isInSample' ); + performance.setEventLog( fakeEventLog ); + + performance.isInSample.returns( true ); + + performance.recordEntry( type, total, 'URL1', undefined, extraStatsPromise ); + + assert.strictEqual( fakeEventLog.logEvent.callCount, 0, 'Stats should not be logged if the promise hasn\'t completed yet' ); + + extraStatsPromise.reject(); + + extraStatsPromise.then( null, function () { + assert.strictEqual( fakeEventLog.logEvent.callCount, 1, 'Stats should be logged' ); + + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 0 ], 'MultimediaViewerNetworkPerformance', 'EventLogging schema is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].type, type, 'type is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].total, total, 'total is correct' ); + } ); + + // make sure first promise is completed before recording another entry, + // to make sure data in fakeEventLog doesn't suffer race conditions + clock.tick( 10 ); + clock.restore(); + + extraStatsPromise = $.Deferred(); + + performance.recordEntry( type, total, 'URL2', undefined, extraStatsPromise ); + + assert.strictEqual( fakeEventLog.logEvent.callCount, 1, 'Stats should not be logged if the promise hasn\'t been resolved yet' ); + + extraStatsPromise.resolve( { type: overriddenType, foo: foo } ); + + return extraStatsPromise.then( function () { + assert.strictEqual( fakeEventLog.logEvent.callCount, 2, 'Stats should be logged' ); + + assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 0 ], 'MultimediaViewerNetworkPerformance', 'EventLogging schema is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 1 ].type, overriddenType, 'type is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 1 ].total, total, 'total is correct' ); + assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 1 ].foo, foo, 'extra stat is correct' ); + } ); + } ); + + QUnit.test( 'parseVarnishXCacheHeader', function ( assert ) { + var varnish1 = 'cp1061', + varnish2 = 'cp3006', + varnish3 = 'cp3005', + testString = varnish1 + ' miss (0), ' + varnish2 + ' miss (0), ' + varnish3 + ' frontend hit (1)', + performance = new mw.mmv.logging.PerformanceLogger(), + varnishXCache = performance.parseVarnishXCacheHeader( testString ); + + assert.strictEqual( varnishXCache.varnish1, varnish1, 'First varnish server name extracted' ); + assert.strictEqual( varnishXCache.varnish2, varnish2, 'Second varnish server name extracted' ); + assert.strictEqual( varnishXCache.varnish3, varnish3, 'Third varnish server name extracted' ); + assert.strictEqual( varnishXCache.varnish4, undefined, 'Fourth varnish server is undefined' ); + assert.strictEqual( varnishXCache.varnish1hits, 0, 'First varnish hit count extracted' ); + assert.strictEqual( varnishXCache.varnish2hits, 0, 'Second varnish hit count extracted' ); + assert.strictEqual( varnishXCache.varnish3hits, 1, 'Third varnish hit count extracted' ); + assert.strictEqual( varnishXCache.varnish4hits, undefined, 'Fourth varnish hit count is undefined' ); + + testString = varnish1 + ' miss (36), ' + varnish2 + ' miss (2)'; + varnishXCache = performance.parseVarnishXCacheHeader( testString ); + + assert.strictEqual( varnishXCache.varnish1, varnish1, 'First varnish server name extracted' ); + assert.strictEqual( varnishXCache.varnish2, varnish2, 'Second varnish server name extracted' ); + assert.strictEqual( varnishXCache.varnish3, undefined, 'Third varnish server is undefined' ); + assert.strictEqual( varnishXCache.varnish1hits, 36, 'First varnish hit count extracted' ); + assert.strictEqual( varnishXCache.varnish2hits, 2, 'Second varnish hit count extracted' ); + assert.strictEqual( varnishXCache.varnish3hits, undefined, 'Third varnish hit count is undefined' ); + + varnishXCache = performance.parseVarnishXCacheHeader( 'garbage' ); + assert.ok( $.isEmptyObject( varnishXCache ), 'Varnish cache results are empty' ); + } ); + + QUnit.test( 'record()', function ( assert ) { + var type = 'foo', + url = 'http://example.com/', + response = {}, + done = assert.async(), + performance = new mw.mmv.logging.PerformanceLogger(); + + performance.newXHR = function () { return createFakeXHR( response ); }; + + performance.recordEntryDelayed = function ( recordType, _, recordUrl, recordRequest ) { + assert.strictEqual( recordType, type, 'type is recorded correctly' ); + assert.strictEqual( recordUrl, url, 'url is recorded correctly' ); + assert.strictEqual( recordRequest.response, response, 'response is recorded correctly' ); + done(); + }; + + return performance.record( type, url ).done( function ( recordResponse ) { + assert.strictEqual( recordResponse, response, 'response is passed to callback' ); + } ); + } ); + + QUnit.test( 'record() with old browser', function ( assert ) { + var type = 'foo', + url = 'http://example.com/', + done = assert.async(), + performance = new mw.mmv.logging.PerformanceLogger(); + + performance.newXHR = function () { throw new Error( 'XMLHttpRequest? What\'s that?' ); }; + + performance.record( type, url ).fail( function () { + assert.ok( true, 'the promise is rejected when XMLHttpRequest is not supported' ); + done(); + } ); + } ); + + QUnit.test( 'mw.mmv.logging.Api', function ( assert ) { + var api, + oldRecord = mw.mmv.logging.PerformanceLogger.prototype.recordJQueryEntryDelayed, + oldAjax = mw.Api.prototype.ajax, + ajaxCalled = false, + fakeJqxhr = {}; + + mw.Api.prototype.ajax = function () { + ajaxCalled = true; + return $.Deferred().resolve( {}, fakeJqxhr ); + }; + + mw.mmv.logging.PerformanceLogger.prototype.recordJQueryEntryDelayed = function ( type, total, jqxhr ) { + assert.strictEqual( type, 'foo', 'type was passed correctly' ); + assert.strictEqual( jqxhr, fakeJqxhr, 'jqXHR was passed correctly' ); + }; + + api = new mw.mmv.logging.Api( 'foo' ); + + api.ajax(); + + assert.ok( ajaxCalled, 'parent ajax() function was called' ); + + mw.mmv.logging.PerformanceLogger.prototype.recordJQueryEntryDelayed = oldRecord; + mw.Api.prototype.ajax = oldAjax; + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ViewLogger.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ViewLogger.test.js new file mode 100644 index 00000000..5da3633f --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ViewLogger.test.js @@ -0,0 +1,87 @@ +( function ( mw, $ ) { + QUnit.module( 'mmv.logging.ViewLogger', QUnit.newMwEnvironment( { + setup: function () { + this.clock = this.sandbox.useFakeTimers(); + + // since jQuery 2/3, $.now will capture a reference to Date.now + // before above fake timer gets a chance to override it, so I'll + // override that new behavior in order to run these tests... + // @see https://github.com/sinonjs/lolex/issues/76 + this.oldNow = $.now; + $.now = function () { return +( new Date() ); }; + }, + + teardown: function () { + $.now = this.oldNow; + this.clock.restore(); + } + } ) ); + + QUnit.test( 'unview()', function ( assert ) { + var logger = { log: $.noop }, + viewLogger = new mw.mmv.logging.ViewLogger( { recordVirtualViewBeaconURI: $.noop }, {}, logger ); + + this.sandbox.stub( logger, 'log' ); + + viewLogger.unview(); + + assert.ok( !logger.log.called, 'action logger not called' ); + + viewLogger.setLastViewLogged( false ); + viewLogger.unview(); + + assert.ok( !logger.log.called, 'action logger not called' ); + + viewLogger.setLastViewLogged( true ); + viewLogger.unview(); + + assert.ok( logger.log.calledOnce, 'action logger called' ); + + viewLogger.unview(); + + assert.ok( logger.log.calledOnce, 'action logger not called again' ); + } ); + + QUnit.test( 'focus and blur', function ( assert ) { + var fakeWindow = $( '<div>' ), + viewLogger = new mw.mmv.logging.ViewLogger( { recordVirtualViewBeaconURI: $.noop }, fakeWindow, { log: $.noop } ); + + this.clock.tick( 1 ); // This is just so that $.now() > 0 in the fake timer environment + + viewLogger.attach(); + + this.clock.tick( 5 ); + + fakeWindow.triggerHandler( 'blur' ); + + this.clock.tick( 2 ); + + fakeWindow.triggerHandler( 'focus' ); + + this.clock.tick( 3 ); + + fakeWindow.triggerHandler( 'blur' ); + + this.clock.tick( 4 ); + + assert.strictEqual( viewLogger.viewDuration, 8, 'Only focus duration was logged' ); + } ); + + QUnit.test( 'stopViewDuration before startViewDuration', function ( assert ) { + var viewLogger = new mw.mmv.logging.ViewLogger( { recordVirtualViewBeaconURI: $.noop }, {}, { log: $.noop } ); + + this.clock.tick( 1 ); // This is just so that $.now() > 0 in the fake timer environment + + viewLogger.stopViewDuration(); + + this.clock.tick( 2 ); + + viewLogger.startViewDuration(); + + this.clock.tick( 3 ); + + viewLogger.stopViewDuration(); + + assert.strictEqual( viewLogger.viewDuration, 3, 'Only last timeframe was logged' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.Config.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.Config.test.js new file mode 100644 index 00000000..0a1b6a61 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.Config.test.js @@ -0,0 +1,203 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.Config', QUnit.newMwEnvironment() ); + + QUnit.test( 'Constructor sanity test', function ( assert ) { + var config = new mw.mmv.Config( {}, {}, {}, {}, null ); + assert.ok( config ); + } ); + + QUnit.test( 'Localstorage get', function ( assert ) { + var localStorage, config; + + localStorage = mw.mmv.testHelpers.getUnsupportedLocalStorage(); // no browser support + config = new mw.mmv.Config( {}, {}, {}, {}, localStorage ); + assert.strictEqual( config.getFromLocalStorage( 'foo' ), null, 'Returns null when not supported' ); + assert.strictEqual( config.getFromLocalStorage( 'foo', 'bar' ), 'bar', 'Returns fallback when not supported' ); + + localStorage = mw.mmv.testHelpers.getDisabledLocalStorage(); // browser supports it but disabled + config = new mw.mmv.Config( {}, {}, {}, {}, localStorage ); + assert.strictEqual( config.getFromLocalStorage( 'foo' ), null, 'Returns null when disabled' ); + assert.strictEqual( config.getFromLocalStorage( 'foo', 'bar' ), 'bar', 'Returns fallback when disabled' ); + + localStorage = mw.mmv.testHelpers.createLocalStorage( { getItem: this.sandbox.stub() } ); + config = new mw.mmv.Config( {}, {}, {}, {}, localStorage ); + + localStorage.store.getItem.withArgs( 'foo' ).returns( null ); + assert.strictEqual( config.getFromLocalStorage( 'foo' ), null, 'Returns null when key not set' ); + assert.strictEqual( config.getFromLocalStorage( 'foo', 'bar' ), 'bar', 'Returns fallback when key not set' ); + + localStorage.store.getItem.reset(); + localStorage.store.getItem.withArgs( 'foo' ).returns( 'boom' ); + assert.strictEqual( config.getFromLocalStorage( 'foo' ), 'boom', 'Returns correct value' ); + assert.strictEqual( config.getFromLocalStorage( 'foo', 'bar' ), 'boom', 'Returns correct value ignoring fallback' ); + } ); + + QUnit.test( 'Localstorage set', function ( assert ) { + var localStorage, config; + + localStorage = mw.mmv.testHelpers.getUnsupportedLocalStorage(); // no browser support + config = new mw.mmv.Config( {}, {}, {}, {}, localStorage ); + assert.strictEqual( config.setInLocalStorage( 'foo', 'bar' ), false, 'Returns false when not supported' ); + + localStorage = mw.mmv.testHelpers.getDisabledLocalStorage(); // browser supports it but disabled + config = new mw.mmv.Config( {}, {}, {}, {}, localStorage ); + assert.strictEqual( config.setInLocalStorage( 'foo', 'bar' ), false, 'Returns false when disabled' ); + + localStorage = mw.mmv.testHelpers.createLocalStorage( { setItem: this.sandbox.stub() } ); + config = new mw.mmv.Config( {}, {}, {}, {}, localStorage ); + + assert.strictEqual( config.setInLocalStorage( 'foo', 'bar' ), true, 'Returns true when works' ); + + localStorage.store.setItem.throwsException( 'localStorage full!' ); + assert.strictEqual( config.setInLocalStorage( 'foo', 'bar' ), false, 'Returns false on error' ); + } ); + + QUnit.test( 'Localstorage remove', function ( assert ) { + var localStorage, config; + + localStorage = mw.mmv.testHelpers.getUnsupportedLocalStorage(); // no browser support + config = new mw.mmv.Config( {}, {}, {}, {}, localStorage ); + assert.strictEqual( config.removeFromLocalStorage( 'foo' ), true, 'Returns true when not supported' ); + + localStorage = mw.mmv.testHelpers.getDisabledLocalStorage(); // browser supports it but disabled + config = new mw.mmv.Config( {}, {}, {}, {}, localStorage ); + assert.strictEqual( config.removeFromLocalStorage( 'foo' ), true, 'Returns true when disabled' ); + + localStorage = mw.mmv.testHelpers.createLocalStorage( { removeItem: this.sandbox.stub() } ); + config = new mw.mmv.Config( {}, {}, {}, {}, localStorage ); + assert.strictEqual( config.removeFromLocalStorage( 'foo' ), true, 'Returns true when works' ); + } ); + + QUnit.test( 'isMediaViewerEnabledOnClick', function ( assert ) { + var localStorage = mw.mmv.testHelpers.createLocalStorage( { getItem: this.sandbox.stub() } ), + mwConfig = { get: this.sandbox.stub() }, + mwUser = { isAnon: this.sandbox.stub() }, + config = new mw.mmv.Config( {}, mwConfig, mwUser, {}, localStorage ); + + mwUser.isAnon.returns( false ); + mwConfig.get.withArgs( 'wgMediaViewer' ).returns( true ); + mwConfig.get.withArgs( 'wgMediaViewerOnClick' ).returns( true ); + assert.strictEqual( config.isMediaViewerEnabledOnClick(), true, 'Returns true for logged-in with standard settings' ); + + mwUser.isAnon.returns( false ); + mwConfig.get.withArgs( 'wgMediaViewer' ).returns( false ); + mwConfig.get.withArgs( 'wgMediaViewerOnClick' ).returns( true ); + assert.strictEqual( config.isMediaViewerEnabledOnClick(), false, 'Returns false if opted out via user JS flag' ); + + mwUser.isAnon.returns( false ); + mwConfig.get.withArgs( 'wgMediaViewer' ).returns( true ); + mwConfig.get.withArgs( 'wgMediaViewerOnClick' ).returns( false ); + assert.strictEqual( config.isMediaViewerEnabledOnClick(), false, 'Returns false if opted out via preferences' ); + + mwUser.isAnon.returns( true ); + mwConfig.get.withArgs( 'wgMediaViewer' ).returns( false ); + mwConfig.get.withArgs( 'wgMediaViewerOnClick' ).returns( true ); + assert.strictEqual( config.isMediaViewerEnabledOnClick(), false, 'Returns false if anon user opted out via user JS flag' ); + + mwUser.isAnon.returns( true ); + mwConfig.get.withArgs( 'wgMediaViewer' ).returns( true ); + mwConfig.get.withArgs( 'wgMediaViewerOnClick' ).returns( false ); + assert.strictEqual( config.isMediaViewerEnabledOnClick(), false, 'Returns false if anon user opted out in some weird way' ); // apparently someone created a browser extension to do this + + mwUser.isAnon.returns( true ); + mwConfig.get.withArgs( 'wgMediaViewer' ).returns( true ); + mwConfig.get.withArgs( 'wgMediaViewerOnClick' ).returns( true ); + localStorage.store.getItem.withArgs( 'wgMediaViewerOnClick' ).returns( null ); + assert.strictEqual( config.isMediaViewerEnabledOnClick(), true, 'Returns true for anon with standard settings' ); + + mwUser.isAnon.returns( true ); + mwConfig.get.withArgs( 'wgMediaViewer' ).returns( true ); + mwConfig.get.withArgs( 'wgMediaViewerOnClick' ).returns( true ); + localStorage.store.getItem.withArgs( 'wgMediaViewerOnClick' ).returns( '0' ); + assert.strictEqual( config.isMediaViewerEnabledOnClick(), false, 'Returns true for anon opted out via localSettings' ); + } ); + + QUnit.test( 'setMediaViewerEnabledOnClick sanity check', function ( assert ) { + var localStorage = mw.mmv.testHelpers.createLocalStorage( { + getItem: this.sandbox.stub(), + setItem: this.sandbox.stub(), + removeItem: this.sandbox.stub() + } ), + mwUser = { isAnon: this.sandbox.stub() }, + mwConfig = new mw.Map(), + api = { saveOption: this.sandbox.stub().returns( $.Deferred().resolve() ) }, + config = new mw.mmv.Config( {}, mwConfig, mwUser, api, localStorage ); + mwConfig.set( 'wgMediaViewerEnabledByDefault', false ); + + mwUser.isAnon.returns( false ); + api.saveOption.returns( $.Deferred().resolve() ); + config.setMediaViewerEnabledOnClick( false ); + assert.ok( api.saveOption.called, 'For logged-in users, pref change is via API' ); + + mwUser.isAnon.returns( true ); + config.setMediaViewerEnabledOnClick( false ); + assert.ok( localStorage.store.setItem.called, 'For anons, opt-out is set in localStorage' ); + + mwUser.isAnon.returns( true ); + config.setMediaViewerEnabledOnClick( true ); + assert.ok( localStorage.store.removeItem.called, 'For anons, opt-in means clearing localStorage' ); + } ); + + QUnit.test( 'shouldShowStatusInfo', function ( assert ) { + var config, + mwConfig = new mw.Map(), + fakeLocalStorage = mw.mmv.testHelpers.getFakeLocalStorage(), + mwUser = { isAnon: this.sandbox.stub() }, + api = { saveOption: this.sandbox.stub().returns( $.Deferred().resolve() ) }; + + mwConfig.set( { + wgMediaViewer: true, + wgMediaViewerOnClick: true, + wgMediaViewerEnabledByDefault: true + } ); + config = new mw.mmv.Config( {}, mwConfig, mwUser, api, fakeLocalStorage ); + mwUser.isAnon.returns( false ); + + assert.strictEqual( config.shouldShowStatusInfo(), false, 'Status info is not shown by default' ); + config.setMediaViewerEnabledOnClick( false ); + assert.strictEqual( config.shouldShowStatusInfo(), true, 'Status info is shown after MMV is disabled the first time' ); + config.setMediaViewerEnabledOnClick( true ); + assert.strictEqual( config.shouldShowStatusInfo(), false, 'Status info is not shown when MMV is enabled' ); + config.setMediaViewerEnabledOnClick( false ); + assert.strictEqual( config.shouldShowStatusInfo(), true, 'Status info is shown after MMV is disabled the first time #2' ); + config.disableStatusInfo(); + assert.strictEqual( config.shouldShowStatusInfo(), false, 'Status info is not shown when already displayed once' ); + config.setMediaViewerEnabledOnClick( true ); + assert.strictEqual( config.shouldShowStatusInfo(), false, 'Further status changes have no effect' ); + config.setMediaViewerEnabledOnClick( false ); + assert.strictEqual( config.shouldShowStatusInfo(), false, 'Further status changes have no effect #2' ); + + // make sure disabling calls maybeEnableStatusInfo() for logged-in as well + config.localStorage = mw.mmv.testHelpers.getFakeLocalStorage(); + mwUser.isAnon.returns( true ); + assert.strictEqual( config.shouldShowStatusInfo(), false, 'Status info is not shown by default for logged-in users' ); + config.setMediaViewerEnabledOnClick( false ); + assert.strictEqual( config.shouldShowStatusInfo(), true, 'Status info is shown after MMV is disabled the first time for logged-in users' ); + + // make sure popup is not shown immediately on disabled-by-default sites, but still works otherwise + config.localStorage = mw.mmv.testHelpers.getFakeLocalStorage(); + mwConfig.set( 'wgMediaViewerEnabledByDefault', false ); + assert.strictEqual( config.shouldShowStatusInfo(), false, 'Status info is not shown by default #2' ); + config.setMediaViewerEnabledOnClick( true ); + assert.strictEqual( config.shouldShowStatusInfo(), false, 'Status info is not shown when MMV is enabled #2' ); + config.setMediaViewerEnabledOnClick( false ); + assert.strictEqual( config.shouldShowStatusInfo(), true, 'Status info is shown after MMV is disabled the first time #2' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.EmbedFileFormatter.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.EmbedFileFormatter.test.js new file mode 100644 index 00000000..d215eaf5 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.EmbedFileFormatter.test.js @@ -0,0 +1,293 @@ +( function ( mw ) { + QUnit.module( 'mmv.EmbedFileFormatter', QUnit.newMwEnvironment() ); + + function createEmbedFileInfo( options ) { + var license = options.licenseShortName ? new mw.mmv.model.License( options.licenseShortName, + options.licenseInternalName, options.licenseLongName, options.licenseUrl ) : undefined, + imageInfo = new mw.mmv.model.Image( + + options.title, + options.title.getNameText(), + undefined, + undefined, + undefined, + undefined, + options.imgUrl, + options.filePageUrl, + options.shortFilePageUrl, + 42, + 'repo', + undefined, + undefined, + undefined, + undefined, + options.source, + options.author, + options.authorCount, + license ), + repoInfo = { displayName: options.siteName, getSiteLink: + function () { return options.siteUrl; } }; + + return new mw.mmv.model.EmbedFileInfo( imageInfo, repoInfo, options.caption ); + } + + QUnit.test( 'EmbedFileFormatter constructor sanity check', function ( assert ) { + var formatter = new mw.mmv.EmbedFileFormatter(); + assert.ok( formatter, 'constructor with no argument works' ); + } ); + + QUnit.test( 'getByline():', function ( assert ) { + var formatter = new mw.mmv.EmbedFileFormatter(), + author = '<span class="mw-mmv-author">Homer</span>', + source = '<span class="mw-mmv-source">Iliad</span>', + attribution = '<span class="mw-mmv-attr">Cat</span>', + byline; + + // Works with no arguments + byline = formatter.getByline(); + assert.strictEqual( byline, undefined, 'No argument case handled correctly.' ); + + // Attribution present + byline = formatter.getByline( author, source, attribution ); + assert.ok( byline.match( /Cat/ ), 'Attribution found in bylines' ); + + // Author and source present + byline = formatter.getByline( author, source ); + assert.ok( byline.match( /Homer|Iliad/ ), 'Author and source found in bylines' ); + + // Only author present + byline = formatter.getByline( author ); + assert.ok( byline.match( /Homer/ ), 'Author found in bylines.' ); + + // Only source present + byline = formatter.getByline( undefined, source ); + assert.ok( byline.match( /Iliad/ ), 'Source found in bylines.' ); + } ); + + QUnit.test( 'getSiteLink():', function ( assert ) { + var repoInfo = new mw.mmv.model.Repo( 'Wikipedia', '//wikipedia.org/favicon.ico', true ), + info = new mw.mmv.model.EmbedFileInfo( {}, repoInfo ), + formatter = new mw.mmv.EmbedFileFormatter(), + siteUrl = repoInfo.getSiteLink(), + siteLink = formatter.getSiteLink( info ); + + assert.ok( siteLink.match( 'Wikipedia' ), 'Site name is present in site link' ); + assert.ok( siteLink.indexOf( siteUrl ) !== -1, 'Site URL is present in site link' ); + } ); + + QUnit.test( 'getThumbnailHtml():', function ( assert ) { + var formatter = new mw.mmv.EmbedFileFormatter(), + titleText = 'Music Room', + title = mw.Title.newFromText( titleText ), + imgUrl = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg', + filePageUrl = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg', + filePageShortUrl = 'https://commons.wikimedia.org/wiki/index.php?curid=42', + siteName = 'Site Name', + siteUrl = '//site.url/', + licenseShortName = 'Public License', + licenseInternalName = '-', + licenseLongName = 'Public Domain, copyrights have lapsed', + licenseUrl = '//example.com/pd', + author = '<span class="mw-mmv-author">Homer</span>', + source = '<span class="mw-mmv-source">Iliad</span>', + thumbUrl = 'https://upload.wikimedia.org/wikipedia/thumb/Foobar.jpg', + width = 700, + height = 500, + info, + generatedHtml; + + // Bylines, license and site + info = createEmbedFileInfo( { title: title, imgUrl: imgUrl, filePageUrl: filePageUrl, + shortFilePageUrl: filePageShortUrl, siteName: siteName, siteUrl: siteUrl, + licenseShortName: licenseShortName, licenseInternalName: licenseInternalName, + licenseLongName: licenseLongName, licenseUrl: licenseUrl, author: author, source: source } ); + + generatedHtml = formatter.getThumbnailHtml( info, thumbUrl, width, height ); + assert.ok( generatedHtml.match( titleText ), 'Title appears in generated HTML.' ); + assert.ok( generatedHtml.match( filePageUrl ), 'Page url appears in generated HTML.' ); + assert.ok( generatedHtml.match( thumbUrl ), 'Thumbnail url appears in generated HTML' ); + assert.ok( generatedHtml.match( 'Public License' ), 'License appears in generated HTML' ); + assert.ok( generatedHtml.match( 'Homer' ), 'Author appears in generated HTML' ); + assert.ok( generatedHtml.match( 'Iliad' ), 'Source appears in generated HTML' ); + assert.ok( generatedHtml.match( width ), 'Width appears in generated HTML' ); + assert.ok( generatedHtml.match( height ), 'Height appears in generated HTML' ); + // .includes() for checking the short url since it contains a ? (bad for regex). Could escape instead. + assert.ok( generatedHtml.includes( filePageShortUrl ), 'Short URL appears in generated HTML' ); + + // Bylines, no license and site + info = createEmbedFileInfo( { title: title, imgUrl: imgUrl, filePageUrl: filePageUrl, + shortFilePageUrl: filePageShortUrl, siteName: siteName, siteUrl: siteUrl, + author: author, source: source } ); + generatedHtml = formatter.getThumbnailHtml( info, thumbUrl, width, height ); + + assert.ok( generatedHtml.match( titleText ), 'Title appears in generated HTML.' ); + assert.ok( generatedHtml.match( filePageUrl ), 'Page url appears in generated HTML.' ); + assert.ok( generatedHtml.match( thumbUrl ), 'Thumbnail url appears in generated HTML' ); + assert.ok( !generatedHtml.match( 'Public License' ), 'License should not appear in generated HTML' ); + assert.ok( generatedHtml.match( 'Homer' ), 'Author appears in generated HTML' ); + assert.ok( generatedHtml.match( 'Iliad' ), 'Source appears in generated HTML' ); + assert.ok( generatedHtml.match( width ), 'Width appears in generated HTML' ); + assert.ok( generatedHtml.match( height ), 'Height appears in generated HTML' ); + assert.ok( generatedHtml.includes( filePageShortUrl ), 'Short URL appears in generated HTML' ); + + // No bylines, license and site + info = createEmbedFileInfo( { title: title, imgUrl: imgUrl, filePageUrl: filePageUrl, + siteName: siteName, siteUrl: siteUrl, licenseShortName: licenseShortName, + licenseInternalName: licenseInternalName, licenseLongName: licenseLongName, + licenseUrl: licenseUrl, shortFilePageUrl: filePageShortUrl } ); + generatedHtml = formatter.getThumbnailHtml( info, thumbUrl, width, height ); + + assert.ok( generatedHtml.match( titleText ), 'Title appears in generated HTML.' ); + assert.ok( generatedHtml.match( filePageUrl ), 'Page url appears in generated HTML.' ); + assert.ok( generatedHtml.match( thumbUrl ), 'Thumbnail url appears in generated HTML' ); + assert.ok( generatedHtml.match( 'Public License' ), 'License appears in generated HTML' ); + assert.ok( !generatedHtml.match( 'Homer' ), 'Author should not appear in generated HTML' ); + assert.ok( !generatedHtml.match( 'Iliad' ), 'Source should not appear in generated HTML' ); + assert.ok( generatedHtml.match( width ), 'Width appears in generated HTML' ); + assert.ok( generatedHtml.match( height ), 'Height appears in generated HTML' ); + assert.ok( generatedHtml.includes( filePageShortUrl ), 'Short URL appears in generated HTML' ); + + // No bylines, no license and site + info = createEmbedFileInfo( { title: title, imgUrl: imgUrl, filePageUrl: filePageUrl, + siteName: siteName, siteUrl: siteUrl, shortFilePageUrl: filePageShortUrl } ); + generatedHtml = formatter.getThumbnailHtml( info, thumbUrl, width, height ); + + assert.ok( generatedHtml.match( titleText ), 'Title appears in generated HTML.' ); + assert.ok( generatedHtml.match( filePageUrl ), 'Page url appears in generated HTML.' ); + assert.ok( generatedHtml.match( thumbUrl ), 'Thumbnail url appears in generated HTML' ); + assert.ok( !generatedHtml.match( 'Public License' ), 'License should not appear in generated HTML' ); + assert.ok( !generatedHtml.match( 'Homer' ), 'Author should not appear in generated HTML' ); + assert.ok( !generatedHtml.match( 'Iliad' ), 'Source should not appear in generated HTML' ); + assert.ok( generatedHtml.match( width ), 'Width appears in generated HTML' ); + assert.ok( generatedHtml.match( height ), 'Height appears in generated HTML' ); + assert.ok( generatedHtml.includes( filePageShortUrl ), 'Short URL appears in generated HTML' ); + + } ); + + QUnit.test( 'getThumbnailWikitext():', function ( assert ) { + var formatter = new mw.mmv.EmbedFileFormatter(), + title = mw.Title.newFromText( 'File:Foobar.jpg' ), + imgUrl = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg', + filePageUrl = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg', + caption = 'Foobar caption.', + width = 700, + info, + wikitext; + + // Title, width and caption + info = createEmbedFileInfo( { title: title, imgUrl: imgUrl, filePageUrl: filePageUrl, + caption: caption } ); + wikitext = formatter.getThumbnailWikitextFromEmbedFileInfo( info, width ); + + assert.strictEqual( + wikitext, + '[[File:Foobar.jpg|700px|thumb|Foobar caption.]]', + 'Wikitext generated correctly.' ); + + // Title, width and no caption + info = createEmbedFileInfo( { title: title, imgUrl: imgUrl, filePageUrl: filePageUrl } ); + wikitext = formatter.getThumbnailWikitextFromEmbedFileInfo( info, width ); + + assert.strictEqual( + wikitext, + '[[File:Foobar.jpg|700px|thumb|Foobar]]', + 'Wikitext generated correctly.' ); + + // Title, no width and no caption + info = createEmbedFileInfo( { title: title, imgUrl: imgUrl, filePageUrl: filePageUrl } ); + wikitext = formatter.getThumbnailWikitextFromEmbedFileInfo( info ); + + assert.strictEqual( + wikitext, + '[[File:Foobar.jpg|thumb|Foobar]]', + 'Wikitext generated correctly.' ); + } ); + + QUnit.test( 'getCreditText():', function ( assert ) { + var txt, formatter = new mw.mmv.EmbedFileFormatter(); + + txt = formatter.getCreditText( { + repoInfo: { + displayName: 'Localcommons' + }, + + imageInfo: { + author: 'Author', + source: 'Source', + descriptionShortUrl: 'link', + title: { + getNameText: function () { return 'Image Title'; } + } + } + } ); + + assert.strictEqual( txt, 'By Author - Source, link', 'Sanity check' ); + + txt = formatter.getCreditText( { + repoInfo: { + displayName: 'Localcommons' + }, + + imageInfo: { + author: 'Author', + source: 'Source', + descriptionShortUrl: 'link', + title: { + getNameText: function () { return 'Image Title'; } + }, + license: { + getShortName: function () { return 'WTFPL v2'; }, + longName: 'Do What the Fuck You Want Public License Version 2', + isFree: this.sandbox.stub().returns( true ) + } + } + } ); + + assert.strictEqual( txt, 'By Author - Source, WTFPL v2, link', 'License message works' ); + } ); + + QUnit.test( 'getCreditHtml():', function ( assert ) { + var html, formatter = new mw.mmv.EmbedFileFormatter(); + + html = formatter.getCreditHtml( { + repoInfo: { + displayName: 'Localcommons', + getSiteLink: function () { return 'quux'; } + }, + + imageInfo: { + author: 'Author', + source: 'Source', + descriptionShortUrl: 'some link', + title: { + getNameText: function () { return 'Image Title'; } + } + } + } ); + + assert.strictEqual( html, 'By Author - Source, <a href="some link">Link</a>', 'Sanity check' ); + + html = formatter.getCreditHtml( { + repoInfo: { + displayName: 'Localcommons', + getSiteLink: function () { return 'quux'; } + }, + + imageInfo: { + author: 'Author', + source: 'Source', + descriptionShortUrl: 'some link', + title: { + getNameText: function () { return 'Image Title'; } + }, + license: { + getShortLink: function () { return '<a href="http://www.wtfpl.net/">WTFPL v2</a>'; }, + longName: 'Do What the Fuck You Want Public License Version 2', + isFree: this.sandbox.stub().returns( true ) + } + } + } ); + + assert.strictEqual( html, 'By Author - Source, <a href="http://www.wtfpl.net/">WTFPL v2</a>, <a href="some link">Link</a>', 'Sanity check' ); + } ); +}( mediaWiki ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.HtmlUtils.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.HtmlUtils.test.js new file mode 100644 index 00000000..48c0076f --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.HtmlUtils.test.js @@ -0,0 +1,192 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.HtmlUtils', QUnit.newMwEnvironment() ); + + QUnit.test( 'wrapAndJquerify() for single node', function ( assert ) { + var utils = new mw.mmv.HtmlUtils(), + $el = $( '<span>' ), + el = $( '<span>' ).get( 0 ), + html = '<span></span>', + invalid = {}; + + assert.strictEqual( utils.wrapAndJquerify( $el ).html(), '<span></span>', 'jQuery' ); + assert.strictEqual( utils.wrapAndJquerify( el ).html(), '<span></span>', 'HTMLElement' ); + assert.strictEqual( utils.wrapAndJquerify( html ).html(), '<span></span>', 'HTML string' ); + + try { + utils.wrapAndJquerify( invalid ); + } catch ( e ) { + assert.ok( e, 'throws exception for invalid type' ); + } + } ); + + QUnit.test( 'wrapAndJquerify() for multiple nodes', function ( assert ) { + var utils = new mw.mmv.HtmlUtils(), + $el = $( '<span></span><span></span>' ), + html = '<span></span><span></span>'; + + assert.strictEqual( utils.wrapAndJquerify( $el ).html(), '<span></span><span></span>', 'jQuery' ); + assert.strictEqual( utils.wrapAndJquerify( html ).html(), '<span></span><span></span>', 'HTML string' ); + } ); + + QUnit.test( 'wrapAndJquerify() for text', function ( assert ) { + var utils = new mw.mmv.HtmlUtils(), + $el = $( document.createTextNode( 'foo' ) ), + html = 'foo'; + + assert.strictEqual( utils.wrapAndJquerify( $el ).html(), 'foo', 'jQuery' ); + assert.strictEqual( utils.wrapAndJquerify( html ).html(), 'foo', 'HTML string' ); + } ); + + QUnit.test( 'wrapAndJquerify() does not change original', function ( assert ) { + var utils = new mw.mmv.HtmlUtils(), + $el = $( '<span>' ), + el = $( '<span>' ).get( 0 ); + + utils.wrapAndJquerify( $el ).find( 'span' ).prop( 'data-x', 1 ); + utils.wrapAndJquerify( el ).find( 'span' ).prop( 'data-x', 1 ); + assert.strictEqual( $el.prop( 'data-x' ), undefined, 'wrapped jQuery element is not the same as original' ); + assert.strictEqual( $( el ).prop( 'data-x' ), undefined, 'wrapped HTMLElement is not the same as original' ); + } ); + + QUnit.test( 'filterInvisible()', function ( assert ) { + var utils = new mw.mmv.HtmlUtils(), + $visibleChild = $( '<div><span></span></div>' ), + $invisibleChild = $( '<div><span style="display: none"></span></div>' ), + $invisibleChildInVisibleChild = $( '<div><span><abbr style="display: none"></abbr></span></div>' ), + $visibleChildInInvisibleChild = $( '<div><span style="display: none"><abbr></abbr></span></div>' ), + $invisibleChildWithVisibleSiblings = $( '<div><span></span><abbr style="display: none"></abbr><b></b></div>' ); + + utils.filterInvisible( $visibleChild ); + utils.filterInvisible( $invisibleChild ); + utils.filterInvisible( $invisibleChildInVisibleChild ); + utils.filterInvisible( $visibleChildInInvisibleChild ); + utils.filterInvisible( $invisibleChildWithVisibleSiblings ); + + assert.ok( $visibleChild.has( 'span' ).length, 'visible child is not filtered' ); + assert.ok( !$invisibleChild.has( 'span' ).length, 'invisible child is filtered' ); + assert.ok( $invisibleChildInVisibleChild.has( 'span' ).length, 'visible child is not filtered...' ); + assert.ok( !$invisibleChildInVisibleChild.has( 'abbr' ).length, '... but its invisible child is' ); + assert.ok( !$visibleChildInInvisibleChild.has( 'span' ).length, 'invisible child is filtered...' ); + assert.ok( !$visibleChildInInvisibleChild.has( 'abbr' ).length, '...and its children too' ); + assert.ok( $visibleChild.has( 'span' ).length, 'visible child is not filtered' ); + assert.ok( !$invisibleChildWithVisibleSiblings.has( 'abbr' ).length, 'invisible sibling is filtered...' ); + assert.ok( $invisibleChildWithVisibleSiblings.has( 'span' ).length, '...but its visible siblings are not' ); + assert.ok( $invisibleChildWithVisibleSiblings.has( 'b' ).length, '...but its visible siblings are not' ); + } ); + + QUnit.test( 'whitelistHtml()', function ( assert ) { + var utils = new mw.mmv.HtmlUtils(), + $whitelisted = $( '<div>abc<a>def</a>ghi</div>' ), + $nonWhitelisted = $( '<div>abc<span>def</span>ghi</div>' ), + $nonWhitelistedInWhitelisted = $( '<div>abc<a>d<span>e</span>f</a>ghi</div>' ), + $whitelistedInNonWhitelisted = $( '<div>abc<span>d<a>e</a>f</span>ghi</div>' ), + $siblings = $( '<div>ab<span>c</span>d<a>e</a>f<span>g</span>hi</div>' ); + + utils.whitelistHtml( $whitelisted, 'a' ); + utils.whitelistHtml( $nonWhitelisted, 'a' ); + utils.whitelistHtml( $nonWhitelistedInWhitelisted, 'a' ); + utils.whitelistHtml( $whitelistedInNonWhitelisted, 'a' ); + utils.whitelistHtml( $siblings, 'a' ); + + assert.ok( $whitelisted.has( 'a' ).length, 'Whitelisted elements are kept.' ); + assert.ok( !$nonWhitelisted.has( 'span' ).length, 'Non-whitelisted elements are removed.' ); + assert.ok( $nonWhitelistedInWhitelisted.has( 'a' ).length, 'Whitelisted parents are kept.' ); + assert.ok( !$nonWhitelistedInWhitelisted.has( 'span' ).length, 'Non-whitelisted children are removed.' ); + assert.ok( !$whitelistedInNonWhitelisted.has( 'span' ).length, 'Non-whitelisted parents are removed.' ); + assert.ok( $whitelistedInNonWhitelisted.has( 'a' ).length, 'Whitelisted children are kept.' ); + assert.ok( !$siblings.has( 'span' ).length, 'Non-whitelisted siblings are removed.' ); + assert.ok( $siblings.has( 'a' ).length, 'Whitelisted siblings are kept.' ); + } ); + + QUnit.test( 'appendWhitespaceToBlockElements()', function ( assert ) { + var utils = new mw.mmv.HtmlUtils(), + $noBlockElement = $( '<div>abc<i>def</i>ghi</div>' ), + $blockElement = $( '<div>abc<p>def</p>ghi</div>' ), + $linebreak = $( '<div>abc<br>def</div>' ); + + utils.appendWhitespaceToBlockElements( $noBlockElement ); + utils.appendWhitespaceToBlockElements( $blockElement ); + utils.appendWhitespaceToBlockElements( $linebreak ); + + assert.ok( $noBlockElement.text().match( /abcdefghi/ ), 'Non-block elemens are not whitespaced.' ); + assert.ok( $blockElement.text().match( /abc\s+def\s+ghi/ ), 'Block elemens are whitespaced.' ); + assert.ok( $linebreak.text().match( /abc\s+def/ ), 'Linebreaks are whitespaced.' ); + } ); + + QUnit.test( 'jqueryToHtml()', function ( assert ) { + var utils = new mw.mmv.HtmlUtils(); + + assert.strictEqual( utils.jqueryToHtml( $( '<a>' ) ), '<a></a>', + 'works for single element' ); + assert.strictEqual( utils.jqueryToHtml( $( '<b><a>foo</a></b>' ) ), '<b><a>foo</a></b>', + 'works for complex element' ); + assert.strictEqual( utils.jqueryToHtml( $( '<a>foo</a>' ).contents() ), 'foo', + 'works for text nodes' ); + } ); + + QUnit.test( 'mergeWhitespace()', function ( assert ) { + var utils = new mw.mmv.HtmlUtils(); + + assert.strictEqual( utils.mergeWhitespace( ' x \n' ), 'x', + 'leading/trainling whitespace is trimmed' ); + assert.strictEqual( utils.mergeWhitespace( 'x \n\n \n y' ), 'x\ny', + 'whitespace containing a newline is collapsed into a single newline' ); + assert.strictEqual( utils.mergeWhitespace( 'x y' ), 'x y', + 'multiple spaces are collapsed into a single one' ); + } ); + + QUnit.test( 'htmlToText()', function ( assert ) { + var utils = new mw.mmv.HtmlUtils(), + html = '<table><tr><td>Foo</td><td><a>bar</a></td><td style="display: none">baz</td></tr></table>'; + + assert.strictEqual( utils.htmlToText( html ), 'Foo bar', 'works' ); + } ); + + QUnit.test( 'htmlToTextWithLinks()', function ( assert ) { + var utils = new mw.mmv.HtmlUtils(), + html = '<table><tr><td><b>F</b>o<i>o</i></td><td><a>bar</a></td><td style="display: none">baz</td></tr></table>'; + + assert.strictEqual( utils.htmlToTextWithLinks( html ), 'Foo <a>bar</a>', 'works' ); + } ); + + QUnit.test( 'htmlToTextWithTags()', function ( assert ) { + var utils = new mw.mmv.HtmlUtils(), + html = '<table><tr><td><b>F</b>o<i>o</i><sub>o</sub><sup>o</sup></td><td><a>bar</a></td><td style="display: none">baz</td></tr></table>'; + + assert.strictEqual( utils.htmlToTextWithTags( html ), '<b>F</b>o<i>o</i><sub>o</sub><sup>o</sup> <a>bar</a>', 'works' ); + } ); + + QUnit.test( 'isJQueryOrHTMLElement()', function ( assert ) { + var utils = new mw.mmv.HtmlUtils(); + + assert.ok( utils.isJQueryOrHTMLElement( $( '<span>' ) ), 'Recognizes jQuery objects correctly' ); + assert.ok( utils.isJQueryOrHTMLElement( $( '<span>' ).get( 0 ) ), 'Recognizes HTMLElements correctly' ); + assert.ok( !utils.isJQueryOrHTMLElement( '<span></span>' ), 'Recognizes jQuery objects correctly' ); + } ); + + QUnit.test( 'makeLinkText()', function ( assert ) { + var utils = new mw.mmv.HtmlUtils(); + + assert.strictEqual( utils.makeLinkText( 'foo', { + href: 'http://example.com', + title: 'h<b>t</b><i>m</i>l' + } ), '<a href="http://example.com" title="html">foo</a>', 'works' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.ThumbnailWidthCalculator.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.ThumbnailWidthCalculator.test.js new file mode 100644 index 00000000..33484e42 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.ThumbnailWidthCalculator.test.js @@ -0,0 +1,149 @@ +( function ( mw ) { + QUnit.module( 'mmv.ThumbnailWidthCalculator', QUnit.newMwEnvironment() ); + + QUnit.test( 'ThumbnailWidthCalculator constructor sanity check', function ( assert ) { + var badWidthBuckets = [], + goodWidthBuckets = [ 1 ], + thumbnailWidthCalculator; + + thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator(); + assert.ok( thumbnailWidthCalculator, 'constructor with no argument works' ); + + thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( {} ); + assert.ok( thumbnailWidthCalculator, 'constructor with empty option argument works' ); + + thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( { + widthBuckets: goodWidthBuckets + } ); + assert.ok( thumbnailWidthCalculator, 'constructor with non-default buckets works' ); + + try { + thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( { + widthBuckets: badWidthBuckets + } ); + } catch ( e ) { + assert.ok( e, 'constructor with empty bucket list throws exception' ); + } + } ); + + QUnit.test( 'findNextBucket() test', function ( assert ) { + var thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( { + widthBuckets: [ 100, 200 ] + } ); + + assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 50 ), 100, + 'return first bucket for value smaller than all buckets' ); + + assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 300 ), 200, + 'return last bucket for value larger than all buckets' ); + + assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 150 ), 200, + 'return next bucket for value between two buckets' ); + + assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 100 ), 100, + 'return bucket for value equal to that bucket' ); + } ); + + // Old tests for the default bucket sizes. Preserved because why not. + QUnit.test( 'We get sane image sizes when we ask for them', function ( assert ) { + var twc = new mw.mmv.ThumbnailWidthCalculator(); + + assert.strictEqual( twc.findNextBucket( 200 ), 320, 'Low target size gives us lowest possible size bucket' ); + assert.strictEqual( twc.findNextBucket( 320 ), 320, 'Asking for a bucket size gives us exactly that bucket size' ); + assert.strictEqual( twc.findNextBucket( 320.00001 ), 800, 'Asking for greater than an image bucket definitely gives us the next size up' ); + assert.strictEqual( twc.findNextBucket( 2000 ), 2560, 'The image bucketing also works on big screens' ); + assert.strictEqual( twc.findNextBucket( 3000 ), 2880, 'The image bucketing also works on REALLY big screens' ); + } ); + + QUnit.test( 'findNextBucket() test with unordered bucket list', function ( assert ) { + var thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( { + widthBuckets: [ 200, 100 ] + } ); + + assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 50 ), 100, + 'return first bucket for value smaller than all buckets' ); + + assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 300 ), 200, + 'return last bucket for value larger than all buckets' ); + + assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 150 ), 200, + 'return next bucket for value between two buckets' ); + } ); + + QUnit.test( 'calculateFittingWidth() test', function ( assert ) { + var boundingWidth = 100, + boundingHeight = 200, + thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( { widthBuckets: [ 1 ] } ); + + // 50x10 image in 100x200 box - need to scale up 2x + assert.strictEqual( + thumbnailWidthCalculator.calculateFittingWidth( boundingWidth, boundingHeight, 50, 10 ), + 100, 'fit calculation correct when limited by width' ); + + // 10x100 image in 100x200 box - need to scale up 2x + assert.strictEqual( + thumbnailWidthCalculator.calculateFittingWidth( boundingWidth, boundingHeight, 10, 100 ), + 20, 'fit calculation correct when limited by height' ); + + // 10x20 image in 100x200 box - need to scale up 10x + assert.strictEqual( + thumbnailWidthCalculator.calculateFittingWidth( boundingWidth, boundingHeight, 10, 20 ), + 100, 'fit calculation correct when same aspect ratio' ); + } ); + + QUnit.test( 'calculateWidths() test', function ( assert ) { + var boundingWidth = 100, + boundingHeight = 200, + thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( { + widthBuckets: [ 8, 16, 32, 64, 128, 256, 512 ], + devicePixelRatio: 1 + } ), + widths; + + // 50x10 image in 100x200 box - image size should be 100x20, thumbnail should be 128x25.6 + widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 50, 10 ); + assert.strictEqual( widths.cssWidth, 100, 'css width is correct when limited by width' ); + assert.strictEqual( widths.cssHeight, 20, 'css height is correct when limited by width' ); + assert.strictEqual( widths.real, 128, 'real width is correct when limited by width' ); + + // 10x100 image in 100x200 box - image size should be 20x200, thumbnail should be 32x320 + widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 10, 100 ); + assert.strictEqual( widths.cssWidth, 20, 'css width is correct when limited by height' ); + assert.strictEqual( widths.cssHeight, 200, 'css height is correct when limited by width' ); + assert.strictEqual( widths.real, 32, 'real width is correct when limited by height' ); + + // 10x20 image in 100x200 box - image size should be 100x200, thumbnail should be 128x256 + widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 10, 20 ); + assert.strictEqual( widths.cssWidth, 100, 'css width is correct when same aspect ratio' ); + assert.strictEqual( widths.cssHeight, 200, 'css height is correct when limited by width' ); + assert.strictEqual( widths.real, 128, 'real width is correct when same aspect ratio' ); + } ); + + QUnit.test( 'calculateWidths() test with non-standard device pixel ratio', function ( assert ) { + var boundingWidth = 100, + boundingHeight = 200, + thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( { + widthBuckets: [ 8, 16, 32, 64, 128, 256, 512 ], + devicePixelRatio: 2 + } ), + widths; + + // 50x10 image in 100x200 box - image size should be 100x20, thumbnail should be 256x51.2 + widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 50, 10 ); + assert.strictEqual( widths.cssWidth, 100, 'css width is correct when limited by width' ); + assert.strictEqual( widths.cssHeight, 20, 'css height is correct when limited by width' ); + assert.strictEqual( widths.real, 256, 'real width is correct when limited by width' ); + + // 10x100 image in 100x200 box - image size should be 20x200, thumbnail should be 64x640 + widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 10, 100 ); + assert.strictEqual( widths.cssWidth, 20, 'css width is correct when limited by height' ); + assert.strictEqual( widths.cssHeight, 200, 'css height is correct when limited by width' ); + assert.strictEqual( widths.real, 64, 'real width is correct when limited by height' ); + + // 10x20 image in 100x200 box - image size should be 100x200, thumbnail should be 256x512 + widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 10, 20 ); + assert.strictEqual( widths.cssWidth, 100, 'css width is correct when same aspect ratio' ); + assert.strictEqual( widths.cssHeight, 200, 'css height is correct when limited by width' ); + assert.strictEqual( widths.real, 256, 'real width is correct when same aspect ratio' ); + } ); +}( mediaWiki ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.bootstrap.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.bootstrap.test.js new file mode 100644 index 00000000..793f2b52 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.bootstrap.test.js @@ -0,0 +1,582 @@ +( function ( mw, $ ) { + QUnit.module( 'mmv.bootstrap', QUnit.newMwEnvironment( { + setup: function () { + mw.config.set( 'wgMediaViewer', true ); + mw.config.set( 'wgMediaViewerOnClick', true ); + this.sandbox.stub( mw.user, 'isAnon' ).returns( false ); + } + } ) ); + + function createGallery( imageSrc, caption ) { + var $div = $( '<div>' ).addClass( 'gallery' ).appendTo( '#qunit-fixture' ), + $galleryBox = $( '<div>' ).addClass( 'gallerybox' ).appendTo( $div ), + $thumbwrap = $( '<div>' ).addClass( 'thumb' ).appendTo( $galleryBox ), + $link = $( '<a>' ).addClass( 'image' ).appendTo( $thumbwrap ); + + $( '<img>' ).attr( 'src', ( imageSrc || 'thumb.jpg' ) ).appendTo( $link ); + $( '<div>' ).addClass( 'gallerytext' ).text( caption || 'Foobar' ).appendTo( $galleryBox ); + + return $div; + } + + function createThumb( imageSrc, caption, alt ) { + var $div = $( '<div>' ).addClass( 'thumb' ).appendTo( '#qunit-fixture' ), + $link = $( '<a>' ).addClass( 'image' ).appendTo( $div ); + + $( '<div>' ).addClass( 'thumbcaption' ).appendTo( $div ).text( caption ); + $( '<img>' ).attr( 'src', ( imageSrc || 'thumb.jpg' ) ).attr( 'alt', alt ).appendTo( $link ); + + return $div; + } + + function createNormal( imageSrc, caption ) { + var $link = $( '<a>' ).prop( 'title', caption ).addClass( 'image' ).appendTo( '#qunit-fixture' ); + $( '<img>' ).prop( 'src', ( imageSrc || 'thumb.jpg' ) ).appendTo( $link ); + return $link; + } + + function createMultipleImage( images ) { + var i, $div, $thumbimage, $link, + $contain = $( '<div>' ).addClass( 'thumb' ), + $thumbinner = $( '<div>' ).addClass( 'thumbinner' ).appendTo( $contain ); + for ( i = 0; i < images.length; ++i ) { + $div = $( '<div>' ).appendTo( $thumbinner ); + $thumbimage = $( '<div>' ).addClass( 'thumbimage' ).appendTo( $div ); + $link = $( '<a>' ).addClass( 'image' ).appendTo( $thumbimage ); + $( '<img>' ).prop( 'src', images[ i ][ 0 ] ).appendTo( $link ); + $( '<div>' ).addClass( 'thumbcaption' ).text( images[ i ][ 1 ] ).appendTo( $div ); + } + return $contain; + } + + function createBootstrap( viewer ) { + var bootstrap = new mw.mmv.MultimediaViewerBootstrap(); + + bootstrap.processThumbs( $( '#qunit-fixture' ) ); + + // MultimediaViewerBootstrap.ensureEventHandlersAreSetUp() is a weird workaround for gadget bugs. + // MediaViewer should work without it, and so should the tests. + bootstrap.ensureEventHandlersAreSetUp = $.noop; + + bootstrap.getViewer = function () { + return viewer || { initWithThumbs: $.noop, hash: $.noop }; + }; + + return bootstrap; + } + + function hashTest( prefix, bootstrap, assert ) { + var hash = prefix + '/foo', + callCount = 0; + + bootstrap.loadViewer = function () { + callCount++; + return $.Deferred().reject(); + }; + + // Hijack loadViewer, which will return a promise that we'll have to + // wait for if we want to see these tests through + mw.mmv.testHelpers.asyncMethod( bootstrap, 'loadViewer' ); + + bootstrap.setupEventHandlers(); + + // invalid hash, should not trigger MMV load + window.location.hash = 'Foo'; + + // actual hash we want to test for, should trigger MMV load + // use setTimeout to add new hash change to end of the call stack, + // ensuring that event handlers for our previous change can execute + // without us interfering with another immediate change + setTimeout( function () { + window.location.hash = hash; + } ); + + return mw.mmv.testHelpers.waitForAsync().then( function () { + assert.ok( callCount === 1, 'Viewer should be loaded once' ); + bootstrap.cleanupEventHandlers(); + window.location.hash = ''; + } ); + } + + QUnit.test( 'Promise does not hang on ResourceLoader errors', function ( assert ) { + var bootstrap, + errorMessage = 'loading failed', + done = assert.async(); + + this.sandbox.stub( mw.loader, 'using' ) + .callsArgWith( 2, new Error( errorMessage, [ 'mmv' ] ) ) + .withArgs( 'mediawiki.notification' ).returns( $.Deferred().reject() ); // needed for mw.notify + + bootstrap = createBootstrap(); + this.sandbox.stub( bootstrap, 'setupOverlay' ); + this.sandbox.stub( bootstrap, 'cleanupOverlay' ); + + bootstrap.loadViewer( true ).fail( function ( message ) { + assert.ok( bootstrap.setupOverlay.called, 'Overlay was set up' ); + assert.ok( bootstrap.cleanupOverlay.called, 'Overlay was cleaned up' ); + assert.strictEqual( message, errorMessage, 'promise is rejected with the error message when loading fails' ); + done(); + } ); + } ); + + QUnit.test( 'Clicks are not captured once the loading fails', function ( assert ) { + var event, returnValue, + bootstrap = new mw.mmv.MultimediaViewerBootstrap(), + clock = this.sandbox.useFakeTimers(); + + this.sandbox.stub( mw.loader, 'using' ) + .callsArgWith( 2, new Error( 'loading failed', [ 'mmv' ] ) ) + .withArgs( 'mediawiki.notification' ).returns( $.Deferred().reject() ); // needed for mw.notify + bootstrap.ensureEventHandlersAreSetUp = $.noop; + + // trigger first click, which will cause MMV to be loaded (which we've + // set up to fail) + event = new $.Event( 'click', { button: 0, which: 1 } ); + returnValue = bootstrap.click( {}, event, 'foo' ); + clock.tick( 10 ); + assert.ok( event.isDefaultPrevented(), 'First click is caught' ); + assert.strictEqual( returnValue, false, 'First click is caught' ); + + // wait until MMW is loaded (or failed to load, in this case) before we + // trigger another click - which should then not be caught + event = new $.Event( 'click', { button: 0, which: 1 } ); + returnValue = bootstrap.click( {}, event, 'foo' ); + clock.tick( 10 ); + assert.ok( !event.isDefaultPrevented(), 'Click after loading failure is not caught' ); + assert.notStrictEqual( returnValue, false, 'Click after loading failure is not caught' ); + + clock.restore(); + } ); + + /* FIXME: Tests suspended as they do not pass in QUnit 2.x+ – T192932 + QUnit.test( 'Check viewer invoked when clicking on valid image links', function ( assert ) { + // TODO: Is <div class="gallery"><span class="image"><img/></span></div> valid ??? + var div, link, link2, link3, link4, link5, bootstrap, + viewer = { initWithThumbs: $.noop, loadImageByTitle: this.sandbox.stub() }, + clock = this.sandbox.useFakeTimers(); + + // Create gallery with valid link image + div = createGallery(); + link = div.find( 'a.image' ); + + // Valid isolated thumbnail + link2 = $( '<a>' ).addClass( 'image' ).appendTo( '#qunit-fixture' ); + $( '<img>' ).attr( 'src', 'thumb2.jpg' ).appendTo( link2 ); + + // Non-valid fragment + link3 = $( '<a>' ).addClass( 'noImage' ).appendTo( div ); + $( '<img>' ).attr( 'src', 'thumb3.jpg' ).appendTo( link3 ); + + mw.config.set( 'wgTitle', 'Thumb4.jpg' ); + mw.config.set( 'wgNamespaceNumber', 6 ); + $( '<div>' ).addClass( 'fullMedia' ).appendTo( div ); + $( '<img>' ).attr( 'src', 'thumb4.jpg' ).appendTo( + $( '<a>' ) + .appendTo( + $( '<div>' ) + .attr( 'id', 'file' ) + .appendTo( '#qunit-fixture' ) + ) + ); + + // Create a new bootstrap object to trigger the DOM scan, etc. + bootstrap = createBootstrap( viewer ); + this.sandbox.stub( bootstrap, 'setupOverlay' ); + + link4 = $( '.fullMedia .mw-mmv-view-expanded' ); + assert.ok( link4.length, 'Link for viewing expanded file was set up.' ); + + link5 = $( '.fullMedia .mw-mmv-view-config' ); + assert.ok( link5.length, 'Link for opening enable/disable configuration was set up.' ); + + // Click on valid link + link.trigger( { type: 'click', which: 1 } ); + clock.tick( 10 ); + // FIXME: Actual bootstrap.setupOverlay.callCount: 2 + assert.equal( bootstrap.setupOverlay.callCount, 1, 'setupOverlay called (1st click)' ); + assert.equal( viewer.loadImageByTitle.callCount, 1, 'loadImageByTitle called (1st click)' ); + this.sandbox.reset(); + + // Click on valid link + link2.trigger( { type: 'click', which: 1 } ); + clock.tick( 10 ); + assert.equal( bootstrap.setupOverlay.callCount, 1, 'setupOverlay called (2nd click)' ); + assert.equal( viewer.loadImageByTitle.callCount, 1, 'loadImageByTitle called (2nd click)' ); + this.sandbox.reset(); + + // Click on valid link + link4.trigger( { type: 'click', which: 1 } ); + clock.tick( 10 ); + assert.equal( bootstrap.setupOverlay.callCount, 1, 'setupOverlay called (3rd click)' ); + assert.equal( viewer.loadImageByTitle.callCount, 1, 'loadImageByTitle called (3rd click)' ); + this.sandbox.reset(); + + // Click on valid link even when preference says not to + mw.config.set( 'wgMediaViewerOnClick', false ); + link4.trigger( { type: 'click', which: 1 } ); + clock.tick( 10 ); + mw.config.set( 'wgMediaViewerOnClick', true ); + assert.equal( bootstrap.setupOverlay.callCount, 1, 'setupOverlay called on-click with pref off' ); + assert.equal( viewer.loadImageByTitle.callCount, 1, 'loadImageByTitle called on-click with pref off' ); + this.sandbox.reset(); + + // @todo comment that above clicks should result in call, below clicks should not + + // Click on non-valid link + link3.trigger( { type: 'click', which: 1 } ); + clock.tick( 10 ); + assert.equal( bootstrap.setupOverlay.callCount, 0, 'setupOverlay not called on non-valid link click' ); + assert.equal( viewer.loadImageByTitle.callCount, 0, 'loadImageByTitle not called on non-valid link click' ); + this.sandbox.reset(); + + // Click on valid links with preference off + mw.config.set( 'wgMediaViewerOnClick', false ); + link.trigger( { type: 'click', which: 1 } ); + link2.trigger( { type: 'click', which: 1 } ); + clock.tick( 10 ); + assert.equal( bootstrap.setupOverlay.callCount, 0, 'setupOverlay not called on non-valid link click with pref off' ); + assert.equal( viewer.loadImageByTitle.callCount, 0, 'loadImageByTitle not called on non-valid link click with pref off' ); + + clock.restore(); + } ); + */ + + QUnit.test( 'Skip images with invalid extensions', function ( assert ) { + var div, link, + viewer = { initWithThumbs: $.noop, loadImageByTitle: this.sandbox.stub() }, + clock = this.sandbox.useFakeTimers(); + + // Create gallery with image that has invalid name extension + div = createGallery( 'thumb.badext' ); + link = div.find( 'a.image' ); + + // Create a new bootstrap object to trigger the DOM scan, etc. + createBootstrap( viewer ); + + // Click on valid link with wrong image extension + link.trigger( { type: 'click', which: 1 } ); + clock.tick( 10 ); + + assert.ok( !viewer.loadImageByTitle.called, 'Image should not be loaded' ); + + clock.restore(); + } ); + + /* FIXME: Tests suspended as they do not pass in QUnit 2.x+ – T192932 + QUnit.test( 'Accept only left clicks without modifier keys, skip the rest', function ( assert ) { + var $div, $link, bootstrap, + viewer = { initWithThumbs: $.noop, loadImageByTitle: this.sandbox.stub() }, + clock = this.sandbox.useFakeTimers(); + + // Create gallery with image that has valid name extension + $div = createGallery(); + + // Create a new bootstrap object to trigger the DOM scan, etc. + bootstrap = createBootstrap( viewer ); + this.sandbox.stub( bootstrap, 'setupOverlay' ); + + $link = $div.find( 'a.image' ); + + // Handle valid left click, it should try to load the image + $link.trigger( { type: 'click', which: 1 } ); + clock.tick( 10 ); + + // FIXME: Actual bootstrap.setupOverlay.callCount: 2 + assert.equal( bootstrap.setupOverlay.callCount, 1, 'Left-click: Set up overlay' ); + assert.equal( viewer.loadImageByTitle.callCount, 1, 'Left-click: Load image' ); + this.sandbox.reset(); + + // Skip Ctrl-left-click, no image is loaded + $link.trigger( { type: 'click', which: 1, ctrlKey: true } ); + clock.tick( 10 ); + assert.equal( bootstrap.setupOverlay.callCount, 0, 'Ctrl-left-click: No overlay' ); + assert.equal( viewer.loadImageByTitle.callCount, 0, 'Ctrl-left-click: No image load' ); + this.sandbox.reset(); + + // Skip invalid right click, no image is loaded + $link.trigger( { type: 'click', which: 2 } ); + clock.tick( 10 ); + assert.equal( bootstrap.setupOverlay.callCount, 0, 'Right-click: No overlay' ); + assert.equal( viewer.loadImageByTitle.callCount, 0, 'Right-click: Image was not loaded' ); + + clock.restore(); + } ); + */ + + QUnit.test( 'Ensure that the correct title is loaded when clicking', function ( assert ) { + var bootstrap, + viewer = { initWithThumbs: $.noop, loadImageByTitle: this.sandbox.stub() }, + $div = createGallery( 'foo.jpg' ), + $link = $div.find( 'a.image' ), + clock = this.sandbox.useFakeTimers(); + + // Create a new bootstrap object to trigger the DOM scan, etc. + bootstrap = createBootstrap( viewer ); + this.sandbox.stub( bootstrap, 'setupOverlay' ); + + $link.trigger( { type: 'click', which: 1 } ); + clock.tick( 10 ); + assert.ok( bootstrap.setupOverlay.called, 'Overlay was set up' ); + assert.strictEqual( viewer.loadImageByTitle.firstCall.args[ 0 ].getPrefixedDb(), 'File:Foo.jpg', 'Titles are identical' ); + + clock.restore(); + } ); + + /* FIXME: Tests suspended as they do not pass in QUnit 2.x+ – T192932 + QUnit.test( 'Validate new LightboxImage object has sane constructor parameters', function ( assert ) { + var bootstrap, + $div, + $link, + viewer = mw.mmv.testHelpers.getMultimediaViewer(), + fname = 'valid', + imgSrc = '/' + fname + '.jpg/300px-' + fname + '.jpg', + imgRegex = new RegExp( imgSrc + '$' ), + clock = this.sandbox.useFakeTimers(); + + $div = createThumb( imgSrc, 'Blah blah', 'meow' ); + $link = $div.find( 'a.image' ); + + // Create a new bootstrap object to trigger the DOM scan, etc. + bootstrap = createBootstrap( viewer ); + this.sandbox.stub( bootstrap, 'setupOverlay' ); + this.sandbox.stub( viewer, 'createNewImage' ); + viewer.loadImage = $.noop; + viewer.createNewImage = function ( fileLink, filePageLink, fileTitle, index, thumb, caption, alt ) { + var html = thumb.outerHTML; + + // FIXME: fileLink doesn't match imgRegex (null) + assert.ok( fileLink.match( imgRegex ), 'Thumbnail URL used in creating new image object' ); + assert.strictEqual( filePageLink, '', 'File page link is sane when creating new image object' ); + assert.strictEqual( fileTitle.title, fname, 'Filename is correct when passed into new image constructor' ); + assert.strictEqual( index, 0, 'The only image we created in the gallery is set at index 0 in the images array' ); + assert.ok( html.indexOf( ' src="' + imgSrc + '"' ) > 0, 'The image element passed in contains the src=... we want.' ); + assert.ok( html.indexOf( ' alt="meow"' ) > 0, 'The image element passed in contains the alt=... we want.' ); + assert.strictEqual( caption, 'Blah blah', 'The caption passed in is correct' ); + assert.strictEqual( alt, 'meow', 'The alt text passed in is correct' ); + }; + + $link.trigger( { type: 'click', which: 1 } ); + clock.tick( 10 ); + assert.equal( bootstrap.setupOverlay.callCount, 1, 'Overlay was set up' ); + + clock.reset(); + } ); + */ + + QUnit.test( 'Only load the viewer on a valid hash (modern browsers)', function ( assert ) { + var bootstrap; + + window.location.hash = ''; + + bootstrap = createBootstrap(); + + return hashTest( '/media', bootstrap, assert ); + } ); + + QUnit.test( 'Only load the viewer on a valid hash (old browsers)', function ( assert ) { + var bootstrap; + + window.location.hash = ''; + + bootstrap = createBootstrap(); + bootstrap.browserHistory = undefined; + + return hashTest( '/media', bootstrap, assert ); + } ); + + QUnit.test( 'Load the viewer on a legacy hash (modern browsers)', function ( assert ) { + var bootstrap; + + window.location.hash = ''; + + bootstrap = createBootstrap(); + + return hashTest( 'mediaviewer', bootstrap, assert ); + } ); + + QUnit.test( 'Load the viewer on a legacy hash (old browsers)', function ( assert ) { + var bootstrap; + + window.location.hash = ''; + + bootstrap = createBootstrap(); + bootstrap.browserHistory = undefined; + + return hashTest( 'mediaviewer', bootstrap, assert ); + } ); + + QUnit.test( 'Overlay is set up on hash change', function ( assert ) { + var bootstrap; + + window.location.hash = '#/media/foo'; + + bootstrap = createBootstrap(); + this.sandbox.stub( bootstrap, 'setupOverlay' ); + + bootstrap.hash(); + + assert.ok( bootstrap.setupOverlay.called, 'Overlay is set up' ); + } ); + + QUnit.test( 'Overlay is not set up on an irrelevant hash change', function ( assert ) { + var bootstrap; + + window.location.hash = '#foo'; + + bootstrap = createBootstrap(); + this.sandbox.stub( bootstrap, 'setupOverlay' ); + bootstrap.loadViewer(); + bootstrap.setupOverlay.reset(); + + bootstrap.hash(); + + assert.ok( !bootstrap.setupOverlay.called, 'Overlay is not set up' ); + } ); + + QUnit.test( 'internalHashChange', function ( assert ) { + var bootstrap = createBootstrap(), + hash = '#/media/foo', + callCount = 0, + clock = this.sandbox.useFakeTimers(); + + window.location.hash = ''; + + bootstrap.loadViewer = function () { + callCount++; + return $.Deferred().reject(); + }; + + bootstrap.setupEventHandlers(); + + bootstrap.internalHashChange( { hash: hash } ); + clock.tick( 10 ); + + assert.ok( callCount === 0, 'Viewer should not be loaded' ); + assert.strictEqual( window.location.hash, hash, 'Window\'s hash has been updated correctly' ); + + bootstrap.cleanupEventHandlers(); + window.location.hash = ''; + clock.restore(); + } ); + + QUnit.test( 'internalHashChange (legacy)', function ( assert ) { + var bootstrap = createBootstrap(), + hash = '#mediaviewer/foo', + callCount = 0, + clock = this.sandbox.useFakeTimers(); + + window.location.hash = ''; + + bootstrap.loadViewer = function () { + callCount++; + return $.Deferred().reject(); + }; + + bootstrap.setupEventHandlers(); + + bootstrap.internalHashChange( { hash: hash } ); + clock.tick( 10 ); + + assert.ok( callCount === 0, 'Viewer should not be loaded' ); + assert.strictEqual( window.location.hash, hash, 'Window\'s hash has been updated correctly' ); + + bootstrap.cleanupEventHandlers(); + window.location.hash = ''; + clock.restore(); + } ); + + QUnit.test( 'Restoring article scroll position', function ( assert ) { + var stubbedScrollTop, + bootstrap = createBootstrap(), + $window = $( window ), + done = assert.async(); + + this.sandbox.stub( $.fn, 'scrollTop', function ( scrollTop ) { + if ( scrollTop !== undefined ) { + stubbedScrollTop = scrollTop; + return this; + } else { + return stubbedScrollTop; + } + } ); + + $window.scrollTop( 50 ); + bootstrap.setupOverlay(); + // Calling this a second time because it can happen in history navigation context + bootstrap.setupOverlay(); + // Clear scrollTop to check it is restored + $window.scrollTop( 0 ); + bootstrap.cleanupOverlay(); + + // Scroll restoration is on a setTimeout + setTimeout( function () { + assert.strictEqual( $( window ).scrollTop(), 50, 'Scroll is correctly reset to original top position' ); + done(); + } ); + } ); + + QUnit.test( 'Preload JS/CSS dependencies on thumb hover', function ( assert ) { + var $div, bootstrap, + clock = this.sandbox.useFakeTimers(), + viewer = { initWithThumbs: $.noop }; + + // Create gallery with image that has valid name extension + $div = createThumb(); + + // Create a new bootstrap object to trigger the DOM scan, etc. + bootstrap = createBootstrap( viewer ); + + this.sandbox.stub( mw.loader, 'load' ); + + $div.mouseenter(); + clock.tick( bootstrap.hoverWaitDuration - 50 ); + $div.mouseleave(); + + assert.ok( !mw.loader.load.called, 'Dependencies should not be preloaded if the thumb is not hovered long enough' ); + + $div.mouseenter(); + clock.tick( bootstrap.hoverWaitDuration + 50 ); + $div.mouseleave(); + + assert.ok( mw.loader.load.called, 'Dependencies should be preloaded if the thumb is hovered long enough' ); + + clock.restore(); + } ); + + QUnit.test( 'isAllowedThumb', function ( assert ) { + var $container = $( '<div>' ), + $thumb = $( '<img>' ).appendTo( $container ), + bootstrap = createBootstrap(); + + assert.ok( bootstrap.isAllowedThumb( $thumb ), 'Normal image in a div is allowed.' ); + + $container.addClass( 'metadata' ); + assert.strictEqual( bootstrap.isAllowedThumb( $thumb ), false, 'Image in a metadata container is disallowed.' ); + + $container.prop( 'class', '' ); + $container.addClass( 'noviewer' ); + assert.strictEqual( bootstrap.isAllowedThumb( $thumb ), false, 'Image in a noviewer container is disallowed.' ); + + $container.prop( 'class', '' ); + $container.addClass( 'noarticletext' ); + assert.strictEqual( bootstrap.isAllowedThumb( $thumb ), false, 'Image in an empty article is disallowed.' ); + + $container.prop( 'class', '' ); + $thumb.addClass( 'noviewer' ); + assert.strictEqual( bootstrap.isAllowedThumb( $thumb ), false, 'Image with a noviewer class is disallowed.' ); + } ); + + QUnit.test( 'findCaption', function ( assert ) { + var gallery = createGallery( 'foo.jpg', 'Baz' ), + thumb = createThumb( 'foo.jpg', 'Quuuuux' ), + link = createNormal( 'foo.jpg', 'Foobar' ), + multiple = createMultipleImage( [ [ 'foo.jpg', 'Image #1' ], [ 'bar.jpg', 'Image #2' ], + [ 'foobar.jpg', 'Image #3' ] ] ), + bootstrap = createBootstrap(); + + assert.strictEqual( bootstrap.findCaption( gallery.find( '.thumb' ), gallery.find( 'a.image' ) ), 'Baz', 'A gallery caption is found.' ); + assert.strictEqual( bootstrap.findCaption( thumb, thumb.find( 'a.image' ) ), 'Quuuuux', 'A thumbnail caption is found.' ); + assert.strictEqual( bootstrap.findCaption( $(), link ), 'Foobar', 'The caption is found even if the image is not a thumbnail.' ); + assert.strictEqual( bootstrap.findCaption( multiple, multiple.find( 'img[src="bar.jpg"]' ).closest( 'a' ) ), 'Image #2', 'The caption is found in {{Multiple image}}.' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboximage.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboximage.test.js new file mode 100644 index 00000000..ce824707 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboximage.test.js @@ -0,0 +1,10 @@ +( function ( mw ) { + QUnit.module( 'mmv.lightboximage', QUnit.newMwEnvironment() ); + + QUnit.test( 'Sanity test, object creation', function ( assert ) { + var lightboxImage = new mw.mmv.LightboxImage( 'foo.png' ); + + assert.ok( lightboxImage, 'Object created !' ); + } ); + +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboxinterface.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboxinterface.test.js new file mode 100644 index 00000000..1f85eeca --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboxinterface.test.js @@ -0,0 +1,306 @@ +( function ( mw, $ ) { + var oldScrollTo; + + function stubScrollTo() { + oldScrollTo = $.scrollTo; + $.scrollTo = function () { return { scrollTop: $.noop, on: $.noop, off: $.noop }; }; + } + + function restoreScrollTo() { + $.scrollTo = oldScrollTo; + } + + QUnit.module( 'mmv.lightboxInterface', QUnit.newMwEnvironment( { + setup: function () { + // animation would keep running, conflict with other tests + this.sandbox.stub( $.fn, 'animate' ).returnsThis(); + } + } ) ); + + QUnit.test( 'Sanity test, object creation and ui construction', function ( assert ) { + var lightbox = new mw.mmv.LightboxInterface(); + + stubScrollTo(); + + function checkIfUIAreasAttachedToDocument( inDocument ) { + var msg = ( inDocument === 1 ? ' ' : ' not ' ) + 'attached.'; + assert.strictEqual( $( '.mw-mmv-wrapper' ).length, inDocument, 'Wrapper area' + msg ); + assert.strictEqual( $( '.mw-mmv-main' ).length, inDocument, 'Main area' + msg ); + assert.strictEqual( $( '.mw-mmv-title' ).length, inDocument, 'Title area' + msg ); + assert.strictEqual( $( '.mw-mmv-credit' ).length, inDocument, 'Author/source area' + msg ); + assert.strictEqual( $( '.mw-mmv-image-desc' ).length, inDocument, 'Description area' + msg ); + assert.strictEqual( $( '.mw-mmv-image-links' ).length, inDocument, 'Links area' + msg ); + } + + // UI areas not attached to the document yet. + checkIfUIAreasAttachedToDocument( 0 ); + + // Attach lightbox to testing fixture to avoid interference with other tests. + lightbox.attach( '#qunit-fixture' ); + + // UI areas should now be attached to the document. + checkIfUIAreasAttachedToDocument( 1 ); + + // Check that the close button on the lightbox still follow the spec (being visible right away) + assert.strictEqual( $( '#qunit-fixture .mw-mmv-close' ).length, 1, 'There should be a close button' ); + assert.ok( $( '#qunit-fixture .mw-mmv-close' ).is( ':visible' ), 'The close button should be visible' ); + + // Unattach lightbox from document + lightbox.unattach(); + + // UI areas not attached to the document anymore. + checkIfUIAreasAttachedToDocument( 0 ); + + restoreScrollTo(); + } ); + + QUnit.test( 'Handler registration and clearance work OK', function ( assert ) { + var lightbox = new mw.mmv.LightboxInterface(), + handlerCalls = 0, + clock = this.sandbox.useFakeTimers(); + + function handleEvent() { + handlerCalls++; + } + + lightbox.handleEvent( 'test', handleEvent ); + $( document ).trigger( 'test' ); + clock.tick( 10 ); + assert.strictEqual( handlerCalls, 1, 'The handler was called when we triggered the event.' ); + + lightbox.clearEvents(); + + $( document ).trigger( 'test' ); + clock.tick( 10 ); + assert.strictEqual( handlerCalls, 1, 'The handler was not called after calling lightbox.clearEvents().' ); + + clock.restore(); + } ); + + QUnit.test( 'Fullscreen mode', function ( assert ) { + var lightbox = new mw.mmv.LightboxInterface(), + oldFnEnterFullscreen = $.fn.enterFullscreen, + oldFnExitFullscreen = $.fn.exitFullscreen, + oldSupportFullscreen = $.support.fullscreen; + + // Since we don't want these tests to really open fullscreen + // which is subject to user security confirmation, + // we use a mock that pretends regular jquery.fullscreen behavior happened + $.fn.enterFullscreen = mw.mmv.testHelpers.enterFullscreenMock; + $.fn.exitFullscreen = mw.mmv.testHelpers.exitFullscreenMock; + + stubScrollTo(); + + lightbox.buttons.fadeOut = $.noop; + + // Attach lightbox to testing fixture to avoid interference with other tests. + lightbox.attach( '#qunit-fixture' ); + + $.support.fullscreen = false; + lightbox.setupCanvasButtons(); + + assert.strictEqual( lightbox.$fullscreenButton.css( 'display' ), 'none', + 'Fullscreen button is hidden when fullscreen mode is unavailable' ); + + $.support.fullscreen = true; + lightbox.setupCanvasButtons(); + + assert.strictEqual( lightbox.$fullscreenButton.css( 'display' ), '', + 'Fullscreen button is visible when fullscreen mode is available' ); + + // Entering fullscreen + lightbox.$fullscreenButton.click(); + + assert.strictEqual( lightbox.$main.hasClass( 'jq-fullscreened' ), true, + 'Fullscreened area has the fullscreen class' ); + assert.strictEqual( lightbox.isFullscreen, true, 'Lightbox knows it\'s in fullscreen mode' ); + + // Exiting fullscreen + lightbox.$fullscreenButton.click(); + + assert.strictEqual( lightbox.$main.hasClass( 'jq-fullscreened' ), false, + 'Fullscreened area doesn\'t have the fullscreen class anymore' ); + assert.strictEqual( lightbox.isFullscreen, false, 'Lightbox knows it\'s not in fullscreen mode' ); + + // Entering fullscreen + lightbox.$fullscreenButton.click(); + + // Hard-exiting fullscreen + lightbox.$closeButton.click(); + + // Re-attach after hard-exit + lightbox.attach( '#qunit-fixture' ); + + assert.strictEqual( lightbox.$main.hasClass( 'jq-fullscreened' ), false, + 'Fullscreened area doesn\'t have the fullscreen class anymore' ); + assert.strictEqual( lightbox.isFullscreen, false, 'Lightbox knows it\'s not in fullscreen mode' ); + + // Unattach lightbox from document + lightbox.unattach(); + + $.fn.enterFullscreen = oldFnEnterFullscreen; + $.fn.exitFullscreen = oldFnExitFullscreen; + $.support.fullscreen = oldSupportFullscreen; + restoreScrollTo(); + } ); + + QUnit.test( 'Fullscreen mode', function ( assert ) { + var buttonOffset, panelBottom, + oldRevealButtonsAndFadeIfNeeded, + lightbox = new mw.mmv.LightboxInterface(), + viewer = mw.mmv.testHelpers.getMultimediaViewer(), + oldFnEnterFullscreen = $.fn.enterFullscreen, + oldFnExitFullscreen = $.fn.exitFullscreen; + + stubScrollTo(); + + // ugly hack to avoid preloading which would require lightbox list being set up + viewer.preloadDistance = -1; + + // Since we don't want these tests to really open fullscreen + // which is subject to user security confirmation, + // we use a mock that pretends regular jquery.fullscreen behavior happened + $.fn.enterFullscreen = mw.mmv.testHelpers.enterFullscreenMock; + $.fn.exitFullscreen = mw.mmv.testHelpers.exitFullscreenMock; + + // Attach lightbox to testing fixture to avoid interference with other tests. + lightbox.attach( '#qunit-fixture' ); + viewer.ui = lightbox; + viewer.ui = lightbox; + + assert.ok( !lightbox.isFullscreen, 'Lightbox knows that it\'s not in fullscreen mode' ); + assert.ok( lightbox.panel.$imageMetadata.is( ':visible' ), 'Image metadata is visible' ); + + lightbox.buttons.fadeOut = function () { + assert.ok( true, 'Opening fullscreen triggers a fadeout' ); + }; + + // Pretend that the mouse cursor is on top of the button + buttonOffset = lightbox.buttons.$fullscreen.offset(); + lightbox.mousePosition = { x: buttonOffset.left, y: buttonOffset.top }; + + // Enter fullscreen + lightbox.buttons.$fullscreen.click(); + + lightbox.buttons.fadeOut = $.noop; + assert.ok( lightbox.isFullscreen, 'Lightbox knows that it\'s in fullscreen mode' ); + + oldRevealButtonsAndFadeIfNeeded = lightbox.buttons.revealAndFade; + + lightbox.buttons.revealAndFade = function ( position ) { + assert.ok( true, 'Moving the cursor triggers a reveal + fade' ); + + oldRevealButtonsAndFadeIfNeeded.call( this, position ); + }; + + // Pretend that the mouse cursor moved to the top-left corner + lightbox.mousemove( { pageX: 0, pageY: 0 } ); + + lightbox.buttons.revealAndFadeIfNeeded = $.noop; + + panelBottom = $( '.mw-mmv-post-image' ).position().top + $( '.mw-mmv-post-image' ).height(); + + assert.ok( panelBottom === $( window ).height(), 'Image metadata does not extend beyond the viewport' ); + + lightbox.buttons.revealAndFade = function ( position ) { + assert.ok( true, 'Closing fullscreen triggers a reveal + fade' ); + + oldRevealButtonsAndFadeIfNeeded.call( this, position ); + }; + + // Exiting fullscreen + lightbox.buttons.$fullscreen.click(); + + panelBottom = $( '.mw-mmv-post-image' ).position().top + $( '.mw-mmv-post-image' ).height(); + + assert.ok( panelBottom > $( window ).height(), 'Image metadata extends beyond the viewport' ); + assert.ok( !lightbox.isFullscreen, 'Lightbox knows that it\'s not in fullscreen mode' ); + + // Unattach lightbox from document + lightbox.unattach(); + + $.fn.enterFullscreen = oldFnEnterFullscreen; + $.fn.exitFullscreen = oldFnExitFullscreen; + restoreScrollTo(); + } ); + + QUnit.test( 'isAnyActiveButtonHovered', function ( assert ) { + var lightbox = new mw.mmv.LightboxInterface(); + + stubScrollTo(); + + // Attach lightbox to testing fixture to avoid interference with other tests. + lightbox.attach( '#qunit-fixture' ); + + $.each( lightbox.buttons.$buttons, function ( idx, e ) { + var $e = $( e ), + offset = $e.show().offset(), + width = $e.width(), + height = $e.height(), + disabled = $e.hasClass( 'disabled' ); + + assert.strictEqual( lightbox.buttons.isAnyActiveButtonHovered( offset.left, offset.top ), + !disabled, + 'Hover detection works for top-left corner of element' ); + assert.strictEqual( lightbox.buttons.isAnyActiveButtonHovered( offset.left + width, offset.top ), + !disabled, + 'Hover detection works for top-right corner of element' ); + assert.strictEqual( lightbox.buttons.isAnyActiveButtonHovered( offset.left, offset.top + height ), + !disabled, + 'Hover detection works for bottom-left corner of element' ); + assert.strictEqual( lightbox.buttons.isAnyActiveButtonHovered( offset.left + width, offset.top + height ), + !disabled, + 'Hover detection works for bottom-right corner of element' ); + assert.strictEqual( + lightbox.buttons.isAnyActiveButtonHovered( + offset.left + ( width / 2 ), offset.top + ( height / 2 ) + ), + !disabled, + 'Hover detection works for center of element' + ); + } ); + + // Unattach lightbox from document + lightbox.unattach(); + restoreScrollTo(); + } ); + + QUnit.test( 'Keyboard prev/next', function ( assert ) { + var viewer = mw.mmv.testHelpers.getMultimediaViewer(), + lightbox = new mw.mmv.LightboxInterface(); + + viewer.setupEventHandlers(); + + // Since we define both, the test works regardless of RTL settings + lightbox.on( 'next', function () { + assert.ok( true, 'Next image was open' ); + } ); + + lightbox.on( 'prev', function () { + assert.ok( true, 'Prev image was open' ); + } ); + + // 37 is left arrow, 39 is right arrow + lightbox.keydown( $.Event( 'keydown', { which: 37 } ) ); + lightbox.keydown( $.Event( 'keydown', { which: 39 } ) ); + + lightbox.off( 'next' ).on( 'next', function () { + assert.ok( false, 'Next image should not have been open' ); + } ); + + lightbox.off( 'prev' ).on( 'prev', function () { + assert.ok( false, 'Prev image should not have been open' ); + } ); + + lightbox.keydown( $.Event( 'keydown', { which: 37, altKey: true } ) ); + lightbox.keydown( $.Event( 'keydown', { which: 39, altKey: true } ) ); + lightbox.keydown( $.Event( 'keydown', { which: 37, ctrlKey: true } ) ); + lightbox.keydown( $.Event( 'keydown', { which: 39, ctrlKey: true } ) ); + lightbox.keydown( $.Event( 'keydown', { which: 37, shiftKey: true } ) ); + lightbox.keydown( $.Event( 'keydown', { which: 39, shiftKey: true } ) ); + lightbox.keydown( $.Event( 'keydown', { which: 37, metaKey: true } ) ); + lightbox.keydown( $.Event( 'keydown', { which: 39, metaKey: true } ) ); + + viewer.cleanupEventHandlers(); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.test.js new file mode 100644 index 00000000..95a01c36 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.test.js @@ -0,0 +1,706 @@ +( function ( mw, $ ) { + QUnit.module( 'mmv', QUnit.newMwEnvironment() ); + + QUnit.test( 'eachPrealoadableLightboxIndex()', function ( assert ) { + var viewer = mw.mmv.testHelpers.getMultimediaViewer(), + expectedIndices, + i; + + viewer.preloadDistance = 3; + viewer.thumbs = []; + + // 0..10 + for ( i = 0; i < 11; i++ ) { + viewer.thumbs.push( { image: false } ); + } + + viewer.currentIndex = 2; + i = 0; + expectedIndices = [ 2, 3, 1, 4, 0, 5 ]; + viewer.eachPrealoadableLightboxIndex( function ( index ) { + assert.strictEqual( index, expectedIndices[ i++ ], 'preload on left edge' ); + } ); + + viewer.currentIndex = 9; + i = 0; + expectedIndices = [ 9, 10, 8, 7, 6 ]; + viewer.eachPrealoadableLightboxIndex( function ( index ) { + assert.strictEqual( index, expectedIndices[ i++ ], 'preload on right edge' ); + } ); + } ); + + QUnit.test( 'Hash handling', function ( assert ) { + var oldUnattach, + viewer = mw.mmv.testHelpers.getMultimediaViewer(), + ui = new mw.mmv.LightboxInterface(), + imageSrc = 'Foo bar.jpg', + image = { filePageTitle: new mw.Title( 'File:' + imageSrc ) }; + + // animation would keep running, conflict with other tests + this.sandbox.stub( $.fn, 'animate' ).returnsThis(); + + window.location.hash = ''; + + viewer.setupEventHandlers(); + oldUnattach = ui.unattach; + + ui.unattach = function () { + assert.ok( true, 'Lightbox was unattached' ); + oldUnattach.call( this ); + }; + + viewer.ui = ui; + viewer.close(); + + assert.ok( !viewer.isOpen, 'Viewer is closed' ); + + viewer.isOpen = true; + + // Verify that passing an invalid mmv hash when the mmv is open triggers unattach() + window.location.hash = 'Foo'; + viewer.hash(); + + // Verify that mmv doesn't reset a foreign hash + assert.strictEqual( window.location.hash, '#Foo', 'Foreign hash remains intact' ); + assert.ok( !viewer.isOpen, 'Viewer is closed' ); + + ui.unattach = function () { + assert.ok( false, 'Lightbox was not unattached' ); + oldUnattach.call( this ); + }; + + // Verify that passing an invalid mmv hash when the mmv is closed doesn't trigger unattach() + window.location.hash = 'Bar'; + viewer.hash(); + + // Verify that mmv doesn't reset a foreign hash + assert.strictEqual( window.location.hash, '#Bar', 'Foreign hash remains intact' ); + + viewer.ui = { images: [ image ], disconnect: $.noop }; + + $( '#qunit-fixture' ).append( '<a class="image"><img src="' + imageSrc + '"></a>' ); + + viewer.loadImageByTitle = function ( title ) { + assert.strictEqual( title.getPrefixedText(), 'File:' + imageSrc, 'The title matches' ); + }; + + // Open a valid mmv hash link and check that the right image is requested. + // imageSrc contains a space without any encoding on purpose + window.location.hash = '/media/File:' + imageSrc; + viewer.hash(); + + // Reset the hash, because for some browsers switching from the non-URI-encoded to + // the non-URI-encoded version of the same text with a space will not trigger a hash change + window.location.hash = ''; + viewer.hash(); + + // Try again with an URI-encoded imageSrc containing a space + window.location.hash = '/media/File:' + encodeURIComponent( imageSrc ); + viewer.hash(); + + // Reset the hash + window.location.hash = ''; + viewer.hash(); + + // Try again with a legacy hash + window.location.hash = 'mediaviewer/File:' + imageSrc; + viewer.hash(); + + viewer.cleanupEventHandlers(); + + window.location.hash = ''; + } ); + + QUnit.test( 'Progress', function ( assert ) { + var imageDeferred = $.Deferred(), + viewer = mw.mmv.testHelpers.getMultimediaViewer(), + fakeImage = { + filePageTitle: new mw.Title( 'File:Stuff.jpg' ), + extraStatsDeferred: $.Deferred().reject() + }, + // custom clock ensures progress handlers execute in correct sequence + clock = this.sandbox.useFakeTimers(); + + viewer.thumbs = []; + viewer.displayPlaceholderThumbnail = $.noop; + viewer.setImage = $.noop; + viewer.scroll = $.noop; + viewer.preloadFullscreenThumbnail = $.noop; + viewer.fetchSizeIndependentLightboxInfo = function () { return $.Deferred().resolve( {} ); }; + viewer.ui = { + setFileReuseData: $.noop, + setupForLoad: $.noop, + canvas: { set: $.noop, + unblurWithAnimation: $.noop, + unblur: $.noop, + getCurrentImageWidths: function () { return { real: 0 }; }, + getDimensions: function () { return {}; } + }, + panel: { + setImageInfo: $.noop, + scroller: { + animateMetadataOnce: $.noop + }, + progressBar: { + animateTo: this.sandbox.stub(), + jumpTo: this.sandbox.stub() + } + }, + open: $.noop }; + + viewer.imageProvider.get = function () { return imageDeferred.promise(); }; + viewer.imageInfoProvider.get = function () { return $.Deferred().resolve( {} ); }; + viewer.thumbnailInfoProvider.get = function () { return $.Deferred().resolve( {} ); }; + + // loadImage will call setupProgressBar, which will attach done, fail & + // progress handlers + viewer.loadImage( fakeImage, new Image() ); + clock.tick( 10 ); + assert.ok( viewer.ui.panel.progressBar.jumpTo.lastCall.calledWith( 0 ), + 'Percentage correctly reset by loadImage' ); + assert.ok( viewer.ui.panel.progressBar.animateTo.firstCall.calledWith( 5 ), + 'Percentage correctly animated to 5 by loadImage' ); + + imageDeferred.notify( 'response', 45 ); + clock.tick( 10 ); + assert.ok( viewer.ui.panel.progressBar.animateTo.secondCall.calledWith( 45 ), + 'Percentage correctly funneled to panel UI' ); + + imageDeferred.resolve( {}, {} ); + clock.tick( 10 ); + assert.ok( viewer.ui.panel.progressBar.animateTo.thirdCall.calledWith( 100 ), + 'Percentage correctly funneled to panel UI' ); + + clock.restore(); + + viewer.close(); + } ); + + QUnit.test( 'Progress when switching images', function ( assert ) { + var firstImageDeferred = $.Deferred(), + secondImageDeferred = $.Deferred(), + firstImage = { + index: 1, + filePageTitle: new mw.Title( 'File:First.jpg' ), + extraStatsDeferred: $.Deferred().reject() + }, + secondImage = { + index: 2, + filePageTitle: new mw.Title( 'File:Second.jpg' ), + extraStatsDeferred: $.Deferred().reject() + }, + viewer = mw.mmv.testHelpers.getMultimediaViewer(), + // custom clock ensures progress handlers execute in correct sequence + clock = this.sandbox.useFakeTimers(); + + // animation would keep running, conflict with other tests + this.sandbox.stub( $.fn, 'animate' ).returnsThis(); + + viewer.thumbs = []; + viewer.displayPlaceholderThumbnail = $.noop; + viewer.setImage = $.noop; + viewer.scroll = $.noop; + viewer.preloadFullscreenThumbnail = $.noop; + viewer.preloadImagesMetadata = $.noop; + viewer.preloadThumbnails = $.noop; + viewer.fetchSizeIndependentLightboxInfo = function () { return $.Deferred().resolve( {} ); }; + viewer.ui = { + setFileReuseData: $.noop, + setupForLoad: $.noop, + canvas: { set: $.noop, + unblurWithAnimation: $.noop, + unblur: $.noop, + getCurrentImageWidths: function () { return { real: 0 }; }, + getDimensions: function () { return {}; } + }, + panel: { + setImageInfo: $.noop, + scroller: { + animateMetadataOnce: $.noop + }, + progressBar: { + hide: this.sandbox.stub(), + animateTo: this.sandbox.stub(), + jumpTo: this.sandbox.stub() + } + }, + open: $.noop, + empty: $.noop }; + + viewer.imageInfoProvider.get = function () { return $.Deferred().resolve( {} ); }; + viewer.thumbnailInfoProvider.get = function () { return $.Deferred().resolve( {} ); }; + + // load some image + viewer.imageProvider.get = this.sandbox.stub().returns( firstImageDeferred ); + viewer.loadImage( firstImage, new Image() ); + clock.tick( 10 ); + assert.ok( viewer.ui.panel.progressBar.jumpTo.getCall( 0 ).calledWith( 0 ), + 'Percentage correctly reset for new first image' ); + assert.ok( viewer.ui.panel.progressBar.animateTo.getCall( 0 ).calledWith( 5 ), + 'Percentage correctly animated to 5 for first new image' ); + + // progress on active image + firstImageDeferred.notify( 'response', 20 ); + clock.tick( 10 ); + assert.ok( viewer.ui.panel.progressBar.animateTo.getCall( 1 ).calledWith( 20 ), + 'Percentage correctly animated when active image is loading' ); + + // change to another image + viewer.imageProvider.get = this.sandbox.stub().returns( secondImageDeferred ); + viewer.loadImage( secondImage, new Image() ); + clock.tick( 10 ); + assert.ok( viewer.ui.panel.progressBar.jumpTo.getCall( 1 ).calledWith( 0 ), + 'Percentage correctly reset for second new image' ); + assert.ok( viewer.ui.panel.progressBar.animateTo.getCall( 2 ).calledWith( 5 ), + 'Percentage correctly animated to 5 for second new image' ); + + // progress on active image + secondImageDeferred.notify( 'response', 30 ); + clock.tick( 10 ); + assert.ok( viewer.ui.panel.progressBar.animateTo.getCall( 3 ).calledWith( 30 ), + 'Percentage correctly animated when active image is loading' ); + + // progress on inactive image + firstImageDeferred.notify( 'response', 40 ); + clock.tick( 10 ); + assert.ok( viewer.ui.panel.progressBar.animateTo.callCount === 4, + 'Percentage not animated when inactive image is loading' ); + + // progress on active image + secondImageDeferred.notify( 'response', 50 ); + clock.tick( 10 ); + assert.ok( viewer.ui.panel.progressBar.animateTo.getCall( 4 ).calledWith( 50 ), + 'Percentage correctly ignored inactive image & only animated when active image is loading' ); + + // change back to first image + viewer.imageProvider.get = this.sandbox.stub().returns( firstImageDeferred ); + viewer.loadImage( firstImage, new Image() ); + clock.tick( 10 ); + assert.ok( viewer.ui.panel.progressBar.jumpTo.getCall( 2 ).calledWith( 40 ), + 'Percentage jumps to right value when changing images' ); + + secondImageDeferred.resolve( {}, {} ); + clock.tick( 10 ); + assert.ok( !viewer.ui.panel.progressBar.hide.called, + 'Progress bar not hidden when something finishes in the background' ); + + // change back to second image, which has finished loading + viewer.imageProvider.get = this.sandbox.stub().returns( secondImageDeferred ); + viewer.loadImage( secondImage, new Image() ); + clock.tick( 10 ); + assert.ok( viewer.ui.panel.progressBar.hide.called, + 'Progress bar hidden when switching to finished image' ); + + clock.restore(); + + viewer.close(); + } ); + + QUnit.test( 'resetBlurredThumbnailStates', function ( assert ) { + var viewer = mw.mmv.testHelpers.getMultimediaViewer(); + + // animation would keep running, conflict with other tests + this.sandbox.stub( $.fn, 'animate' ).returnsThis(); + + assert.ok( !viewer.realThumbnailShown, 'Real thumbnail state is correct' ); + assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + + viewer.realThumbnailShown = true; + viewer.blurredThumbnailShown = true; + + viewer.resetBlurredThumbnailStates(); + + assert.ok( !viewer.realThumbnailShown, 'Real thumbnail state is correct' ); + assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + } ); + + QUnit.test( 'Placeholder first, then real thumbnail', function ( assert ) { + var viewer = mw.mmv.testHelpers.getMultimediaViewer(); + + viewer.setImage = $.noop; + viewer.ui = { canvas: { + unblurWithAnimation: $.noop, + unblur: $.noop, + maybeDisplayPlaceholder: function () { return true; } + } }; + viewer.imageInfoProvider.get = this.sandbox.stub(); + + viewer.displayPlaceholderThumbnail( { originalWidth: 100, originalHeight: 100 }, undefined, undefined ); + + assert.ok( viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + assert.ok( !viewer.realThumbnailShown, 'Real thumbnail state is correct' ); + + viewer.displayRealThumbnail( { url: undefined } ); + + assert.ok( viewer.realThumbnailShown, 'Real thumbnail state is correct' ); + assert.ok( viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + } ); + + QUnit.test( 'Placeholder first, then real thumbnail - missing size', function ( assert ) { + var viewer = mw.mmv.testHelpers.getMultimediaViewer(); + + viewer.currentIndex = 1; + viewer.setImage = $.noop; + viewer.ui = { canvas: { + unblurWithAnimation: $.noop, + unblur: $.noop, + maybeDisplayPlaceholder: function () { return true; } + } }; + viewer.imageInfoProvider.get = this.sandbox.stub().returns( $.Deferred().resolve( { width: 100, height: 100 } ) ); + + viewer.displayPlaceholderThumbnail( { index: 1 }, undefined, undefined ); + + assert.ok( viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + assert.ok( !viewer.realThumbnailShown, 'Real thumbnail state is correct' ); + + viewer.displayRealThumbnail( { url: undefined } ); + + assert.ok( viewer.realThumbnailShown, 'Real thumbnail state is correct' ); + assert.ok( viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + } ); + + QUnit.test( 'Real thumbnail first, then placeholder', function ( assert ) { + var viewer = mw.mmv.testHelpers.getMultimediaViewer(); + + viewer.setImage = $.noop; + viewer.ui = { + showImage: $.noop, + canvas: { + unblurWithAnimation: $.noop, + unblur: $.noop + } }; + + viewer.displayRealThumbnail( { url: undefined } ); + + assert.ok( viewer.realThumbnailShown, 'Real thumbnail state is correct' ); + assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + + viewer.displayPlaceholderThumbnail( {}, undefined, undefined ); + + assert.ok( viewer.realThumbnailShown, 'Real thumbnail state is correct' ); + assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + } ); + + QUnit.test( 'displayRealThumbnail', function ( assert ) { + var viewer = mw.mmv.testHelpers.getMultimediaViewer(); + + viewer.setImage = $.noop; + viewer.ui = { canvas: { + unblurWithAnimation: this.sandbox.stub(), + unblur: $.noop + } }; + viewer.blurredThumbnailShown = true; + + // Should not result in an unblurWithAnimation animation (image cache from cache) + viewer.displayRealThumbnail( { url: undefined }, undefined, undefined, 5 ); + assert.ok( !viewer.ui.canvas.unblurWithAnimation.called, 'There should not be an unblurWithAnimation animation' ); + + // Should result in an unblurWithAnimation (image didn't come from cache) + viewer.displayRealThumbnail( { url: undefined }, undefined, undefined, 1000 ); + assert.ok( viewer.ui.canvas.unblurWithAnimation.called, 'There should be an unblurWithAnimation animation' ); + } ); + + QUnit.test( 'New image loaded while another one is loading', function ( assert ) { + var viewer = mw.mmv.testHelpers.getMultimediaViewer(), + firstImageDeferred = $.Deferred(), + secondImageDeferred = $.Deferred(), + firstLigthboxInfoDeferred = $.Deferred(), + secondLigthboxInfoDeferred = $.Deferred(), + firstImage = { + filePageTitle: new mw.Title( 'File:Foo.jpg' ), + index: 0, + extraStatsDeferred: $.Deferred().reject() + }, + secondImage = { + filePageTitle: new mw.Title( 'File:Bar.jpg' ), + index: 1, + extraStatsDeferred: $.Deferred().reject() + }, + // custom clock ensures progress handlers execute in correct sequence + clock = this.sandbox.useFakeTimers(); + + viewer.preloadFullscreenThumbnail = $.noop; + viewer.fetchSizeIndependentLightboxInfo = this.sandbox.stub(); + viewer.ui = { + setFileReuseData: $.noop, + setupForLoad: $.noop, + canvas: { + set: $.noop, + getCurrentImageWidths: function () { return { real: 0 }; }, + getDimensions: function () { return {}; } + }, + panel: { + setImageInfo: this.sandbox.stub(), + scroller: { + animateMetadataOnce: $.noop + }, + progressBar: { + animateTo: this.sandbox.stub(), + jumpTo: this.sandbox.stub() + }, + empty: $.noop + }, + open: $.noop, + empty: $.noop }; + viewer.displayRealThumbnail = this.sandbox.stub(); + viewer.eachPrealoadableLightboxIndex = $.noop; + viewer.animateMetadataDivOnce = this.sandbox.stub().returns( $.Deferred().reject() ); + viewer.imageProvider.get = this.sandbox.stub(); + viewer.imageInfoProvider.get = function () { return $.Deferred().reject(); }; + viewer.thumbnailInfoProvider.get = function () { return $.Deferred().resolve( {} ); }; + + viewer.imageProvider.get.returns( firstImageDeferred.promise() ); + viewer.fetchSizeIndependentLightboxInfo.returns( firstLigthboxInfoDeferred.promise() ); + viewer.loadImage( firstImage, new Image() ); + clock.tick( 10 ); + assert.ok( !viewer.animateMetadataDivOnce.called, 'Metadata of the first image should not be animated' ); + assert.ok( !viewer.ui.panel.setImageInfo.called, 'Metadata of the first image should not be shown' ); + + viewer.imageProvider.get.returns( secondImageDeferred.promise() ); + viewer.fetchSizeIndependentLightboxInfo.returns( secondLigthboxInfoDeferred.promise() ); + viewer.loadImage( secondImage, new Image() ); + clock.tick( 10 ); + + viewer.ui.panel.progressBar.animateTo.reset(); + firstImageDeferred.notify( undefined, 45 ); + clock.tick( 10 ); + assert.ok( !viewer.ui.panel.progressBar.animateTo.reset.called, 'Progress of the first image should not be shown' ); + + firstImageDeferred.resolve( {}, {} ); + firstLigthboxInfoDeferred.resolve( {} ); + clock.tick( 10 ); + assert.ok( !viewer.displayRealThumbnail.called, 'The first image being done loading should have no effect' ); + + viewer.displayRealThumbnail = this.sandbox.spy( function () { viewer.close(); } ); + secondImageDeferred.resolve( {}, {} ); + secondLigthboxInfoDeferred.resolve( {} ); + clock.tick( 10 ); + assert.ok( viewer.displayRealThumbnail.called, 'The second image being done loading should result in the image being shown' ); + + clock.restore(); + } ); + + QUnit.test( 'Events are not trapped after the viewer is closed', function ( assert ) { + var i, j, k, eventParameters, + viewer = mw.mmv.testHelpers.getMultimediaViewer(), + $document = $( document ), + $qf = $( '#qunit-fixture' ), + eventTypes = [ 'keydown', 'keyup', 'keypress', 'click', 'mousedown', 'mouseup' ], + modifiers = [ undefined, 'altKey', 'ctrlKey', 'shiftKey', 'metaKey' ], + // Events are async, we need to wait for the last event to be caught before ending the test + done = assert.async(), + oldScrollTo = $.scrollTo; + + assert.expect( 0 ); + + // animation would keep running, conflict with other tests + this.sandbox.stub( $.fn, 'animate' ).returnsThis(); + + $.scrollTo = function () { return { scrollTop: $.noop, on: $.noop, off: $.noop }; }; + + viewer.setupEventHandlers(); + + viewer.imageProvider.get = function () { return $.Deferred().reject(); }; + viewer.imageInfoProvider.get = function () { return $.Deferred().reject(); }; + viewer.thumbnailInfoProvider.get = function () { return $.Deferred().reject(); }; + viewer.fileRepoInfoProvider.get = function () { return $.Deferred().reject(); }; + + viewer.preloadFullscreenThumbnail = $.noop; + viewer.initWithThumbs( [] ); + + viewer.loadImage( + { + filePageTitle: new mw.Title( 'File:Stuff.jpg' ), + thumbnail: new mw.mmv.model.Thumbnail( 'foo', 10, 10 ), + extraStatsDeferred: $.Deferred().reject() + }, + new Image() + ); + + viewer.ui.$closeButton.click(); + + function eventHandler( e ) { + if ( e.isDefaultPrevented() ) { + assert.ok( false, 'Event was incorrectly trapped: ' + e.which ); + } + + e.preventDefault(); + + // Wait for the last event + if ( e.which === 32 && e.type === 'mouseup' ) { + $document.off( '.mmvtest' ); + viewer.cleanupEventHandlers(); + $.scrollTo = oldScrollTo; + done(); + } + } + + for ( j = 0; j < eventTypes.length; j++ ) { + $document.on( eventTypes[ j ] + '.mmvtest', eventHandler ); + + eventloop: + for ( i = 0; i < 256; i++ ) { + // Save some time by not testing unlikely values for mouse events + if ( i > 32 ) { + switch ( eventTypes[ j ] ) { + case 'click': + case 'mousedown': + case 'mouseup': + break eventloop; + } + } + + for ( k = 0; k < modifiers.length; k++ ) { + eventParameters = { which: i }; + if ( modifiers[ k ] !== undefined ) { + eventParameters[ modifiers[ k ] ] = true; + } + $qf.trigger( $.Event( eventTypes[ j ], eventParameters ) ); + } + } + } + } ); + + QUnit.test( 'Refuse to load too-big thumbnails', function ( assert ) { + var viewer = mw.mmv.testHelpers.getMultimediaViewer(), + intendedWidth = 50, + title = mw.Title.newFromText( 'File:Foobar.svg' ); + + viewer.thumbnailInfoProvider.get = function ( fileTitle, width ) { + assert.strictEqual( width, intendedWidth ); + return $.Deferred().reject(); + }; + + viewer.fetchThumbnail( title, 1000, null, intendedWidth, 60 ); + } ); + + QUnit.test( 'fetchThumbnail()', function ( assert ) { + var guessedThumbnailInfoStub, + thumbnailInfoStub, + imageStub, + promise, + useThumbnailGuessing, + viewer = new mw.mmv.MultimediaViewer( { imageQueryParameter: $.noop, language: $.noop, recordVirtualViewBeaconURI: $.noop, extensions: function () { return { jpg: 'default' }; }, useThumbnailGuessing: function () { return useThumbnailGuessing; } } ), + sandbox = this.sandbox, + file = new mw.Title( 'File:Copyleft.svg' ), + sampleURL = 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png', + width = 100, + originalWidth = 1000, + originalHeight = 1000, + image = {}, + // custom clock ensures progress handlers execute in correct sequence + clock = this.sandbox.useFakeTimers(); + + function setupStubs() { + guessedThumbnailInfoStub = viewer.guessedThumbnailInfoProvider.get = sandbox.stub(); + thumbnailInfoStub = viewer.thumbnailInfoProvider.get = sandbox.stub(); + imageStub = viewer.imageProvider.get = sandbox.stub(); + } + + useThumbnailGuessing = true; + + // When we lack sample URL and original dimensions, the classic provider should be used + setupStubs(); + guessedThumbnailInfoStub.returns( $.Deferred().resolve( { url: 'guessedURL' } ) ); + thumbnailInfoStub.returns( $.Deferred().resolve( { url: 'apiURL' } ) ); + imageStub.returns( $.Deferred().resolve( image ) ); + promise = viewer.fetchThumbnail( file, width ); + clock.tick( 10 ); + assert.ok( !guessedThumbnailInfoStub.called, 'When we lack sample URL and original dimensions, GuessedThumbnailInfoProvider is not called' ); + assert.ok( thumbnailInfoStub.calledOnce, 'When we lack sample URL and original dimensions, ThumbnailInfoProvider is called once' ); + assert.ok( imageStub.calledOnce, 'When we lack sample URL and original dimensions, ImageProvider is called once' ); + assert.ok( imageStub.calledWith( 'apiURL' ), 'When we lack sample URL and original dimensions, ImageProvider is called with the API url' ); + assert.strictEqual( promise.state(), 'resolved', 'When we lack sample URL and original dimensions, fetchThumbnail resolves' ); + + // When the guesser bails out, the classic provider should be used + setupStubs(); + guessedThumbnailInfoStub.returns( $.Deferred().reject() ); + thumbnailInfoStub.returns( $.Deferred().resolve( { url: 'apiURL' } ) ); + imageStub.returns( $.Deferred().resolve( image ) ); + promise = viewer.fetchThumbnail( file, width, sampleURL, originalWidth, originalHeight ); + clock.tick( 10 ); + assert.ok( guessedThumbnailInfoStub.calledOnce, 'When the guesser bails out, GuessedThumbnailInfoProvider is called once' ); + assert.ok( thumbnailInfoStub.calledOnce, 'When the guesser bails out, ThumbnailInfoProvider is called once' ); + assert.ok( imageStub.calledOnce, 'When the guesser bails out, ImageProvider is called once' ); + assert.ok( imageStub.calledWith( 'apiURL' ), 'When the guesser bails out, ImageProvider is called with the API url' ); + assert.strictEqual( promise.state(), 'resolved', 'When the guesser bails out, fetchThumbnail resolves' ); + + // When the guesser returns an URL, that should be used + setupStubs(); + guessedThumbnailInfoStub.returns( $.Deferred().resolve( { url: 'guessedURL' } ) ); + thumbnailInfoStub.returns( $.Deferred().resolve( { url: 'apiURL' } ) ); + imageStub.returns( $.Deferred().resolve( image ) ); + promise = viewer.fetchThumbnail( file, width, sampleURL, originalWidth, originalHeight ); + clock.tick( 10 ); + assert.ok( guessedThumbnailInfoStub.calledOnce, 'When the guesser returns an URL, GuessedThumbnailInfoProvider is called once' ); + assert.ok( !thumbnailInfoStub.called, 'When the guesser returns an URL, ThumbnailInfoProvider is not called' ); + assert.ok( imageStub.calledOnce, 'When the guesser returns an URL, ImageProvider is called once' ); + assert.ok( imageStub.calledWith( 'guessedURL' ), 'When the guesser returns an URL, ImageProvider is called with the guessed url' ); + assert.strictEqual( promise.state(), 'resolved', 'When the guesser returns an URL, fetchThumbnail resolves' ); + + // When the guesser returns an URL, but that returns 404, image loading should be retried with the classic provider + setupStubs(); + guessedThumbnailInfoStub.returns( $.Deferred().resolve( { url: 'guessedURL' } ) ); + thumbnailInfoStub.returns( $.Deferred().resolve( { url: 'apiURL' } ) ); + imageStub.withArgs( 'guessedURL' ).returns( $.Deferred().reject() ); + imageStub.withArgs( 'apiURL' ).returns( $.Deferred().resolve( image ) ); + promise = viewer.fetchThumbnail( file, width, sampleURL, originalWidth, originalHeight ); + clock.tick( 10 ); + assert.ok( guessedThumbnailInfoStub.calledOnce, 'When the guesser returns an URL, but that returns 404, GuessedThumbnailInfoProvider is called once' ); + assert.ok( thumbnailInfoStub.calledOnce, 'When the guesser returns an URL, but that returns 404, ThumbnailInfoProvider is called once' ); + assert.ok( imageStub.calledTwice, 'When the guesser returns an URL, but that returns 404, ImageProvider is called twice' ); + assert.ok( imageStub.getCall( 0 ).calledWith( 'guessedURL' ), 'When the guesser returns an URL, but that returns 404, ImageProvider is called first with the guessed url' ); + assert.ok( imageStub.getCall( 1 ).calledWith( 'apiURL' ), 'When the guesser returns an URL, but that returns 404, ImageProvider is called second with the guessed url' ); + assert.strictEqual( promise.state(), 'resolved', 'When the guesser returns an URL, but that returns 404, fetchThumbnail resolves' ); + + // When even the retry fails, fetchThumbnail() should reject + setupStubs(); + guessedThumbnailInfoStub.returns( $.Deferred().resolve( { url: 'guessedURL' } ) ); + thumbnailInfoStub.returns( $.Deferred().resolve( { url: 'apiURL' } ) ); + imageStub.withArgs( 'guessedURL' ).returns( $.Deferred().reject() ); + imageStub.withArgs( 'apiURL' ).returns( $.Deferred().reject() ); + promise = viewer.fetchThumbnail( file, width, sampleURL, originalWidth, originalHeight ); + clock.tick( 10 ); + assert.ok( guessedThumbnailInfoStub.calledOnce, 'When even the retry fails, GuessedThumbnailInfoProvider is called once' ); + assert.ok( thumbnailInfoStub.calledOnce, 'When even the retry fails, ThumbnailInfoProvider is called once' ); + assert.ok( imageStub.calledTwice, 'When even the retry fails, ImageProvider is called twice' ); + assert.ok( imageStub.getCall( 0 ).calledWith( 'guessedURL' ), 'When even the retry fails, ImageProvider is called first with the guessed url' ); + assert.ok( imageStub.getCall( 1 ).calledWith( 'apiURL' ), 'When even the retry fails, ImageProvider is called second with the guessed url' ); + assert.strictEqual( promise.state(), 'rejected', 'When even the retry fails, fetchThumbnail rejects' ); + + useThumbnailGuessing = false; + + // When guessing is disabled, the classic provider is used + setupStubs(); + guessedThumbnailInfoStub.returns( $.Deferred().resolve( { url: 'guessedURL' } ) ); + thumbnailInfoStub.returns( $.Deferred().resolve( { url: 'apiURL' } ) ); + imageStub.returns( $.Deferred().resolve( image ) ); + promise = viewer.fetchThumbnail( file, width ); + clock.tick( 10 ); + assert.ok( !guessedThumbnailInfoStub.called, 'When guessing is disabled, GuessedThumbnailInfoProvider is not called' ); + assert.ok( thumbnailInfoStub.calledOnce, 'When guessing is disabled, ThumbnailInfoProvider is called once' ); + assert.ok( imageStub.calledOnce, 'When guessing is disabled, ImageProvider is called once' ); + assert.ok( imageStub.calledWith( 'apiURL' ), 'When guessing is disabled, ImageProvider is called with the API url' ); + assert.strictEqual( promise.state(), 'resolved', 'When guessing is disabled, fetchThumbnail resolves' ); + + clock.restore(); + } ); + + QUnit.test( 'document.title', function ( assert ) { + var viewer = mw.mmv.testHelpers.getMultimediaViewer(), + bootstrap = new mw.mmv.MultimediaViewerBootstrap(), + title = new mw.Title( 'File:This_should_show_up_in_document_title.png' ), + oldDocumentTitle = document.title; + + viewer.currentImageFileTitle = title; + bootstrap.setupEventHandlers(); + viewer.setHash(); + + assert.ok( document.title.match( title.getNameText() ), 'File name is visible in title' ); + + viewer.close(); + bootstrap.cleanupEventHandlers(); + + assert.strictEqual( document.title, oldDocumentTitle, 'Original title restored after viewer is closed' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.testhelpers.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.testhelpers.js new file mode 100644 index 00000000..4010883a --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.testhelpers.js @@ -0,0 +1,174 @@ +( function ( mw, $ ) { + var MTH = {}; + + MTH.enterFullscreenMock = function () { + this.first().addClass( 'jq-fullscreened' ).data( 'isFullscreened', true ); + + $( document ).trigger( $.Event( 'jq-fullscreen-change', { element: this, fullscreen: true } ) ); + }; + + MTH.exitFullscreenMock = function () { + this.first().removeClass( 'jq-fullscreened' ).data( 'isFullscreened', false ); + + $( document ).trigger( $.Event( 'jq-fullscreen-change', { element: this, fullscreen: false } ) ); + }; + + /** + * Returns the exception thrown by callback, or undefined if no exception was thrown. + * + * @param {Function} callback + * @return {Error} + */ + MTH.getException = function ( callback ) { + var ex; + try { + callback(); + } catch ( e ) { + ex = e; + } + return ex; + }; + + /** + * Creates an mw.storage-like object. + * + * @param {Object} storage localStorage stub with getItem, setItem, removeItem methods + * @return {mw.storage} Local storage-like object + */ + MTH.createLocalStorage = function ( storage ) { + return new ( Object.getPrototypeOf( mw.storage ) ).constructor( storage ); + }; + + /** + * Returns an mw.storage that mimicks lack of localStorage support. + * + * @return {mw.storage} Local storage-like object + */ + MTH.getUnsupportedLocalStorage = function () { + return this.createLocalStorage( undefined ); + }; + + /** + * Returns an mw.storage that mimicks localStorage being disabled in browser. + * + * @return {mw.storage} Local storage-like object + */ + MTH.getDisabledLocalStorage = function () { + var e = function () { + throw new Error( 'Error' ); + }; + + return this.createLocalStorage( { + getItem: e, + setItem: e, + removeItem: e + } ); + }; + + /** + * Returns a fake local storage which is not saved between reloads. + * + * @param {Object} [initialData] + * @return {mw.storage} Local storage-like object + */ + MTH.getFakeLocalStorage = function ( initialData ) { + var bag = new mw.Map(); + bag.set( initialData ); + + return this.createLocalStorage( { + getItem: function ( key ) { return bag.get( key ); }, + setItem: function ( key, value ) { bag.set( key, value ); }, + removeItem: function ( key ) { bag.set( key, null ); } + } ); + }; + + /** + * Returns a viewer object with all the appropriate placeholder functions. + * + * @return {mv.mmv.MultiMediaViewer} [description] + */ + MTH.getMultimediaViewer = function () { + return new mw.mmv.MultimediaViewer( { + imageQueryParameter: $.noop, + language: $.noop, + recordVirtualViewBeaconURI: $.noop, + extensions: function () { + return { jpg: 'default' }; + } + } ); + }; + + MTH.asyncPromises = []; + + /** + * Given a method/function that returns a promise, this'll return a function + * that just wraps the original & returns the original result, but also + * executes an assert.async() right before it's called, and resolves that + * async after that promise has completed. + * + * Example usage: given a method `bootstrap.openImage` that returns a + * promise, just call it like this to wrap this functionality around it: + * `bootstrap.openImage = asyncMethod( bootstrap.openImage, bootstrap );` + * + * Now, every time some part of the code calls this function, it'll just + * execute as it normally would, but your tests won't finish until these + * functions (and any .then tacked on to them) have completed. + * + * This method will make sure your tests don't end prematurely (before the + * promises have been resolved), but that's it. If you need to run + * additional code after all promises have resolved, you can call the + * complementary `waitForAsync`, which will return a promise that doesn't + * resolve until all of these promises have. + * + * @param {Object} object + * @param {string} method + * @param {QUnit.assert} [assert] + * @return {Function} + */ + MTH.asyncMethod = function ( object, method, assert ) { + return function () { + // apply arguments to original promise + var promise = object[ method ].apply( object, arguments ), + done; + + this.asyncPromises.push( promise ); + + if ( assert ) { + done = assert.async(); + // use setTimeout to ensure `done` is not the first callback handler + // to execute (possibly ending the test's wait right before + // the result of the promise is executed) + setTimeout( promise.then.bind( null, done, done ) ); + } + + return promise; + }.bind( this ); + }; + + /** + * Returns a promise that will not resolve until all of the promises that + * were created in functions upon which `asyncMethod` was called have + * resolved. + * + * @return {$.Promise} + */ + MTH.waitForAsync = function () { + var deferred = $.Deferred(); + + // it's possible that, before this function call, some code was executed + // that triggers async code that will eventually end up `asyncPromises` + // in order to give that code a chance to run, we'll add another promise + // to the array, that will only resolve at the end of the current call + // stack (using setTimeout) + this.asyncPromises.push( deferred.promise() ); + setTimeout( deferred.resolve ); + + return QUnit.whenPromisesComplete.apply( null, this.asyncPromises ).then( + function () { + this.asyncPromises = []; + }.bind( this ) + ); + }; + + mw.mmv.testHelpers = MTH; +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.EmbedFileInfo.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.EmbedFileInfo.test.js new file mode 100644 index 00000000..be606300 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.EmbedFileInfo.test.js @@ -0,0 +1,40 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw ) { + QUnit.module( 'mmv.model.EmbedFileInfo', QUnit.newMwEnvironment() ); + + QUnit.test( 'EmbedFileInfo constructor sanity check', function ( assert ) { + var imageInfo = {}, + repoInfo = {}, + caption = 'Foo', + alt = 'Bar', + embedFileInfo = new mw.mmv.model.EmbedFileInfo( imageInfo, repoInfo, caption, alt ); + + assert.strictEqual( embedFileInfo.imageInfo, imageInfo, 'ImageInfo is set correctly' ); + assert.strictEqual( embedFileInfo.repoInfo, repoInfo, 'ImageInfo is set correctly' ); + assert.strictEqual( embedFileInfo.caption, caption, 'Caption is set correctly' ); + assert.strictEqual( embedFileInfo.alt, alt, 'Alt text is set correctly' ); + + try { + embedFileInfo = new mw.mmv.model.EmbedFileInfo( {} ); + } catch ( e ) { + assert.ok( e, 'Exception is thrown when parameters are missing' ); + } + } ); + +}( mediaWiki ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Image.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Image.test.js new file mode 100644 index 00000000..f02c55db --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Image.test.js @@ -0,0 +1,151 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw ) { + QUnit.module( 'mmv.model.Image', QUnit.newMwEnvironment() ); + + QUnit.test( 'Image model constructor sanity check', function ( assert ) { + var + title = mw.Title.newFromText( 'File:Foobar.jpg' ), + name = 'Foo bar', + size = 100, + width = 10, + height = 15, + mime = 'image/jpeg', + url = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg', + pageID = 42, + descurl = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg', + descShortUrl = '', + repo = 'wikimediacommons', + datetime = '2011-07-04T23:31:14Z', + anondatetime = '20110704000000', + origdatetime = '2010-07-04T23:31:14Z', + description = 'This is a test file.', + source = 'WMF', + author = 'Ryan Kaldari', + authorCount = 1, + permission = 'only use for good, not evil', + deletionReason = 'poor quality', + license = new mw.mmv.model.License( 'cc0' ), + attribution = 'Created by my cats on a winter morning', + latitude = 39.12381283, + longitude = 100.983829, + restrictions = [ 'trademarked' ], + imageData = new mw.mmv.model.Image( + title, name, size, width, height, mime, url, + descurl, descShortUrl, pageID, repo, datetime, anondatetime, origdatetime, + description, source, author, authorCount, license, permission, attribution, + deletionReason, latitude, longitude, restrictions ); + + assert.strictEqual( imageData.title, title, 'Title is set correctly' ); + assert.strictEqual( imageData.name, name, 'Name is set correctly' ); + assert.strictEqual( imageData.size, size, 'Size is set correctly' ); + assert.strictEqual( imageData.width, width, 'Width is set correctly' ); + assert.strictEqual( imageData.height, height, 'Height is set correctly' ); + assert.strictEqual( imageData.mimeType, mime, 'MIME type is set correctly' ); + assert.strictEqual( imageData.url, url, 'URL for original image is set correctly' ); + assert.strictEqual( imageData.descriptionUrl, descurl, 'URL for image description page is set correctly' ); + assert.strictEqual( imageData.pageID, pageID, 'Page ID of image description is set correctly' ); + assert.strictEqual( imageData.repo, repo, 'Repository name is set correctly' ); + assert.strictEqual( imageData.uploadDateTime, datetime, 'Date and time of last upload is set correctly' ); + assert.strictEqual( imageData.anonymizedUploadDateTime, anondatetime, 'Anonymized date and time of last upload is set correctly' ); + assert.strictEqual( imageData.creationDateTime, origdatetime, 'Date and time of original upload is set correctly' ); + assert.strictEqual( imageData.description, description, 'Description is set correctly' ); + assert.strictEqual( imageData.source, source, 'Source is set correctly' ); + assert.strictEqual( imageData.author, author, 'Author is set correctly' ); + assert.strictEqual( imageData.authorCount, authorCount, 'Author is set correctly' ); + assert.strictEqual( imageData.license, license, 'License is set correctly' ); + assert.strictEqual( imageData.permission, permission, 'Permission is set correctly' ); + assert.strictEqual( imageData.attribution, attribution, 'Attribution is set correctly' ); + assert.strictEqual( imageData.deletionReason, deletionReason, 'Deletion reason is set correctly' ); + assert.strictEqual( imageData.latitude, latitude, 'Latitude is set correctly' ); + assert.strictEqual( imageData.longitude, longitude, 'Longitude is set correctly' ); + assert.deepEqual( imageData.restrictions, restrictions, 'Restrictions is set correctly' ); + assert.ok( imageData.thumbUrls, 'Thumb URL cache is set up properly' ); + } ); + + QUnit.test( 'hasCoords()', function ( assert ) { + var + firstImageData = new mw.mmv.model.Image( + mw.Title.newFromText( 'File:Foobar.pdf.jpg' ), 'Foo bar', + 10, 10, 10, 'image/jpeg', 'http://example.org', 'http://example.com', 42, + 'example', 'tester', '2013-11-10', '20131110', '2013-11-09', 'Blah blah blah', + 'A person', 'Another person', 1, 'CC-BY-SA-3.0', 'Permitted', 'My cat' + ), + secondImageData = new mw.mmv.model.Image( + mw.Title.newFromText( 'File:Foobar.pdf.jpg' ), 'Foo bar', + 10, 10, 10, 'image/jpeg', 'http://example.org', 'http://example.com', 42, + 'example', 'tester', '2013-11-10', '20131110', '2013-11-09', 'Blah blah blah', + 'A person', 'Another person', 1, 'CC-BY-SA-3.0', 'Permitted', 'My cat', + undefined, '39.91820938', '78.09812938' + ); + + assert.strictEqual( firstImageData.hasCoords(), false, 'No coordinates present means hasCoords returns false.' ); + assert.strictEqual( secondImageData.hasCoords(), true, 'Coordinates present means hasCoords returns true.' ); + } ); + + QUnit.test( 'parseExtmeta()', function ( assert ) { + var Image = mw.mmv.model.Image, + stringData = { value: 'foo' }, + plaintextData = { value: 'fo<b>o</b>' }, + integerData = { value: 3 }, + integerStringData = { value: '3' }, + zeroPrefixedIntegerStringData = { value: '03' }, + floatData = { value: 1.23 }, + floatStringData = { value: '1.23' }, + booleanData = { value: 'yes' }, + wrongBooleanData = { value: 'blah' }, + listDataEmpty = { value: '' }, + listDataSingle = { value: 'foo' }, + listDataMultiple = { value: 'foo|bar|baz' }, + missingData; + + assert.strictEqual( Image.parseExtmeta( stringData, 'string' ), 'foo', + 'Extmeta string parsed correctly.' ); + assert.strictEqual( Image.parseExtmeta( plaintextData, 'plaintext' ), 'foo', + 'Extmeta plaintext parsed correctly.' ); + assert.strictEqual( Image.parseExtmeta( floatData, 'float' ), 1.23, + 'Extmeta float parsed correctly.' ); + assert.strictEqual( Image.parseExtmeta( floatStringData, 'float' ), 1.23, + 'Extmeta float string parsed correctly.' ); + assert.strictEqual( Image.parseExtmeta( booleanData, 'boolean' ), true, + 'Extmeta boolean string parsed correctly.' ); + assert.strictEqual( Image.parseExtmeta( wrongBooleanData, 'boolean' ), undefined, + 'Extmeta boolean string with error ignored.' ); + assert.strictEqual( Image.parseExtmeta( integerData, 'integer' ), 3, + 'Extmeta integer parsed correctly.' ); + assert.strictEqual( Image.parseExtmeta( integerStringData, 'integer' ), 3, + 'Extmeta integer string parsed correctly.' ); + assert.strictEqual( Image.parseExtmeta( zeroPrefixedIntegerStringData, 'integer' ), 3, + 'Extmeta zero-prefixed integer string parsed correctly.' ); + assert.deepEqual( Image.parseExtmeta( listDataEmpty, 'list' ), [], + 'Extmeta empty list parsed correctly.' ); + assert.deepEqual( Image.parseExtmeta( listDataSingle, 'list' ), [ 'foo' ], + 'Extmeta list with single element parsed correctly.' ); + assert.deepEqual( Image.parseExtmeta( listDataMultiple, 'list' ), [ 'foo', 'bar', 'baz' ], + 'Extmeta list with multipleelements parsed correctly.' ); + assert.strictEqual( Image.parseExtmeta( missingData, 'string' ), undefined, + 'Extmeta missing data parsed correctly.' ); + + try { + Image.parseExtmeta( stringData, 'strong' ); + } catch ( e ) { + assert.ok( e, 'Exception is thrown on invalid argument' ); + } + } ); + +}( mediaWiki ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.IwTitle.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.IwTitle.test.js new file mode 100644 index 00000000..27cf119d --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.IwTitle.test.js @@ -0,0 +1,43 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw ) { + QUnit.module( 'mmv.model.IwTitle', QUnit.newMwEnvironment() ); + + QUnit.test( 'constructor sanity test', function ( assert ) { + var namespace = 4, + fullPageName = 'User_talk:John_Doe', + domain = 'en.wikipedia.org', + url = 'https://en.wikipedia.org/wiki/User_talk:John_Doe', + title = new mw.mmv.model.IwTitle( namespace, fullPageName, domain, url ); + + assert.ok( title ); + } ); + + QUnit.test( 'getters', function ( assert ) { + var namespace = 4, + fullPageName = 'User_talk:John_Doe', + domain = 'en.wikipedia.org', + url = 'https://en.wikipedia.org/wiki/User_talk:John_Doe', + title = new mw.mmv.model.IwTitle( namespace, fullPageName, domain, url ); + + assert.strictEqual( title.getUrl(), url, 'getUrl()' ); + assert.strictEqual( title.getDomain(), domain, 'getDomain()' ); + assert.strictEqual( title.getPrefixedDb(), fullPageName, 'getPrefixedDb()' ); + assert.strictEqual( title.getPrefixedText(), 'User talk:John Doe', 'getPrefixedText()' ); + } ); +}( mediaWiki ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.License.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.License.test.js new file mode 100644 index 00000000..7d758f09 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.License.test.js @@ -0,0 +1,161 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + + QUnit.module( 'mmv.model.License', QUnit.newMwEnvironment() ); + + QUnit.test( 'License constructor sanity check', function ( assert ) { + var license, + shortName = 'CC-BY-SA-3.0', + internalName = 'cc-by-sa-3.0', + longName = 'Creative Commons Attribution--Share-Alike 3.0', + url = 'http://creativecommons.org/licenses/by-sa/3.0/'; + + license = new mw.mmv.model.License( shortName ); + assert.ok( license, 'License created successfully' ); + assert.strictEqual( license.shortName, shortName, 'License has correct short name' ); + assert.ok( !license.internalName, 'License has no internal name' ); + assert.ok( !license.longName, 'License has no long name' ); + assert.ok( !license.deedUrl, 'License has no deed URL' ); + + license = new mw.mmv.model.License( shortName, internalName, longName, url ); + assert.ok( license, 'License created successfully' ); + assert.strictEqual( license.shortName, shortName, 'License has correct short name' ); + assert.strictEqual( license.internalName, internalName, 'License has correct internal name' ); + assert.strictEqual( license.longName, longName, 'License has correct long name' ); + assert.strictEqual( license.deedUrl, url, 'License has correct deed URL' ); + + try { + license = new mw.mmv.model.License(); + } catch ( e ) { + assert.ok( e, 'License cannot be created without a short name' ); + } + } ); + + QUnit.test( 'getShortName()', function ( assert ) { + var existingMessageKey = 'Internal name that does exist', + nonExistingMessageKey = 'Internal name that does not exist', + license1 = new mw.mmv.model.License( 'Shortname' ), + license2 = new mw.mmv.model.License( 'Shortname', nonExistingMessageKey ), + license3 = new mw.mmv.model.License( 'Shortname', existingMessageKey ), + oldMwMessage = mw.message, + oldMwMessagesExists = mw.messages.exists; + + mw.message = function ( name ) { + return name === 'multimediaviewer-license-' + existingMessageKey ? + { text: function () { return 'Translated name'; } } : + oldMwMessage.apply( mw, arguments ); + }; + mw.messages.exists = function ( name ) { + return name === 'multimediaviewer-license-' + existingMessageKey ? + true : oldMwMessagesExists.apply( mw.messages, arguments ); + }; + + assert.strictEqual( license1.getShortName(), 'Shortname', + 'Short name is returned when there is no translated name' ); + assert.strictEqual( license2.getShortName(), 'Shortname', + 'Short name is returned when translated name is missing' ); + assert.strictEqual( license3.getShortName(), 'Translated name', + 'Translated name is returned when it exists' ); + + mw.message = oldMwMessage; + mw.messages.exists = oldMwMessagesExists; + } ); + + QUnit.test( 'getShortLink()', function ( assert ) { + var $html, + license1 = new mw.mmv.model.License( 'lorem ipsum' ), + license2 = new mw.mmv.model.License( 'lorem ipsum', 'lipsum' ), + license3 = new mw.mmv.model.License( 'lorem ipsum', 'lipsum', 'Lorem ipsum dolor sit amet' ), + license4 = new mw.mmv.model.License( 'lorem ipsum', 'lipsum', 'Lorem ipsum dolor sit amet', + 'http://www.lipsum.com/' ); + + assert.strictEqual( license1.getShortLink(), 'lorem ipsum', + 'Code for license without link is formatted correctly' ); + assert.strictEqual( license2.getShortLink(), 'lorem ipsum', + 'Code for license without link is formatted correctly' ); + assert.strictEqual( license3.getShortLink(), 'lorem ipsum', + 'Code for license without link is formatted correctly' ); + + $html = $( license4.getShortLink() ); + assert.strictEqual( $html.text(), 'lorem ipsum', + 'Text for license with link is formatted correctly' ); + assert.strictEqual( $html.prop( 'href' ), 'http://www.lipsum.com/', + 'URL for license with link is formatted correctly' ); + assert.strictEqual( $html.prop( 'title' ), 'Lorem ipsum dolor sit amet', + 'Title for license with link is formatted correctly' ); + } ); + + QUnit.test( 'isCc()', function ( assert ) { + var license; + + license = new mw.mmv.model.License( 'CC-BY-SA-2.0', 'cc-by-sa-2.0', + 'Creative Commons Attribution - ShareAlike 2.0', + 'http://creativecommons.org/licenses/by-sa/2.0/' ); + assert.strictEqual( license.isCc(), true, 'CC license recognized' ); + + license = new mw.mmv.model.License( 'Public Domain', 'pd', + 'Public Domain for lack of originality' ); + assert.strictEqual( license.isCc(), false, 'Non-CC license not recognized' ); + + license = new mw.mmv.model.License( 'MIT' ); + assert.strictEqual( license.isCc(), false, 'Non-CC license with no internal name not recognized' ); + } ); + + QUnit.test( 'isPd()', function ( assert ) { + var license; + + license = new mw.mmv.model.License( 'Public Domain', 'pd', + 'Public Domain for lack of originality' ); + assert.strictEqual( license.isPd(), true, 'PD license recognized' ); + + license = new mw.mmv.model.License( 'CC-BY-SA-2.0', 'cc-by-sa-2.0', + 'Creative Commons Attribution - ShareAlike 2.0', + 'http://creativecommons.org/licenses/by-sa/2.0/' ); + assert.strictEqual( license.isPd(), false, 'Non-PD license not recognized' ); + + license = new mw.mmv.model.License( 'MIT' ); + assert.strictEqual( license.isPd(), false, 'Non-PD license with no internal name not recognized' ); + } ); + + QUnit.test( 'isFree()', function ( assert ) { + var license; + + license = new mw.mmv.model.License( 'CC-BY-SA-2.0', 'cc-by-sa-2.0', + 'Creative Commons Attribution - ShareAlike 2.0', + 'http://creativecommons.org/licenses/by-sa/2.0/' ); + assert.strictEqual( license.isFree(), true, 'Licenses default to free' ); + + license = new mw.mmv.model.License( 'Fair use', 'fairuse', + 'Fair use', undefined, undefined, true ); + assert.strictEqual( license.isFree(), false, 'Non-free flag handled correctly' ); + } ); + + QUnit.test( 'needsAttribution()', function ( assert ) { + var license; + + license = new mw.mmv.model.License( 'CC-BY-SA-2.0', 'cc-by-sa-2.0', + 'Creative Commons Attribution - ShareAlike 2.0', + 'http://creativecommons.org/licenses/by-sa/2.0/' ); + assert.strictEqual( license.needsAttribution(), true, 'Licenses assumed to need attribution by default' ); + + license = new mw.mmv.model.License( 'Public Domain', 'pd', + 'Public Domain for lack of originality', false ); + assert.strictEqual( license.needsAttribution(), false, 'Attribution required flag handled correctly' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Repo.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Repo.test.js new file mode 100644 index 00000000..a9bc7a2d --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Repo.test.js @@ -0,0 +1,100 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw ) { + QUnit.module( 'mmv.model.Repo', QUnit.newMwEnvironment() ); + + QUnit.test( 'Repo constructor sanity check', function ( assert ) { + var displayName = 'Wikimedia Commons', + favicon = '//commons.wikimedia.org/favicon.ico', + apiUrl = '//commons.wikimedia.org/w/api.php', + server = '//commons.wikimedia.org', + articlePath = '//commons.wikimedia.org/wiki/$1', + descBaseUrl = '//commons.wikimedia.org/wiki/File:', + localRepo = new mw.mmv.model.Repo( displayName, favicon, true ), + foreignApiRepo = new mw.mmv.model.ForeignApiRepo( displayName, favicon, + false, apiUrl, server, articlePath ), + foreignDbRepo = new mw.mmv.model.ForeignDbRepo( displayName, favicon, false, descBaseUrl ); + + assert.ok( localRepo, 'Local repo creation works' ); + assert.ok( foreignApiRepo, + 'Foreign API repo creation works' ); + assert.ok( foreignDbRepo, 'Foreign DB repo creation works' ); + } ); + + QUnit.test( 'getArticlePath()', function ( assert ) { + var displayName = 'Wikimedia Commons', + favicon = '//commons.wikimedia.org/favicon.ico', + apiUrl = '//commons.wikimedia.org/w/api.php', + server = '//commons.wikimedia.org', + articlePath = '/wiki/$1', + descBaseUrl = '//commons.wikimedia.org/wiki/File:', + localRepo = new mw.mmv.model.Repo( displayName, favicon, true ), + foreignApiRepo = new mw.mmv.model.ForeignApiRepo( displayName, favicon, + false, apiUrl, server, articlePath ), + foreignDbRepo = new mw.mmv.model.ForeignDbRepo( displayName, favicon, false, descBaseUrl ), + expectedLocalArticlePath = '/wiki/$1', + expectedFullArticlePath = '//commons.wikimedia.org/wiki/$1', + oldWgArticlePath = mw.config.get( 'wgArticlePath' ), + oldWgServer = mw.config.get( 'wgServer' ); + + mw.config.set( 'wgArticlePath', '/wiki/$1' ); + mw.config.set( 'wgServer', server ); + + assert.strictEqual( localRepo.getArticlePath(), expectedLocalArticlePath, + 'Local repo article path is correct' ); + assert.strictEqual( localRepo.getArticlePath( true ), expectedFullArticlePath, + 'Local repo absolute article path is correct' ); + assert.strictEqual( foreignApiRepo.getArticlePath(), expectedFullArticlePath, + 'Foreign API article path is correct' ); + assert.strictEqual( foreignDbRepo.getArticlePath(), expectedFullArticlePath, + 'Foreign DB article path is correct' ); + + mw.config.set( 'wgArticlePath', oldWgArticlePath ); + mw.config.set( 'wgServer', oldWgServer ); + } ); + + QUnit.test( 'getSiteLink()', function ( assert ) { + var displayName = 'Wikimedia Commons', + favicon = '//commons.wikimedia.org/favicon.ico', + apiUrl = '//commons.wikimedia.org/w/api.php', + server = '//commons.wikimedia.org', + articlePath = '/wiki/$1', + descBaseUrl = '//commons.wikimedia.org/wiki/File:', + localRepo = new mw.mmv.model.Repo( displayName, favicon, true ), + foreignApiRepo = new mw.mmv.model.ForeignApiRepo( displayName, favicon, + false, apiUrl, server, articlePath ), + foreignDbRepo = new mw.mmv.model.ForeignDbRepo( displayName, favicon, false, descBaseUrl ), + expectedSiteLink = '//commons.wikimedia.org/wiki/', + oldWgArticlePath = mw.config.get( 'wgArticlePath' ), + oldWgServer = mw.config.get( 'wgServer' ); + + mw.config.set( 'wgArticlePath', '/wiki/$1' ); + mw.config.set( 'wgServer', server ); + + assert.strictEqual( localRepo.getSiteLink(), expectedSiteLink, + 'Local repo site link is correct' ); + assert.strictEqual( foreignApiRepo.getSiteLink(), expectedSiteLink, + 'Foreign API repo site link is correct' ); + assert.strictEqual( foreignDbRepo.getSiteLink(), expectedSiteLink, + 'Foreign DB repo site link is correct' ); + + mw.config.set( 'wgArticlePath', oldWgArticlePath ); + mw.config.set( 'wgServer', oldWgServer ); + } ); + +}( mediaWiki ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.TaskQueue.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.TaskQueue.test.js new file mode 100644 index 00000000..f3958a24 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.TaskQueue.test.js @@ -0,0 +1,276 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.model.TaskQueue', QUnit.newMwEnvironment() ); + + QUnit.test( 'TaskQueue constructor sanity check', function ( assert ) { + var taskQueue = new mw.mmv.model.TaskQueue(); + + assert.ok( taskQueue, 'TaskQueue created successfully' ); + } ); + + QUnit.test( 'Queue length check', function ( assert ) { + var taskQueue = new mw.mmv.model.TaskQueue(); + + assert.strictEqual( taskQueue.queue.length, 0, 'queue is initially empty' ); + + taskQueue.push( function () {} ); + + assert.strictEqual( taskQueue.queue.length, 1, 'queue length is incremented on push' ); + } ); + + QUnit.test( 'State check', function ( assert ) { + var taskQueue = new mw.mmv.model.TaskQueue(), + task = $.Deferred(), + promise; + + taskQueue.push( function () { return task; } ); + + assert.strictEqual( taskQueue.state, mw.mmv.model.TaskQueue.State.NOT_STARTED, + 'state is initially NOT_STARTED' ); + + promise = taskQueue.execute().then( function () { + assert.strictEqual( taskQueue.state, mw.mmv.model.TaskQueue.State.FINISHED, + 'state is FINISHED after execution finished' ); + } ); + + assert.strictEqual( taskQueue.state, mw.mmv.model.TaskQueue.State.RUNNING, + 'state is RUNNING after execution started' ); + + task.resolve(); + + return promise; + } ); + + QUnit.test( 'State check for cancellation', function ( assert ) { + var taskQueue = new mw.mmv.model.TaskQueue(), + task = $.Deferred(); + + taskQueue.push( function () { return task; } ); + taskQueue.execute(); + taskQueue.cancel(); + + assert.strictEqual( taskQueue.state, mw.mmv.model.TaskQueue.State.CANCELLED, + 'state is CANCELLED after cancellation' ); + } ); + + QUnit.test( 'Test executing empty queue', function ( assert ) { + var taskQueue = new mw.mmv.model.TaskQueue(); + + return taskQueue.execute().done( function () { + assert.ok( true, 'Queue promise resolved' ); + } ); + } ); + + QUnit.test( 'Simple execution test', function ( assert ) { + var taskQueue = new mw.mmv.model.TaskQueue(), + called = false; + + taskQueue.push( function () { + called = true; + } ); + + return taskQueue.execute().then( function () { + assert.strictEqual( called, true, 'Task executed successfully' ); + } ); + } ); + + QUnit.test( 'Task execution order test', function ( assert ) { + var taskQueue = new mw.mmv.model.TaskQueue(), + order = []; + + taskQueue.push( function () { + order.push( 1 ); + } ); + + taskQueue.push( function () { + var deferred = $.Deferred(); + + order.push( 2 ); + + setTimeout( function () { + deferred.resolve(); + }, 0 ); + + return deferred; + } ); + + taskQueue.push( function () { + order.push( 3 ); + } ); + + return taskQueue.execute().then( function () { + assert.deepEqual( order, [ 1, 2, 3 ], 'Tasks executed in order' ); + } ); + } ); + + QUnit.test( 'Double execution test', function ( assert ) { + var taskQueue = new mw.mmv.model.TaskQueue(), + called = 0; + + taskQueue.push( function () { + called++; + } ); + + return taskQueue.execute().then( function () { + return taskQueue.execute(); + } ).then( function () { + assert.strictEqual( called, 1, 'Task executed only once' ); + } ); + } ); + + QUnit.test( 'Parallel execution test', function ( assert ) { + var taskQueue = new mw.mmv.model.TaskQueue(), + called = 0; + + taskQueue.push( function () { + called++; + } ); + + return $.when( + taskQueue.execute(), + taskQueue.execute() + ).then( function () { + assert.strictEqual( called, 1, 'Task executed only once' ); + } ); + } ); + + QUnit.test( 'Test push after execute', function ( assert ) { + var taskQueue = new mw.mmv.model.TaskQueue(); + + taskQueue.execute(); + + try { + taskQueue.push( function () {} ); + } catch ( e ) { + assert.ok( e, 'Exception thrown when trying to push to an already running queue' ); + } + } ); + + QUnit.test( 'Test failed task', function ( assert ) { + var taskQueue = new mw.mmv.model.TaskQueue(); + + taskQueue.push( function () { + return $.Deferred().reject(); + } ); + + return taskQueue.execute().done( function () { + assert.ok( true, 'Queue promise resolved' ); + } ); + } ); + + QUnit.test( 'Test that tasks wait for each other', function ( assert ) { + var taskQueue = new mw.mmv.model.TaskQueue(), + longRunningTaskFinished = false, + seenFinished = false; + + taskQueue.push( function () { + var deferred = $.Deferred(); + + setTimeout( function () { + longRunningTaskFinished = true; + deferred.resolve(); + }, 0 ); + + return deferred; + } ); + + taskQueue.push( function () { + seenFinished = longRunningTaskFinished; + } ); + + return taskQueue.execute().then( function () { + assert.ok( seenFinished, 'Task waits for previous task to finish' ); + } ); + } ); + + QUnit.test( 'Test cancellation before start', function ( assert ) { + var taskQueue = new mw.mmv.model.TaskQueue(), + triggered = false, + verificationTask = function () { + triggered = true; + }; + + taskQueue.push( verificationTask ); + + taskQueue.cancel(); + + taskQueue.execute() + .done( function () { + assert.ok( false, 'Queue promise rejected' ); + } ) + .fail( function () { + assert.ok( true, 'Queue promise rejected' ); + assert.strictEqual( triggered, false, 'Task was not triggered' ); + } ) + .always( assert.async() ); + } ); + + QUnit.test( 'Test cancellation within callback', function ( assert ) { + var taskQueue = new mw.mmv.model.TaskQueue(), + triggered = false, + verificationTask = function () { + triggered = true; + }; + + taskQueue.push( function () { + taskQueue.cancel(); + } ); + taskQueue.push( verificationTask ); + + taskQueue.execute() + .done( function () { + assert.ok( false, 'Queue promise rejected' ); + } ) + .fail( function () { + assert.ok( true, 'Queue promise rejected' ); + assert.strictEqual( triggered, false, 'Task was not triggered' ); + } ) + .always( assert.async() ); + } ); + + QUnit.test( 'Test cancellation from task', function ( assert ) { + var taskQueue = new mw.mmv.model.TaskQueue(), + triggered = false, + task1 = $.Deferred(), + verificationTask = function () { + triggered = true; + }; + + taskQueue.push( function () { + return task1; + } ); + taskQueue.push( verificationTask ); + + setTimeout( function () { + taskQueue.cancel(); + task1.resolve(); + }, 0 ); + + taskQueue.execute() + .done( function () { + assert.ok( false, 'Queue promise rejected' ); + } ) + .fail( function () { + assert.ok( true, 'Queue promise rejected' ); + assert.strictEqual( triggered, false, 'Task was not triggered' ); + } ) + .always( assert.async() ); + } ); + +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.test.js new file mode 100644 index 00000000..a24241dc --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.test.js @@ -0,0 +1,58 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MultimediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw ) { + QUnit.module( 'mmv.model', QUnit.newMwEnvironment() ); + + QUnit.test( 'Thumbnail constructor sanity check', function ( assert ) { + var width = 23, + height = 42, + url = 'http://example.com/foo.jpg', + thumbnail = new mw.mmv.model.Thumbnail( url, width, height ); + + assert.strictEqual( thumbnail.url, url, 'Url is set correctly' ); + assert.strictEqual( thumbnail.width, width, 'Width is set correctly' ); + assert.strictEqual( thumbnail.height, height, 'Height is set correctly' ); + + try { + thumbnail = new mw.mmv.model.Thumbnail( url, width ); + } catch ( e ) { + assert.ok( e, 'Exception is thrown when parameters are missing' ); + } + } ); + + QUnit.test( 'ThumbnailWidth constructor sanity check', function ( assert ) { + var cssWidth = 23, + cssHeight = 29, + screenWidth = 42, + realWidth = 123, + thumbnailWidth = new mw.mmv.model.ThumbnailWidth( + cssWidth, cssHeight, screenWidth, realWidth ); + + assert.strictEqual( thumbnailWidth.cssWidth, cssWidth, 'Width is set correctly' ); + assert.strictEqual( thumbnailWidth.cssHeight, cssHeight, 'Height is set correctly' ); + assert.strictEqual( thumbnailWidth.screen, screenWidth, 'Screen width is set correctly' ); + assert.strictEqual( thumbnailWidth.real, realWidth, 'Real width is set correctly' ); + + try { + thumbnailWidth = new mw.mmv.model.ThumbnailWidth( cssWidth, screenWidth ); + } catch ( e ) { + assert.ok( e, 'Exception is thrown when parameters are missing' ); + } + } ); + +}( mediaWiki ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Api.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Api.test.js new file mode 100644 index 00000000..371a22a5 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Api.test.js @@ -0,0 +1,270 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MultimediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.provider.Api', QUnit.newMwEnvironment() ); + + QUnit.test( 'Api constructor sanity check', function ( assert ) { + var api = { get: function () {} }, + options = {}, + apiProvider = new mw.mmv.provider.Api( api, options ), + ApiProviderWithNoOptions = new mw.mmv.provider.Api( api ); + + assert.ok( apiProvider ); + assert.ok( ApiProviderWithNoOptions ); + } ); + + QUnit.test( 'apiGetWithMaxAge()', function ( assert ) { + var api = {}, + options = {}, + apiProvider = new mw.mmv.provider.Api( api, options ); + + api.get = this.sandbox.stub(); + apiProvider.apiGetWithMaxAge( {} ); + assert.ok( !( 'maxage' in api.get.getCall( 0 ).args[ 0 ] ), 'maxage is not set by default' ); + assert.ok( !( 'smaxage' in api.get.getCall( 0 ).args[ 0 ] ), 'smaxage is not set by default' ); + + options = { maxage: 123 }; + apiProvider = new mw.mmv.provider.Api( api, options ); + + api.get = this.sandbox.stub(); + apiProvider.apiGetWithMaxAge( {} ); + assert.strictEqual( api.get.getCall( 0 ).args[ 0 ].maxage, 123, 'maxage falls back to provider default' ); + assert.strictEqual( api.get.getCall( 0 ).args[ 0 ].smaxage, 123, 'smaxage falls back to provider default' ); + + api.get = this.sandbox.stub(); + apiProvider.apiGetWithMaxAge( {}, null, 456 ); + assert.strictEqual( api.get.getCall( 0 ).args[ 0 ].maxage, 456, 'maxage can be overridden' ); + assert.strictEqual( api.get.getCall( 0 ).args[ 0 ].smaxage, 456, 'smaxage can be overridden' ); + + api.get = this.sandbox.stub(); + apiProvider.apiGetWithMaxAge( {}, null, null ); + assert.ok( !( 'maxage' in api.get.getCall( 0 ).args[ 0 ] ), 'maxage can be overridden to unset' ); + assert.ok( !( 'smaxage' in api.get.getCall( 0 ).args[ 0 ] ), 'smaxage can be overridden to unset' ); + } ); + + QUnit.test( 'getCachedPromise success', function ( assert ) { + var api = { get: function () {} }, + apiProvider = new mw.mmv.provider.Api( api ), + oldMwLog = mw.log, + promiseSource, + promiseShouldBeCached = false; + + mw.log = function () { + assert.ok( false, 'mw.log should not have been called' ); + }; + + promiseSource = function ( result ) { + return function () { + assert.ok( !promiseShouldBeCached, 'promise was not cached' ); + return $.Deferred().resolve( result ); + }; + }; + + apiProvider.getCachedPromise( 'foo', promiseSource( 1 ) ).done( function ( result ) { + assert.strictEqual( result, 1, 'result comes from the promise source' ); + } ); + + apiProvider.getCachedPromise( 'bar', promiseSource( 2 ) ).done( function ( result ) { + assert.strictEqual( result, 2, 'result comes from the promise source' ); + } ); + + promiseShouldBeCached = true; + apiProvider.getCachedPromise( 'foo', promiseSource( 3 ) ).done( function ( result ) { + assert.strictEqual( result, 1, 'result comes from cache' ); + } ); + + mw.log = oldMwLog; + } ); + + QUnit.test( 'getCachedPromise failure', function ( assert ) { + var api = { get: function () {} }, + apiProvider = new mw.mmv.provider.Api( api ), + oldMwLog = mw.log, + promiseSource, + promiseShouldBeCached = false; + + mw.log = function () { + assert.ok( true, 'mw.log was called' ); + }; + + promiseSource = function ( result ) { + return function () { + assert.ok( !promiseShouldBeCached, 'promise was not cached' ); + return $.Deferred().reject( result ); + }; + }; + + apiProvider.getCachedPromise( 'foo', promiseSource( 1 ) ).fail( function ( result ) { + assert.strictEqual( result, 1, 'result comes from the promise source' ); + } ); + + apiProvider.getCachedPromise( 'bar', promiseSource( 2 ) ).fail( function ( result ) { + assert.strictEqual( result, 2, 'result comes from the promise source' ); + } ); + + promiseShouldBeCached = true; + apiProvider.getCachedPromise( 'foo', promiseSource( 3 ) ).fail( function ( result ) { + assert.strictEqual( result, 1, 'result comes from cache' ); + } ); + + mw.log = oldMwLog; + } ); + + QUnit.test( 'getErrorMessage', function ( assert ) { + var api = { get: function () {} }, + apiProvider = new mw.mmv.provider.Api( api ), + errorMessage; + + errorMessage = apiProvider.getErrorMessage( { + servedby: 'mw1194', + error: { + code: 'unknown_action', + info: 'Unrecognized value for parameter \'action\': FOO' + } + } ); + assert.strictEqual( errorMessage, + 'unknown_action: Unrecognized value for parameter \'action\': FOO', + 'error message is parsed correctly' ); + + assert.strictEqual( apiProvider.getErrorMessage( {} ), 'unknown error', 'missing error message is handled' ); + } ); + + QUnit.test( 'getNormalizedTitle', function ( assert ) { + var api = { get: function () {} }, + apiProvider = new mw.mmv.provider.Api( api ), + title = new mw.Title( 'Image:Stuff.jpg' ), + normalizedTitle; + + normalizedTitle = apiProvider.getNormalizedTitle( title, {} ); + assert.strictEqual( normalizedTitle, title, 'missing normalization block is handled' ); + + normalizedTitle = apiProvider.getNormalizedTitle( title, { + query: { + normalized: [ + { + from: 'Image:Foo.jpg', + to: 'File:Foo.jpg' + } + ] + } + } ); + assert.strictEqual( normalizedTitle, title, 'irrelevant normalization info is skipped' ); + + normalizedTitle = apiProvider.getNormalizedTitle( title, { + query: { + normalized: [ + { + from: 'Image:Stuff.jpg', + to: 'File:Stuff.jpg' + } + ] + } + } ); + assert.strictEqual( normalizedTitle.getPrefixedDb(), 'File:Stuff.jpg', 'normalization happens' ); + } ); + + QUnit.test( 'getQueryField', function ( assert ) { + var api = { get: function () {} }, + apiProvider = new mw.mmv.provider.Api( api ), + done = assert.async( 3 ), + data; + + data = { + query: { + imageusage: [ + { + pageid: 736, + ns: 0, + title: 'Albert Einstein' + } + ] + } + }; + + apiProvider.getQueryField( 'imageusage', data ).then( function ( field ) { + assert.strictEqual( field, data.query.imageusage, 'specified field is found' ); + done(); + } ); + apiProvider.getQueryField( 'imageusage', {} ).fail( function () { + assert.ok( true, 'promise rejected when data is missing' ); + done(); + } ); + + apiProvider.getQueryField( 'imageusage', { data: { query: {} } } ).fail( function () { + assert.ok( true, 'promise rejected when field is missing' ); + done(); + } ); + } ); + + QUnit.test( 'getQueryPage', function ( assert ) { + var api = { get: function () {} }, + apiProvider = new mw.mmv.provider.Api( api ), + title = new mw.Title( 'File:Stuff.jpg' ), + titleWithNamespaceAlias = new mw.Title( 'Image:Stuff.jpg' ), + otherTitle = new mw.Title( 'File:Foo.jpg' ), + done = assert.async( 6 ), + data; + + data = { + normalized: [ + { + from: 'Image:Stuff.jpg', + to: 'File:Stuff.jpg' + } + ], + query: { + pages: { + '-1': { + title: 'File:Stuff.jpg' + } + } + } + }; + + apiProvider.getQueryPage( title, data ).then( function ( field ) { + assert.strictEqual( field, data.query.pages[ '-1' ], 'specified page is found' ); + done(); + } ); + + apiProvider.getQueryPage( titleWithNamespaceAlias, data ).then( function ( field ) { + assert.strictEqual( field, data.query.pages[ '-1' ], + 'specified page is found even if its title was normalized' ); + done(); + } ); + + apiProvider.getQueryPage( otherTitle, {} ).fail( function () { + assert.ok( true, 'promise rejected when page has different title' ); + done(); + } ); + + apiProvider.getQueryPage( title, {} ).fail( function () { + assert.ok( true, 'promise rejected when data is missing' ); + done(); + } ); + + apiProvider.getQueryPage( title, { data: { query: {} } } ).fail( function () { + assert.ok( true, 'promise rejected when pages are missing' ); + done(); + } ); + + apiProvider.getQueryPage( title, { data: { query: { pages: {} } } } ).fail( function () { + assert.ok( true, 'promise rejected when pages are empty' ); + done(); + } ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.FileRepoInfo.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.FileRepoInfo.test.js new file mode 100644 index 00000000..80771784 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.FileRepoInfo.test.js @@ -0,0 +1,126 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MultimediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.provider.FileRepoInfo', QUnit.newMwEnvironment() ); + + QUnit.test( 'FileRepoInfo constructor sanity check', function ( assert ) { + var api = { get: function () {} }, + fileRepoInfoProvider = new mw.mmv.provider.FileRepoInfo( api ); + + assert.ok( fileRepoInfoProvider ); + } ); + + QUnit.test( 'FileRepoInfo get test', function ( assert ) { + var apiCallCount = 0, + api = { get: function () { + apiCallCount++; + return $.Deferred().resolve( { + query: { + repos: [ + { + name: 'shared', + displayname: 'Wikimedia Commons', + rootUrl: '//upload.beta.wmflabs.org/wikipedia/commons', + local: false, + url: '//upload.beta.wmflabs.org/wikipedia/commons', + thumbUrl: '//upload.beta.wmflabs.org/wikipedia/commons/thumb', + initialCapital: true, + descBaseUrl: '//commons.wikimedia.beta.wmflabs.org/wiki/File:', + scriptDirUrl: '//commons.wikimedia.beta.wmflabs.org/w', + fetchDescription: true, + favicon: 'http://en.wikipedia.org/favicon.ico' + }, + { + name: 'wikimediacommons', + displayname: 'Wikimedia Commons', + rootUrl: '//upload.beta.wmflabs.org/wikipedia/en', + local: false, + url: '//upload.beta.wmflabs.org/wikipedia/en', + thumbUrl: '//upload.beta.wmflabs.org/wikipedia/en/thumb', + initialCapital: true, + scriptDirUrl: 'http://commons.wikimedia.org/w', + fetchDescription: true, + descriptionCacheExpiry: 43200, + apiurl: 'http://commons.wikimedia.org/w/api.php', + articlepath: '/wiki/$1', + server: '//commons.wikimedia.org', + favicon: '//commons.wikimedia.org/favicon.ico' + }, + { + name: 'local', + displayname: null, + rootUrl: '//upload.beta.wmflabs.org/wikipedia/en', + local: true, + url: '//upload.beta.wmflabs.org/wikipedia/en', + thumbUrl: '//upload.beta.wmflabs.org/wikipedia/en/thumb', + initialCapital: true, + scriptDirUrl: '/w', + favicon: 'http://en.wikipedia.org/favicon.ico' + } + ] + } + } ); + } }, + fileRepoInfoProvider = new mw.mmv.provider.FileRepoInfo( api ); + + return fileRepoInfoProvider.get().then( function ( repos ) { + assert.strictEqual( repos.shared.displayName, + 'Wikimedia Commons', 'displayName is set correctly' ); + assert.strictEqual( repos.shared.favIcon, + 'http://en.wikipedia.org/favicon.ico', 'favIcon is set correctly' ); + assert.strictEqual( repos.shared.isLocal, false, 'isLocal is set correctly' ); + assert.strictEqual( repos.shared.descBaseUrl, + '//commons.wikimedia.beta.wmflabs.org/wiki/File:', 'descBaseUrl is set correctly' ); + + assert.strictEqual( repos.wikimediacommons.displayName, + 'Wikimedia Commons', 'displayName is set correctly' ); + assert.strictEqual( repos.wikimediacommons.favIcon, + '//commons.wikimedia.org/favicon.ico', 'favIcon is set correctly' ); + assert.strictEqual( repos.wikimediacommons.isLocal, false, 'isLocal is set correctly' ); + assert.strictEqual( repos.wikimediacommons.apiUrl, + 'http://commons.wikimedia.org/w/api.php', 'apiUrl is set correctly' ); + assert.strictEqual( repos.wikimediacommons.server, + '//commons.wikimedia.org', 'server is set correctly' ); + assert.strictEqual( repos.wikimediacommons.articlePath, + '/wiki/$1', 'articlePath is set correctly' ); + + assert.strictEqual( repos.local.displayName, null, 'displayName is set correctly' ); + assert.strictEqual( repos.local.favIcon, + 'http://en.wikipedia.org/favicon.ico', 'favIcon is set correctly' ); + assert.strictEqual( repos.local.isLocal, true, 'isLocal is set correctly' ); + } ).then( function () { + // call the data provider a second time to check caching + return fileRepoInfoProvider.get(); + } ).then( function () { + assert.strictEqual( apiCallCount, 1 ); + } ); + } ); + + QUnit.test( 'FileRepoInfo fail test', function ( assert ) { + var api = { get: function () { + return $.Deferred().resolve( {} ); + } }, + done = assert.async(), + fileRepoInfoProvider = new mw.mmv.provider.FileRepoInfo( api ); + + fileRepoInfoProvider.get().fail( function () { + assert.ok( true, 'promise rejected when no data is returned' ); + done(); + } ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.GuessedThumbnailInfo.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.GuessedThumbnailInfo.test.js new file mode 100644 index 00000000..93cd51fa --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.GuessedThumbnailInfo.test.js @@ -0,0 +1,280 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw ) { + QUnit.module( 'mmv.provider.GuessedThumbnailInfo', QUnit.newMwEnvironment() ); + + QUnit.test( 'Constructor sanity check', function ( assert ) { + var provider = new mw.mmv.provider.GuessedThumbnailInfo(); + assert.ok( provider, 'Constructor call successful' ); + } ); + + QUnit.test( 'get()', function ( assert ) { + var provider = new mw.mmv.provider.GuessedThumbnailInfo(), + file = new mw.Title( 'File:Copyleft.svg' ), + sampleUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/180px-Copyleft.svg.png', + width = 300, + originalWidth = 512, + originalHeight = 512, + resultUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png', + done = assert.async(), + result; + + provider.getUrl = function () { return resultUrl; }; + result = provider.get( file, sampleUrl, width, originalWidth, originalHeight ); + assert.ok( result.then, 'Result is a promise' ); + assert.strictEqual( result.state(), 'resolved', 'Result is resolved' ); + result.then( function ( thumbnailInfo ) { + assert.ok( thumbnailInfo.width, 'Width is set' ); + assert.ok( thumbnailInfo.height, 'Height is set' ); + assert.strictEqual( thumbnailInfo.url, resultUrl, 'URL is set' ); + done(); + } ); + + provider.getUrl = function () { return undefined; }; + result = provider.get( file, sampleUrl, width, originalWidth, originalHeight ); + assert.ok( result.then, 'Result is a promise' ); + assert.strictEqual( result.state(), 'rejected', 'Result is rejected' ); + } ); + + QUnit.test( 'getUrl()', function ( assert ) { + var provider = new mw.mmv.provider.GuessedThumbnailInfo(), + file = new mw.Title( 'File:Elizabeth_I_George_Gower.jpg' ), + originalWidth = 922, + originalHeight = 968, + width, + sampleUrl, + expectedUrl, + resultUrl; + + sampleUrl = 'http://upload.wikimedia.org/wikipedia/commons/7/78/Elizabeth_I_George_Gower.jpg'; + width = 1000; + expectedUrl = 'http://upload.wikimedia.org/wikipedia/commons/7/78/Elizabeth_I_George_Gower.jpg'; + resultUrl = provider.getUrl( file, sampleUrl, width, originalWidth, originalHeight ); + assert.strictEqual( resultUrl, expectedUrl, 'Simple case - full image, needs no resize' ); + + sampleUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/7/78/Elizabeth_I_George_Gower.jpg/180px-Elizabeth_I_George_Gower.jpg'; + width = 400; + expectedUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/7/78/Elizabeth_I_George_Gower.jpg/400px-Elizabeth_I_George_Gower.jpg'; + resultUrl = provider.getUrl( file, sampleUrl, width, originalWidth, originalHeight ); + assert.strictEqual( resultUrl, expectedUrl, 'Mostly simple case - just need to replace size' ); + + sampleUrl = 'http://upload.wikimedia.org/wikipedia/commons/7/78/Elizabeth_I_George_Gower.jpg'; + width = 400; + expectedUrl = undefined; + resultUrl = provider.getUrl( file, sampleUrl, width, originalWidth, originalHeight ); + assert.strictEqual( resultUrl, expectedUrl, 'We bail on hard case - full to thumbnail' ); + + sampleUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/7/78/Elizabeth_I_George_Gower.jpg/180px-Elizabeth_I_George_Gower.jpg'; + width = 1000; + expectedUrl = 'http://upload.wikimedia.org/wikipedia/commons/7/78/Elizabeth_I_George_Gower.jpg'; + resultUrl = provider.getUrl( file, sampleUrl, width, originalWidth, originalHeight ); + assert.strictEqual( resultUrl, expectedUrl, 'Thumbnail to full-size, image with limited size' ); + + file = new mw.Title( 'File:Ranunculus_gmelinii_NRCS-2.tiff' ); + sampleUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Ranunculus_gmelinii_NRCS-2.tiff/lossy-page1-428px-Ranunculus_gmelinii_NRCS-2.tiff.jpg'; + width = 2000; + originalWidth = 1500; + originalHeight = 2100; + expectedUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Ranunculus_gmelinii_NRCS-2.tiff/lossy-page1-1500px-Ranunculus_gmelinii_NRCS-2.tiff.jpg'; + resultUrl = provider.getUrl( file, sampleUrl, width, originalWidth, originalHeight ); + assert.strictEqual( resultUrl, expectedUrl, 'Thumbnail to full-size, image which cannot be displayed directly' ); + + file = new mw.Title( 'File:Copyleft.svg' ); + sampleUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/180px-Copyleft.svg.png'; + width = 1000; + originalWidth = 512; + originalHeight = 512; + expectedUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/1000px-Copyleft.svg.png'; + resultUrl = provider.getUrl( file, sampleUrl, width, originalWidth, originalHeight ); + assert.strictEqual( resultUrl, expectedUrl, 'Thumbnail to "full-size", image with unlimited size' ); + } ); + + QUnit.test( 'needsOriginal()', function ( assert ) { + var provider = new mw.mmv.provider.GuessedThumbnailInfo(), + file = new mw.Title( 'File:Copyleft.svg' ); + + assert.ok( !provider.needsOriginal( file, 100, 1000 ), 'Thumbnail of an SVG smaller than the original size doesn\'t need original' ); + assert.ok( !provider.needsOriginal( file, 1000, 1000 ), 'Thumbnail of an SVG equal to the original size doesn\'t need original' ); + assert.ok( !provider.needsOriginal( file, 2000, 1000 ), 'Thumbnail of an SVG bigger than the original size doesn\'t need original' ); + + file = new mw.Title( 'File:Foo.png' ); + + assert.ok( !provider.needsOriginal( file, 100, 1000 ), 'Thumbnail of a PNG smaller than the original size doesn\'t need original' ); + assert.ok( provider.needsOriginal( file, 1000, 1000 ), 'Thumbnail of a PNG equal to the original size needs original' ); + assert.ok( provider.needsOriginal( file, 2000, 1000 ), 'Thumbnail of a PNG bigger than the original size needs original' ); + } ); + + QUnit.test( 'isFullSizeUrl()', function ( assert ) { + var provider = new mw.mmv.provider.GuessedThumbnailInfo(), + file = new mw.Title( 'File:Copyleft.svg' ); + + assert.ok( !provider.isFullSizeUrl( 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png', file ), + 'Thumbnail url recognized as not being full size' ); + assert.ok( provider.isFullSizeUrl( 'http://upload.wikimedia.org/wikipedia/commons/8/8b/Copyleft.svg', file ), + 'Original url recognized as being full size' ); + } ); + + QUnit.test( 'obscureFilename()', function ( assert ) { + var provider = new mw.mmv.provider.GuessedThumbnailInfo(), + file = new mw.Title( 'File:Copyleft.svg' ); + + assert.strictEqual( provider.obscureFilename( 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png', file ), + 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/<filename>/300px-<filename>.png', 'Filename correctly obscured' ); + + file = new mw.Title( 'File:Hoag\'s_object.jpg' ); + + assert.strictEqual( provider.obscureFilename( 'http://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Hoag%27s_object.jpg/180px-Hoag%27s_object.jpg', file ), + 'http://upload.wikimedia.org/wikipedia/commons/thumb/d/da/<filename>/180px-<filename>', 'Filename with urlencoded character correctly obscured' ); + } ); + + QUnit.test( 'restoreFilename()', function ( assert ) { + var provider = new mw.mmv.provider.GuessedThumbnailInfo(), + file = new mw.Title( 'File:Copyleft.svg' ); + + assert.strictEqual( provider.restoreFilename( 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/<filename>/300px-<filename>.png', file ), + 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png', 'Filename correctly restored' ); + + } ); + + QUnit.test( 'canHaveLargerThumbnailThanOriginal()', function ( assert ) { + var provider = new mw.mmv.provider.GuessedThumbnailInfo(), + file = new mw.Title( 'File:Copyleft.svg' ); + + assert.ok( provider.canHaveLargerThumbnailThanOriginal( file ), 'SVG can have a larger thumbnail than the original' ); + + file = new mw.Title( 'File:Foo.jpg' ); + + assert.ok( !provider.canHaveLargerThumbnailThanOriginal( file ), 'JPG can\'t have a larger thumbnail than the original' ); + + file = new mw.Title( 'File:Foo.png' ); + + assert.ok( !provider.canHaveLargerThumbnailThanOriginal( file ), 'PNG can\'t have a larger thumbnail than the original' ); + + file = new mw.Title( 'File:Foo.jpeg' ); + + assert.ok( !provider.canHaveLargerThumbnailThanOriginal( file ), 'JPEG can\'t have a larger thumbnail than the original' ); + + file = new mw.Title( 'File:Foo.tiff' ); + + assert.ok( !provider.canHaveLargerThumbnailThanOriginal( file ), 'TIFF can\'t have a larger thumbnail than the original' ); + + file = new mw.Title( 'File:Foo.gif' ); + + assert.ok( !provider.canHaveLargerThumbnailThanOriginal( file ), 'GIF can\'t have a larger thumbnail than the original' ); + } ); + + QUnit.test( 'canBeDisplayedInBrowser()', function ( assert ) { + var provider = new mw.mmv.provider.GuessedThumbnailInfo(), + file = new mw.Title( 'File:Copyleft.svg' ); + + assert.ok( !provider.canBeDisplayedInBrowser( file ), 'SVG can\'t be displayed as-is in the browser' ); + + file = new mw.Title( 'File:Foo.jpg' ); + + assert.ok( provider.canBeDisplayedInBrowser( file ), 'JPG can be displayed as-is in the browser' ); + + file = new mw.Title( 'File:Foo.png' ); + + assert.ok( provider.canBeDisplayedInBrowser( file ), 'PNG can be displayed as-is in the browser' ); + + file = new mw.Title( 'File:Foo.jpeg' ); + + assert.ok( provider.canBeDisplayedInBrowser( file ), 'JPEG can be displayed as-is in the browser' ); + + file = new mw.Title( 'File:Foo.tiff' ); + + assert.ok( !provider.canBeDisplayedInBrowser( file ), 'TIFF can\'t be displayed as-is in the browser' ); + + file = new mw.Title( 'File:Foo.gif' ); + + assert.ok( provider.canBeDisplayedInBrowser( file ), 'GIF can be displayed as-is in the browser' ); + } ); + + QUnit.test( 'guessWidth()', function ( assert ) { + var provider = new mw.mmv.provider.GuessedThumbnailInfo(), + file = new mw.Title( 'File:Copyleft.svg' ); + + assert.strictEqual( provider.guessWidth( file, 100, 1000 ), 100, 'Width correctly guessed for SVG thumbnail smaller than the original' ); + assert.strictEqual( provider.guessWidth( file, 2000, 1000 ), 2000, 'Width correctly guessed for SVG thumbnail bigger than the original' ); + + file = new mw.Title( 'File:Copyleft.jpg' ); + + assert.strictEqual( provider.guessWidth( file, 100, 1000 ), 100, 'Width correctly guessed for JPG thumbnail smaller than the original' ); + assert.strictEqual( provider.guessWidth( file, 2000, 1000 ), 1000, 'Width correctly guessed for JPG thumbnail bigger than the original' ); + } ); + + QUnit.test( 'guessHeight()', function ( assert ) { + var provider = new mw.mmv.provider.GuessedThumbnailInfo(), + file = new mw.Title( 'File:Copyleft.svg' ); + + assert.strictEqual( provider.guessHeight( file, 100, 1000, 500 ), 50, 'Height correctly guessed for SVG thumbnail smaller than the original' ); + assert.strictEqual( provider.guessHeight( file, 2000, 1000, 500 ), 1000, 'Height correctly guessed for SVG thumbnail bigger than the original' ); + + file = new mw.Title( 'File:Copyleft.jpg' ); + + assert.strictEqual( provider.guessHeight( file, 100, 1000, 500 ), 50, 'Height correctly guessed for JPG thumbnail smaller than the original' ); + assert.strictEqual( provider.guessHeight( file, 2000, 1000, 500 ), 500, 'Height correctly guessed for JPG thumbnail bigger than the original' ); + } ); + + QUnit.test( 'replaceSize()', function ( assert ) { + var provider = new mw.mmv.provider.GuessedThumbnailInfo(), + file = new mw.Title( 'File:Copyleft.svg' ); + + assert.strictEqual( provider.replaceSize( file, 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png', 220 ), + 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/220px-Copyleft.svg.png', 'Incorrect size correctly replaced' ); + assert.strictEqual( provider.replaceSize( file, 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png', 300 ), + 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png', 'Identical size correctly left the same' ); + assert.strictEqual( provider.replaceSize( file, 'http://upload.wikimedia.org/wikipedia/commons/8/8b/Copyleft.svg', 220 ), + undefined, 'Returns undefined when it cannot handle the URL' ); + + file = new mw.Title( 'File:Copyleft-300px.svg' ); + assert.strictEqual( provider.replaceSize( file, 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft-300px.svg/300px-Copyleft-300px.svg.png', 220 ), + 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft-300px.svg/220px-Copyleft-300px.svg.png', 'Works with strange filename' ); + + file = new mw.Title( 'File:Ranunculus_gmelinii_NRCS-2.tiff' ); + assert.strictEqual( provider.replaceSize( file, 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Ranunculus_gmelinii_NRCS-2.tiff/lossy-page1-428px-Ranunculus_gmelinii_NRCS-2.tiff.jpg', 220 ), + 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Ranunculus_gmelinii_NRCS-2.tiff/lossy-page1-220px-Ranunculus_gmelinii_NRCS-2.tiff.jpg', 'Works with extra parameters' ); + } ); + + QUnit.test( 'guessFullUrl()', function ( assert ) { + var provider = new mw.mmv.provider.GuessedThumbnailInfo(), + file = new mw.Title( 'File:Copyleft.svg' ), + fullUrl = 'http://upload.wikimedia.org/wikipedia/commons/8/8b/Copyleft.svg', + sampleUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png', + result; + + result = provider.guessFullUrl( file, sampleUrl ); + + assert.strictEqual( result, fullUrl, 'guessFullUrl returns correct full URL for SVG' ); + + file = new mw.Title( 'File:அணில்-3-தென்னையின்_வளர்நிலை.jpg' ); + fullUrl = 'https://upload.wikimedia.org/wikipedia/commons/1/15/%E0%AE%85%E0%AE%A3%E0%AE%BF%E0%AE%B2%E0%AF%8D-3-%E0%AE%A4%E0%AF%86%E0%AE%A9%E0%AF%8D%E0%AE%A9%E0%AF%88%E0%AE%AF%E0%AE%BF%E0%AE%A9%E0%AF%8D_%E0%AE%B5%E0%AE%B3%E0%AE%B0%E0%AF%8D%E0%AE%A8%E0%AE%BF%E0%AE%B2%E0%AF%88.jpg'; + sampleUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/%E0%AE%85%E0%AE%A3%E0%AE%BF%E0%AE%B2%E0%AF%8D-3-%E0%AE%A4%E0%AF%86%E0%AE%A9%E0%AF%8D%E0%AE%A9%E0%AF%88%E0%AE%AF%E0%AE%BF%E0%AE%A9%E0%AF%8D_%E0%AE%B5%E0%AE%B3%E0%AE%B0%E0%AF%8D%E0%AE%A8%E0%AE%BF%E0%AE%B2%E0%AF%88.jpg/800px-%E0%AE%85%E0%AE%A3%E0%AE%BF%E0%AE%B2%E0%AF%8D-3-%E0%AE%A4%E0%AF%86%E0%AE%A9%E0%AF%8D%E0%AE%A9%E0%AF%88%E0%AE%AF%E0%AE%BF%E0%AE%A9%E0%AF%8D_%E0%AE%B5%E0%AE%B3%E0%AE%B0%E0%AF%8D%E0%AE%A8%E0%AE%BF%E0%AE%B2%E0%AF%88.jpg'; + + result = provider.guessFullUrl( file, sampleUrl ); + + assert.strictEqual( result, fullUrl, 'guessFullUrl returns correct full URL for JPG with unicode name' ); + + file = new mw.Title( 'File:அணில்-3-தென்னையின்_வளர்நிலை.jpg' ); + sampleUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/அணில்-3-தென்னையின்_வளர்நிலை.jpg/800px-அணில்-3-தென்னையின்_வளர்நிலை.jpg'; + + result = provider.guessFullUrl( file, sampleUrl ); + + assert.strictEqual( result, undefined, 'guessFullUrl bails out when URL encoding is not as expected' ); + } ); +}( mediaWiki ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Image.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Image.test.js new file mode 100644 index 00000000..76b84afe --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Image.test.js @@ -0,0 +1,200 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MultimediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.provider.Image', QUnit.newMwEnvironment() ); + + QUnit.test( 'Image constructor sanity check', function ( assert ) { + var imageProvider = new mw.mmv.provider.Image(); + + assert.ok( imageProvider ); + } ); + + QUnit.test( 'Image load success', function ( assert ) { + var url = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0' + + 'iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH' + + '8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC', + imageProvider = new mw.mmv.provider.Image(); + + imageProvider.imagePreloadingSupported = function () { return false; }; + imageProvider.performance.recordEntry = $.noop; + + return imageProvider.get( url ).then( function ( image ) { + assert.ok( image instanceof HTMLImageElement, + 'success handler was called with the image element' ); + assert.strictEqual( image.src, url, 'image src is correct' ); + } ); + } ); + + QUnit.test( 'Image caching', function ( assert ) { + var url = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0' + + 'iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH' + + '8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC', + url2 = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', + result, + imageProvider = new mw.mmv.provider.Image(); + + imageProvider.imagePreloadingSupported = function () { return false; }; + imageProvider.performance.recordEntry = $.noop; + + return QUnit.whenPromisesComplete( + imageProvider.get( url ).then( function ( image ) { + result = image; + assert.ok( image instanceof HTMLImageElement, + 'success handler was called with the image element' ); + assert.strictEqual( image.src, url, 'image src is correct' ); + } ), + + imageProvider.get( url ).then( function ( image ) { + assert.strictEqual( image, result, 'image element is cached and not regenerated' ); + assert.strictEqual( image.src, url, 'image src is correct' ); + } ), + + imageProvider.get( url2 ).then( function ( image ) { + assert.notStrictEqual( image, result, 'image element for different url is not cached' ); + assert.strictEqual( image.src, url2, 'image src is correct' ); + } ) + ); + } ); + + QUnit.test( 'Image load XHR progress funneling', function ( assert ) { + var i = 0, + imageProvider = new mw.mmv.provider.Image(), + oldPerformance = imageProvider.performance, + fakeURL = 'fakeURL', + response = 'response', + done1 = assert.async(), + done2 = assert.async(); + + imageProvider.performance.delay = 0; + imageProvider.imagePreloadingSupported = function () { return true; }; + imageProvider.rawGet = function () { return $.Deferred().resolve(); }; + + imageProvider.performance.newXHR = function () { + return { readyState: 4, + response: response, + send: function () { + var self = this; + + // The timeout is necessary because without it notify() happens before + // the imageProvider has time to chain its progress() to the returned deferred + setTimeout( function () { + self.onprogress( { lengthComputable: true, loaded: 10, total: 20 } ); + self.onreadystatechange(); + } ); + }, + + open: $.noop }; + }; + + imageProvider.performance.recordEntry = function ( type, total, url ) { + assert.strictEqual( type, 'image', 'Type matches' ); + assert.strictEqual( url, fakeURL, 'URL matches' ); + done1(); + + imageProvider.performance = oldPerformance; + + return $.Deferred().resolve(); + }; + + imageProvider.get( fakeURL ) + .fail( function () { + assert.ok( false, 'Image failed to (pretend to) load' ); + done2(); + } ) + .then( function () { + assert.ok( true, 'Image was pretend-loaded' ); + done2(); + } ) + .progress( function ( response, percent ) { + if ( i === 0 ) { + assert.strictEqual( percent, 50, 'Correctly propagated a 50% progress event' ); + assert.strictEqual( response, response, 'Partial response propagated' ); + } else if ( i === 1 ) { + assert.strictEqual( percent, 100, 'Correctly propagated a 100% progress event' ); + assert.strictEqual( response, response, 'Partial response propagated' ); + } else { + assert.ok( false, 'Only 2 progress events should propagate' ); + } + + i++; + } ); + } ); + + QUnit.test( 'Image load fail', function ( assert ) { + var imageProvider = new mw.mmv.provider.Image(), + oldMwLog = mw.log, + done = assert.async(), + mwLogCalled = false; + + imageProvider.imagePreloadingSupported = function () { return false; }; + imageProvider.performance.recordEntry = $.noop; + mw.log = function () { mwLogCalled = true; }; + + imageProvider.get( 'doesntexist.png' ).fail( function () { + assert.ok( true, 'fail handler was called' ); + assert.ok( mwLogCalled, 'mw.log was called' ); + mw.log = oldMwLog; + done(); + } ); + } ); + + QUnit.test( 'Image load with preloading supported', function ( assert ) { + var url = mw.config.get( 'wgExtensionAssetsPath' ) + '/MultimediaViewer/resources/mmv/img/expand.svg', + imageProvider = new mw.mmv.provider.Image(), + endsWith = function ( a, b ) { return a.indexOf( b ) === a.length - b.length; }; + + imageProvider.imagePreloadingSupported = function () { return true; }; + imageProvider.performance = { + record: function () { return $.Deferred().resolve(); } + }; + + return imageProvider.get( url ).then( function ( image ) { + // can't test equality as browsers transform this to a full URL + assert.ok( endsWith( image.src, url ), 'local image loaded with correct source' ); + } ); + } ); + + QUnit.test( 'Failed image load with preloading supported', function ( assert ) { + var url = 'nosuchimage.png', + imageProvider = new mw.mmv.provider.Image(), + done = assert.async(); + + imageProvider.imagePreloadingSupported = function () { return true; }; + imageProvider.performance = { + record: function () { return $.Deferred().resolve(); } + }; + + imageProvider.get( url ).fail( function () { + assert.ok( true, 'Fail callback called for non-existing image' ); + done(); + } ); + } ); + + QUnit.test( 'imageQueryParameter', function ( assert ) { + var imageProvider = new mw.mmv.provider.Image( 'foo' ); + + imageProvider.imagePreloadingSupported = function () { return false; }; + imageProvider.rawGet = function () { return $.Deferred().resolve(); }; + + imageProvider.performance.recordEntry = function ( type, total, url ) { + assert.strictEqual( url, 'http://www.wikipedia.org/?foo', 'Extra parameter added' ); + }; + + imageProvider.get( 'http://www.wikipedia.org/' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ImageInfo.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ImageInfo.test.js new file mode 100644 index 00000000..3daaac12 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ImageInfo.test.js @@ -0,0 +1,241 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MultimediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.provider.ImageInfo', QUnit.newMwEnvironment() ); + + QUnit.test( 'ImageInfo constructor sanity check', function ( assert ) { + var api = { get: function () {} }, + imageInfoProvider = new mw.mmv.provider.ImageInfo( api ); + + assert.ok( imageInfoProvider ); + } ); + + QUnit.test( 'ImageInfo get test', function ( assert ) { + var apiCallCount = 0, + api = { get: function () { + apiCallCount++; + return $.Deferred().resolve( { + query: { + pages: { + '-1': { + ns: 6, + title: 'File:Stuff.jpg', + missing: '', + imagerepository: 'shared', + imageinfo: [ + { + timestamp: '2013-08-25T14:41:02Z', + userid: '3053121', + size: 346684, + width: 720, + height: 1412, + comment: 'User created page with UploadWizard', + url: 'https://upload.wikimedia.org/wikipedia/commons/1/19/Stuff.jpg', + descriptionurl: 'https://commons.wikimedia.org/wiki/File:Stuff.jpg', + sha1: 'a1ba23d471f4dad208b71c143e2e105a0e3032db', + metadata: [], + extmetadata: { + ObjectName: { + value: 'Some stuff', + source: 'commons-templates' + }, + License: { + value: 'cc0', + source: 'commons-templates', + hidden: '' + }, + LicenseShortName: { + value: 'CC0', + source: 'commons-templates' + }, + UsageTerms: { + value: 'Creative Commons Public Domain Dedication', + source: 'commons-templates' + }, + LicenseUrl: { + value: 'http://creativecommons.org/publicdomain/zero/1.0/', + source: 'commons-templates' + }, + GPSLatitude: { + value: '90.000000', + source: 'commons-desc-page' + }, + GPSLongitude: { + value: ' 180.000000', + source: 'commons-desc-page' + }, + ImageDescription: { + value: 'Wikis stuff', + source: 'commons-desc-page' + }, + DateTimeOriginal: { + value: '<time class="dtstart" datetime="2009-02-18">18 February 2009</time>\u00a0(according to <a href="//en.wikipedia.org/wiki/Exchangeable_image_file_format" class="extiw" title="en:Exchangeable image file format">EXIF</a> data)', + source: 'commons-desc-page' + }, + DateTime: { + value: '2013-08-25T14:41:02Z', + source: 'commons-desc-page' + }, + Credit: { + value: 'Wikipedia', + source: 'commons-desc-page', + hidden: '' + }, + Artist: { + value: 'John Smith', + source: 'commons-desc-page' + }, + AuthorCount: { + value: '2', + source: 'commons-desc-page' + }, + Attribution: { + value: 'By John Smith', + source: 'commons-desc-page' + }, + Permission: { + value: 'Do not use. Ever.', + source: 'commons-desc-page' + }, + AttributionRequired: { + value: 'no', + source: 'commons-desc-page' + }, + NonFree: { + value: 'yes', + source: 'commons-desc-page' + }, + Restrictions: { + value: 'trademarked|insignia', + source: 'commons-desc-page' + }, + DeletionReason: { + value: 'copyvio', + source: 'commons-desc-page' + } + }, + mime: 'image/jpeg', + mediatype: 'BITMAP' + } + ] + } + } + } + } ); + } }, + file = new mw.Title( 'File:Stuff.jpg' ), + imageInfoProvider = new mw.mmv.provider.ImageInfo( api ); + + return imageInfoProvider.get( file ).then( function ( image ) { + assert.strictEqual( image.title.getPrefixedDb(), 'File:Stuff.jpg', 'title is set correctly' ); + assert.strictEqual( image.name, 'Some stuff', 'name is set correctly' ); + assert.strictEqual( image.size, 346684, 'size is set correctly' ); + assert.strictEqual( image.width, 720, 'width is set correctly' ); + assert.strictEqual( image.height, 1412, 'height is set correctly' ); + assert.strictEqual( image.mimeType, 'image/jpeg', 'mimeType is set correctly' ); + assert.strictEqual( image.url, 'https://upload.wikimedia.org/wikipedia/commons/1/19/Stuff.jpg', 'url is set correctly' ); + assert.strictEqual( image.descriptionUrl, 'https://commons.wikimedia.org/wiki/File:Stuff.jpg', 'descriptionUrl is set correctly' ); + assert.strictEqual( image.repo, 'shared', 'repo is set correctly' ); + assert.strictEqual( image.uploadDateTime, '2013-08-25T14:41:02Z', 'uploadDateTime is set correctly' ); + assert.strictEqual( image.anonymizedUploadDateTime, '20130825000000', 'anonymizedUploadDateTime is set correctly' ); + assert.strictEqual( image.creationDateTime, '18 February 2009\u00a0(according to EXIF data)', 'creationDateTime is set correctly' ); + assert.strictEqual( image.description, 'Wikis stuff', 'description is set correctly' ); + assert.strictEqual( image.source, 'Wikipedia', 'source is set correctly' ); + assert.strictEqual( image.author, 'John Smith', 'author is set correctly' ); + assert.strictEqual( image.authorCount, 2, 'author count is set correctly' ); + assert.strictEqual( image.attribution, 'By John Smith', 'attribution is set correctly' ); + assert.strictEqual( image.license.shortName, 'CC0', 'license short name is set correctly' ); + assert.strictEqual( image.license.internalName, 'cc0', 'license internal name is set correctly' ); + assert.strictEqual( image.license.longName, 'Creative Commons Public Domain Dedication', 'license long name is set correctly' ); + assert.strictEqual( image.license.deedUrl, 'http://creativecommons.org/publicdomain/zero/1.0/', 'license URL is set correctly' ); + assert.strictEqual( image.license.attributionRequired, false, 'Attribution required flag is honored' ); + assert.strictEqual( image.license.nonFree, true, 'Non-free flag is honored' ); + assert.strictEqual( image.permission, 'Do not use. Ever.', 'permission is set correctly' ); + assert.strictEqual( image.deletionReason, 'copyvio', 'permission is set correctly' ); + assert.strictEqual( image.latitude, 90, 'latitude is set correctly' ); + assert.strictEqual( image.longitude, 180, 'longitude is set correctly' ); + assert.deepEqual( image.restrictions, [ 'trademarked', 'insignia' ], 'restrictions is set correctly' ); + } ).then( function () { + // call the data provider a second time to check caching + return imageInfoProvider.get( file ); + } ).then( function () { + assert.strictEqual( apiCallCount, 1 ); + } ); + } ); + + QUnit.test( 'ImageInfo fail test', function ( assert ) { + var api = { get: function () { + return $.Deferred().resolve( {} ); + } }, + file = new mw.Title( 'File:Stuff.jpg' ), + done = assert.async(), + imageInfoProvider = new mw.mmv.provider.ImageInfo( api ); + + imageInfoProvider.get( file ).fail( function () { + assert.ok( true, 'promise rejected when no data is returned' ); + done(); + } ); + } ); + + QUnit.test( 'ImageInfo fail test 2', function ( assert ) { + var api = { get: function () { + return $.Deferred().resolve( { + query: { + pages: { + '-1': { + title: 'File:Stuff.jpg' + } + } + } + } ); + } }, + file = new mw.Title( 'File:Stuff.jpg' ), + done = assert.async(), + imageInfoProvider = new mw.mmv.provider.ImageInfo( api ); + + imageInfoProvider.get( file ).fail( function () { + assert.ok( true, 'promise rejected when imageinfo is missing' ); + done(); + } ); + } ); + + QUnit.test( 'ImageInfo missing page test', function ( assert ) { + var api = { get: function () { + return $.Deferred().resolve( { + query: { + pages: { + '-1': { + title: 'File:Stuff.jpg', + missing: '', + imagerepository: '' + } + } + } + } ); + } }, + file = new mw.Title( 'File:Stuff.jpg' ), + done = assert.async(), + imageInfoProvider = new mw.mmv.provider.ImageInfo( api ); + + imageInfoProvider.get( file ).fail( function ( errorMessage ) { + assert.strictEqual( errorMessage, 'file does not exist: File:Stuff.jpg', + 'error message is set correctly for missing file' ); + done(); + } ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ThumbnailInfo.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ThumbnailInfo.test.js new file mode 100644 index 00000000..19a18e6a --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ThumbnailInfo.test.js @@ -0,0 +1,165 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MultimediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.provider.ThumbnailInfo', QUnit.newMwEnvironment() ); + + QUnit.test( 'ThumbnailInfo constructor sanity check', function ( assert ) { + var api = { get: function () {} }, + thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( api ); + + assert.ok( thumbnailInfoProvider ); + } ); + + QUnit.test( 'ThumbnailInfo get test', function ( assert ) { + var apiCallCount = 0, + api = { get: function () { + apiCallCount++; + return $.Deferred().resolve( { + query: { + pages: { + '-1': { + ns: 6, + title: 'File:Stuff.jpg', + missing: '', + imagerepository: 'shared', + imageinfo: [ + { + thumburl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Stuff.jpg/51px-Stuff.jpg', + thumbwidth: 95, + thumbheight: 200, + url: 'https://upload.wikimedia.org/wikipedia/commons/1/19/Stuff.jpg', + descriptionurl: 'https://commons.wikimedia.org/wiki/File:Stuff.jpg' + } + ] + } + } + } + } ); + } }, + file = new mw.Title( 'File:Stuff.jpg' ), + thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( api ); + + return thumbnailInfoProvider.get( file, 100 ).then( function ( thumbnail ) { + assert.strictEqual( thumbnail.url, + 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Stuff.jpg/51px-Stuff.jpg', + 'URL is set correctly' ); + assert.strictEqual( thumbnail.width, 95, 'actual width is set correctly' ); + assert.strictEqual( thumbnail.height, 200, 'actual height is set correctly' ); + } ).then( function () { + assert.strictEqual( apiCallCount, 1 ); + // call the data provider a second time to check caching + return thumbnailInfoProvider.get( file, 100 ); + } ).then( function () { + assert.strictEqual( apiCallCount, 1 ); + // call a third time with different size to check caching + return thumbnailInfoProvider.get( file, 110 ); + } ).then( function () { + assert.strictEqual( apiCallCount, 2 ); + // call it again, with a height specified, to check caching + return thumbnailInfoProvider.get( file, 110, 100 ); + } ).then( function () { + assert.strictEqual( apiCallCount, 3 ); + } ); + } ); + + QUnit.test( 'ThumbnailInfo fail test', function ( assert ) { + var api = { get: function () { + return $.Deferred().resolve( {} ); + } }, + file = new mw.Title( 'File:Stuff.jpg' ), + done = assert.async(), + thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( api ); + + thumbnailInfoProvider.get( file, 100 ).fail( function () { + assert.ok( true, 'promise rejected when no data is returned' ); + done(); + } ); + } ); + + QUnit.test( 'ThumbnailInfo fail test 2', function ( assert ) { + var api = { get: function () { + return $.Deferred().resolve( { + query: { + pages: { + '-1': { + title: 'File:Stuff.jpg' + } + } + } + } ); + } }, + file = new mw.Title( 'File:Stuff.jpg' ), + done = assert.async(), + thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( api ); + + thumbnailInfoProvider.get( file, 100 ).fail( function () { + assert.ok( true, 'promise rejected when imageinfo is missing' ); + done(); + } ); + } ); + + QUnit.test( 'ThumbnailInfo missing page test', function ( assert ) { + var api = { get: function () { + return $.Deferred().resolve( { + query: { + pages: { + '-1': { + title: 'File:Stuff.jpg', + missing: '', + imagerepository: '' + } + } + } + } ); + } }, + file = new mw.Title( 'File:Stuff.jpg' ), + done = assert.async(), + thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( api ); + + thumbnailInfoProvider.get( file ).fail( function ( errorMessage ) { + assert.strictEqual( errorMessage, 'file does not exist: File:Stuff.jpg', + 'error message is set correctly for missing file' ); + done(); + } ); + } ); + + QUnit.test( 'ThumbnailInfo fail test 3', function ( assert ) { + var api = { get: function () { + return $.Deferred().resolve( { + query: { + pages: { + '-1': { + title: 'File:Stuff.jpg', + imageinfo: [ + {} + ] + } + } + } + } ); + } }, + file = new mw.Title( 'File:Stuff.jpg' ), + done = assert.async(), + thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( api ); + + thumbnailInfoProvider.get( file, 100 ).fail( function () { + assert.ok( true, 'promise rejected when thumbnail info is missing' ); + done(); + } ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.MainFileRoute.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.MainFileRoute.test.js new file mode 100644 index 00000000..49fcff6a --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.MainFileRoute.test.js @@ -0,0 +1,24 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw ) { + QUnit.module( 'mmv.routing.MainFileRoute', QUnit.newMwEnvironment() ); + + QUnit.test( 'Constructor sanity checks', function ( assert ) { + assert.ok( new mw.mmv.routing.MainFileRoute(), 'MainFileRoute created successfully' ); + } ); +}( mediaWiki ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.Router.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.Router.test.js new file mode 100644 index 00000000..3da76de5 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.Router.test.js @@ -0,0 +1,232 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw ) { + QUnit.module( 'mmv.routing.Router', QUnit.newMwEnvironment() ); + + QUnit.test( 'Constructor sanity checks', function ( assert ) { + var router; + + router = new mw.mmv.routing.Router(); + assert.ok( router, 'Router created successfully' ); + } ); + + QUnit.test( 'isMediaViewerHash()', function ( assert ) { + var router = new mw.mmv.routing.Router(); + + assert.ok( router.isMediaViewerHash( 'mediaviewer/foo' ), 'Legacy hash' ); + assert.ok( router.isMediaViewerHash( '#mediaviewer/foo' ), 'Legacy hash with #' ); + assert.ok( router.isMediaViewerHash( 'mediaviewer' ), 'Bare legacy hash' ); + assert.ok( router.isMediaViewerHash( '#mediaviewer' ), 'Bare legacy hash with #' ); + assert.ok( router.isMediaViewerHash( '/media/foo' ), 'Normal hash' ); + assert.ok( router.isMediaViewerHash( '#/media/foo' ), 'Normal hash with #' ); + assert.ok( router.isMediaViewerHash( '/media' ), 'Bare hash' ); + assert.ok( router.isMediaViewerHash( '#/media' ), 'Bare hash with #' ); + assert.ok( !router.isMediaViewerHash( 'foo/media' ), 'Foreign hash' ); + assert.ok( !router.isMediaViewerHash( '' ), 'Empty hash' ); + } ); + + QUnit.test( 'createHash()/parseHash()', function ( assert ) { + var route, parsedRoute, hash, title, + router = new mw.mmv.routing.Router(); + + route = new mw.mmv.routing.MainFileRoute(); + hash = router.createHash( route ); + parsedRoute = router.parseHash( hash ); + assert.deepEqual( parsedRoute, route, 'Bare hash' ); + + title = new mw.Title( 'File:Foo.png' ); + route = new mw.mmv.routing.ThumbnailRoute( title ); + hash = router.createHash( route ); + parsedRoute = router.parseHash( hash ); + assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(), + title.getPrefixedDb(), 'Normal hash' ); + assert.ok( hash.match( /File:Foo.png/ ), 'Simple filenames remain readable' ); + + title = new mw.Title( 'File:Foo.png' ); + route = new mw.mmv.routing.ThumbnailRoute( title ); + hash = router.createHash( route ); + assert.notEqual( hash[ 0 ], '#', 'Leading # is not included in the returned hash' ); + parsedRoute = router.parseHash( '#' + hash ); + assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(), + title.getPrefixedDb(), 'Leading # is accepted when parsing a hash' ); + + title = new mw.Title( 'File:Foo.png' ); + route = new mw.mmv.routing.ThumbnailRoute( title ); + hash = router.createHash( route ); + parsedRoute = router.parseHash( hash ); + assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(), + title.getPrefixedDb(), 'Normal hash' ); + assert.ok( hash.match( /File:Foo.png/ ), 'Simple filenames remain readable' ); + + title = new mw.Title( 'File:Foo/bar.png' ); + route = new mw.mmv.routing.ThumbnailRoute( title ); + hash = router.createHash( route ); + parsedRoute = router.parseHash( hash ); + assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(), + title.getPrefixedDb(), 'Filename with /' ); + assert.ok( !hash.match( 'Foo/bar' ), '/ is encoded' ); + + title = new mw.Title( 'File:Foo bar.png' ); + route = new mw.mmv.routing.ThumbnailRoute( title ); + hash = router.createHash( route ); + parsedRoute = router.parseHash( hash ); + assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(), + title.getPrefixedDb(), 'Filename with space' ); + assert.ok( !hash.match( 'Foo bar' ), 'space is replaced...' ); + assert.ok( hash.match( 'Foo_bar' ), '...with underscore' ); + + title = new mw.Title( 'File:看門狗 (遊戲).jpg' ); + route = new mw.mmv.routing.ThumbnailRoute( title ); + hash = router.createHash( route ); + parsedRoute = router.parseHash( hash ); + assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(), + title.getPrefixedDb(), 'Unicode filename' ); + + title = new mw.Title( 'File:%!"$&\'()*,-./:;=?@\\^_`~+.jpg' ); + if ( title ) { + route = new mw.mmv.routing.ThumbnailRoute( title ); + hash = router.createHash( route ); + parsedRoute = router.parseHash( hash ); + assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(), + title.getPrefixedDb(), 'Special characters' ); + } else { + // mw.Title depends on $wgLegalTitleChars - do not fail test if it is non-standard + assert.ok( true, 'Skipped' ); + } + } ); + + QUnit.test( 'createHash() error handling', function ( assert ) { + var router = new mw.mmv.routing.Router(); + + assert.ok( mw.mmv.testHelpers.getException( function () { return new mw.mmv.routing.ThumbnailRoute(); } ), + 'Exception thrown then ThumbnailRoute has no title' ); + assert.ok( mw.mmv.testHelpers.getException( function () { + router.createHash( this.sandbox.createStubInstance( mw.mmv.routing.Route ) ); + } ), 'Exception thrown for unknown Route subclass' ); + assert.ok( mw.mmv.testHelpers.getException( function () { + router.createHash( {} ); + } ), 'Exception thrown for non-Route class' ); + } ); + + QUnit.test( 'parseHash() with invalid hashes', function ( assert ) { + var router = new mw.mmv.routing.Router(); + + assert.ok( !router.parseHash( 'foo' ), 'Non-MMV hash is rejected.' ); + assert.ok( !router.parseHash( '#foo' ), 'Non-MMV hash is rejected (with #).' ); + assert.ok( !router.parseHash( '/media/foo/bar' ), 'Invalid MMV hash is rejected.' ); + assert.ok( !router.parseHash( '#/media/foo/bar' ), 'Invalid MMV hash is rejected (with #).' ); + } ); + + QUnit.test( 'parseHash() backwards compatibility', function ( assert ) { + var route, + router = new mw.mmv.routing.Router(); + + route = router.parseHash( '#mediaviewer/File:Foo bar.png' ); + assert.strictEqual( route.fileTitle.getPrefixedDb(), 'File:Foo_bar.png', + 'Old urls (with space) are handled' ); + + route = router.parseHash( '#mediaviewer/File:Mexican \'Alien\' Piñata.jpg' ); + assert.strictEqual( route.fileTitle.getPrefixedDb(), 'File:Mexican_\'Alien\'_Piñata.jpg', + 'Old urls (without percent-encoding) are handled' ); + } ); + + QUnit.test( 'createHashedUrl()', function ( assert ) { + var url, + route = new mw.mmv.routing.MainFileRoute(), + router = new mw.mmv.routing.Router(); + + url = router.createHashedUrl( route, 'http://example.com/' ); + assert.strictEqual( url, 'http://example.com/#/media', 'Url generation works' ); + + url = router.createHashedUrl( route, 'http://example.com/#foo' ); + assert.strictEqual( url, 'http://example.com/#/media', 'Urls with fragments are handled' ); + } ); + + QUnit.test( 'parseLocation()', function ( assert ) { + var location, route, + router = new mw.mmv.routing.Router(); + + location = { href: 'http://example.com/foo#mediaviewer/File:Foo.png' }; + route = router.parseLocation( location ); + assert.strictEqual( route.fileTitle.getPrefixedDb(), 'File:Foo.png', 'Reading location works' ); + + location = { href: 'http://example.com/foo#/media/File:Foo.png' }; + route = router.parseLocation( location ); + assert.strictEqual( route.fileTitle.getPrefixedDb(), 'File:Foo.png', 'Reading location works' ); + + location = { href: 'http://example.com/foo' }; + route = router.parseLocation( location ); + assert.ok( !route, 'Reading location without fragment part works' ); + } ); + + QUnit.test( 'parseLocation() with real location', function ( assert ) { + var route, title, hash, + router = new mw.mmv.routing.Router(); + + // mw.Title does not accept % in page names + this.sandbox.stub( mw, 'Title', function ( name ) { + return { + name: name, + getMain: function () { return name.replace( /^File:/, '' ); } + }; + } ); + title = new mw.Title( 'File:%40.png' ); + hash = router.createHash( new mw.mmv.routing.ThumbnailRoute( title ) ); + + window.location.hash = hash; + route = router.parseLocation( window.location ); + assert.strictEqual( route.fileTitle.getMain(), '%40.png', + 'Reading location set via location.hash works' ); + + if ( window.history ) { + window.history.pushState( null, null, '#' + hash ); + route = router.parseLocation( window.location ); + assert.strictEqual( route.fileTitle.getMain(), '%40.png', + 'Reading location set via pushState() works' ); + } else { + assert.ok( true, 'Skipped pushState() test, not supported on this browser' ); + } + + // reset location, might interfere with other tests + window.location.hash = '#'; + } ); + + QUnit.test( 'tokenizeHash()', function ( assert ) { + var router = new mw.mmv.routing.Router(); + + router.legacyPrefix = 'legacy'; + router.applicationPrefix = 'prefix'; + + assert.deepEqual( router.tokenizeHash( '#foo/bar' ), [], 'No known prefix' ); + + assert.deepEqual( router.tokenizeHash( '#prefix' ), [ 'prefix' ], 'Current prefix, with #' ); + assert.deepEqual( router.tokenizeHash( 'prefix' ), [ 'prefix' ], 'Current prefix, without #' ); + assert.deepEqual( router.tokenizeHash( '#prefix/bar' ), [ 'prefix', 'bar' ], 'Current prefix, with # and element' ); + assert.deepEqual( router.tokenizeHash( 'prefix/bar' ), [ 'prefix', 'bar' ], 'Current prefix, with element without #' ); + assert.deepEqual( router.tokenizeHash( '#prefix/bar/baz' ), [ 'prefix', 'bar', 'baz' ], 'Current prefix, with # and 2 elements' ); + assert.deepEqual( router.tokenizeHash( 'prefix/bar/baz' ), [ 'prefix', 'bar', 'baz' ], 'Current prefix, with 2 elements without #' ); + + assert.deepEqual( router.tokenizeHash( '#legacy' ), [ 'legacy' ], 'Legacy prefix, with #' ); + assert.deepEqual( router.tokenizeHash( 'legacy' ), [ 'legacy' ], 'Legacy prefix, without #' ); + assert.deepEqual( router.tokenizeHash( '#legacy/bar' ), [ 'legacy', 'bar' ], 'Legacy prefix, with # and element' ); + assert.deepEqual( router.tokenizeHash( 'legacy/bar' ), [ 'legacy', 'bar' ], 'Legacy prefix, with element without #' ); + assert.deepEqual( router.tokenizeHash( '#legacy/bar/baz' ), [ 'legacy', 'bar', 'baz' ], 'Legacy prefix, with # and 2 elements' ); + assert.deepEqual( router.tokenizeHash( 'legacy/bar/baz' ), [ 'legacy', 'bar', 'baz' ], 'Legacy prefix, with 2 elements without #' ); + + } ); +}( mediaWiki ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.ThumbnailRoute.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.ThumbnailRoute.test.js new file mode 100644 index 00000000..0336a628 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.ThumbnailRoute.test.js @@ -0,0 +1,32 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw ) { + QUnit.module( 'mmv.routing.ThumbnailRoute', QUnit.newMwEnvironment() ); + + QUnit.test( 'Constructor sanity checks', function ( assert ) { + var route, + title = new mw.Title( 'File:Foo.png' ); + + route = new mw.mmv.routing.ThumbnailRoute( title ); + assert.ok( route, 'ThumbnailRoute created successfully' ); + + assert.ok( mw.mmv.testHelpers.getException( function () { + return new mw.mmv.routing.ThumbnailRoute(); + } ), 'Exception is thrown when ThumbnailRoute is created without arguments' ); + } ); +}( mediaWiki ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvas.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvas.test.js new file mode 100644 index 00000000..88c74bdb --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvas.test.js @@ -0,0 +1,287 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.ui.Canvas', QUnit.newMwEnvironment() ); + + QUnit.test( 'Constructor sanity check', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + canvas = new mw.mmv.ui.Canvas( $qf, $qf, $qf ); + + assert.ok( canvas.$imageDiv, 'Image container is created.' ); + assert.strictEqual( canvas.$imageWrapper, $qf, '$imageWrapper is set correctly.' ); + assert.strictEqual( canvas.$mainWrapper, $qf, '$mainWrapper is set correctly.' ); + } ); + + QUnit.test( 'empty() and set()', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + canvas = new mw.mmv.ui.Canvas( $qf ), + image = new Image(), + $imageElem = $( image ), + imageRawMetadata = new mw.mmv.LightboxImage( 'foo.png' ); + + canvas.empty(); + + assert.strictEqual( canvas.$imageDiv.html(), '', 'Canvas is empty.' ); + assert.ok( canvas.$imageDiv.hasClass( 'empty' ), 'Canvas is not visible.' ); + + canvas.set( imageRawMetadata, $imageElem ); + + assert.strictEqual( canvas.$image, $imageElem, 'Image element set correctly.' ); + assert.strictEqual( canvas.imageRawMetadata, imageRawMetadata, 'Raw metadata set correctly.' ); + assert.strictEqual( canvas.$imageDiv.html(), '<img>', 'Image added to container.' ); + assert.ok( !canvas.$imageDiv.hasClass( 'empty' ), 'Canvas is visible.' ); + + canvas.empty(); + + assert.strictEqual( canvas.$imageDiv.html(), '', 'Canvas is empty.' ); + assert.ok( canvas.$imageDiv.hasClass( 'empty' ), 'Canvas is not visible.' ); + } ); + + QUnit.test( 'setImageAndMaxDimensions()', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + $mainWrapper = $( '<div>' ).appendTo( $qf ), + $innerWrapper = $( '<div>' ).appendTo( $mainWrapper ), + $imageWrapper = $( '<div>' ).appendTo( $innerWrapper ), + canvas = new mw.mmv.ui.Canvas( $innerWrapper, $imageWrapper, $mainWrapper ), + imageRawMetadata = new mw.mmv.LightboxImage( 'foo.png' ), + image = new Image(), + $imageElem = $( image ), + image2 = new Image(), + thumbnailWidth = 10, + screenWidth = 100, + $currentImage, + originalWidth; + + // Need to call set() before using setImageAndMaxDimensions() + canvas.set( imageRawMetadata, $imageElem ); + originalWidth = image.width; + + // Call with the same image + canvas.setImageAndMaxDimensions( + { width: thumbnailWidth }, + image, + { cssWidth: screenWidth } + ); + + assert.strictEqual( image.width, originalWidth, 'Image width was not modified.' ); + assert.strictEqual( canvas.$image, $imageElem, 'Image element still set correctly.' ); + + $currentImage = canvas.$image; + + // Call with a new image bigger than screen size + thumbnailWidth = 150; + canvas.setImageAndMaxDimensions( + { width: thumbnailWidth }, + image2, + { cssWidth: screenWidth } + ); + + assert.strictEqual( image2.width, screenWidth, 'Image width was trimmed correctly.' ); + assert.notStrictEqual( canvas.$image, $currentImage, 'Image element switched correctly.' ); + } ); + + QUnit.test( 'maybeDisplayPlaceholder: Constrained area for SVG files', function ( assert ) { + var $image, + blurredThumbnailShown, + $qf = $( '#qunit-fixture' ), + imageRawMetadata = new mw.mmv.LightboxImage( 'foo.svg' ), + canvas = new mw.mmv.ui.Canvas( $qf ); + + imageRawMetadata.filePageTitle = { + getExtension: function () { return 'svg'; } + }; + canvas.imageRawMetadata = imageRawMetadata; + + canvas.set = function () { + assert.ok( false, 'Placeholder is not shown' ); + }; + + $image = $( '<img>' ).width( 10 ).height( 5 ); + + blurredThumbnailShown = canvas.maybeDisplayPlaceholder( + { width: 200, height: 100 }, + $image, + { cssWidth: 300, cssHeight: 150 } + ); + + assert.strictEqual( $image.width(), 10, 'Placeholder width was not set to max' ); + assert.strictEqual( $image.height(), 5, 'Placeholder height was not set to max' ); + assert.ok( !$image.hasClass( 'blurred' ), 'Placeholder is not blurred' ); + assert.ok( !blurredThumbnailShown, 'Placeholder state is correct' ); + } ); + + QUnit.test( 'maybeDisplayPlaceholder: placeholder big enough that it doesn\'t need blurring, actual image bigger than the lightbox', function ( assert ) { + var $image, + blurredThumbnailShown, + $qf = $( '#qunit-fixture' ), + imageRawMetadata = new mw.mmv.LightboxImage( 'foo.png' ), + canvas = new mw.mmv.ui.Canvas( $qf ); + + imageRawMetadata.filePageTitle = { + getExtension: function () { return 'png'; } + }; + canvas.imageRawMetadata = imageRawMetadata; + + canvas.set = function () { + assert.ok( true, 'Placeholder shown' ); + }; + + $image = $( '<img>' ).width( 200 ).height( 100 ); + + blurredThumbnailShown = canvas.maybeDisplayPlaceholder( + { width: 1000, height: 500 }, + $image, + { cssWidth: 300, cssHeight: 150 } + ); + + assert.strictEqual( $image.width(), 300, 'Placeholder has the right width' ); + assert.strictEqual( $image.height(), 150, 'Placeholder has the right height' ); + assert.ok( !$image.hasClass( 'blurred' ), 'Placeholder is not blurred' ); + assert.ok( !blurredThumbnailShown, 'Placeholder state is correct' ); + } ); + + QUnit.test( 'maybeDisplayPlaceholder: big-enough placeholder that needs blurring, actual image bigger than the lightbox', function ( assert ) { + var $image, + blurredThumbnailShown, + $qf = $( '#qunit-fixture' ), + imageRawMetadata = new mw.mmv.LightboxImage( 'foo.png' ), + canvas = new mw.mmv.ui.Canvas( $qf ); + + imageRawMetadata.filePageTitle = { + getExtension: function () { return 'png'; } + }; + canvas.imageRawMetadata = imageRawMetadata; + + canvas.set = function () { + assert.ok( true, 'Placeholder shown' ); + }; + + $image = $( '<img>' ).width( 100 ).height( 50 ); + + blurredThumbnailShown = canvas.maybeDisplayPlaceholder( + { width: 1000, height: 500 }, + $image, + { cssWidth: 300, cssHeight: 150 } + ); + + assert.strictEqual( $image.width(), 300, 'Placeholder has the right width' ); + assert.strictEqual( $image.height(), 150, 'Placeholder has the right height' ); + assert.ok( $image.hasClass( 'blurred' ), 'Placeholder is blurred' ); + assert.ok( blurredThumbnailShown, 'Placeholder state is correct' ); + } ); + + QUnit.test( 'maybeDisplayPlaceholder: big-enough placeholder that needs blurring, actual image smaller than the lightbox', function ( assert ) { + var $image, + blurredThumbnailShown, + $qf = $( '#qunit-fixture' ), + imageRawMetadata = new mw.mmv.LightboxImage( 'foo.png' ), + canvas = new mw.mmv.ui.Canvas( $qf ); + + imageRawMetadata.filePageTitle = { + getExtension: function () { return 'png'; } + }; + canvas.imageRawMetadata = imageRawMetadata; + + canvas.set = function () { + assert.ok( true, 'Placeholder shown' ); + }; + + $image = $( '<img>' ).width( 100 ).height( 50 ); + + blurredThumbnailShown = canvas.maybeDisplayPlaceholder( + { width: 1000, height: 500 }, + $image, + { cssWidth: 1200, cssHeight: 600 } + ); + + assert.strictEqual( $image.width(), 1000, 'Placeholder has the right width' ); + assert.strictEqual( $image.height(), 500, 'Placeholder has the right height' ); + assert.ok( $image.hasClass( 'blurred' ), 'Placeholder is blurred' ); + assert.ok( blurredThumbnailShown, 'Placeholder state is correct' ); + } ); + + QUnit.test( 'maybeDisplayPlaceholder: placeholder too small to be displayed, actual image bigger than the lightbox', function ( assert ) { + var $image, + blurredThumbnailShown, + $qf = $( '#qunit-fixture' ), + imageRawMetadata = new mw.mmv.LightboxImage( 'foo.png' ), + canvas = new mw.mmv.ui.Canvas( $qf ); + + imageRawMetadata.filePageTitle = { + getExtension: function () { return 'png'; } + }; + canvas.imageRawMetadata = imageRawMetadata; + + canvas.set = function () { + assert.ok( false, 'Placeholder shown when it should not' ); + }; + + $image = $( '<img>' ).width( 10 ).height( 5 ); + + blurredThumbnailShown = canvas.maybeDisplayPlaceholder( + { width: 1000, height: 500 }, + $image, + { cssWidth: 300, cssHeight: 150 } + ); + + assert.strictEqual( $image.width(), 10, 'Placeholder has the right width' ); + assert.strictEqual( $image.height(), 5, 'Placeholder has the right height' ); + assert.ok( !$image.hasClass( 'blurred' ), 'Placeholder is not blurred' ); + assert.ok( !blurredThumbnailShown, 'Placeholder state is correct' ); + } ); + + QUnit.test( 'Unblur', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + canvas = new mw.mmv.ui.Canvas( $qf ), + oldAnimate = $.fn.animate; + + $.fn.animate = function ( target, options ) { + var self = this, + lastValue; + + $.each( target, function ( key, value ) { + lastValue = self.key = value; + } ); + + if ( options ) { + if ( options.step ) { + options.step.call( this, lastValue ); + } + + if ( options.complete ) { + options.complete.call( this ); + } + } + }; + + canvas.$image = $( '<img>' ); + + canvas.unblurWithAnimation(); + + assert.ok( !canvas.$image.css( '-webkit-filter' ) || !canvas.$image.css( '-webkit-filter' ).length, + 'Image has no -webkit-filter left' ); + assert.ok( !canvas.$image.css( 'filter' ) || !canvas.$image.css( 'filter' ).length || canvas.$image.css( 'filter' ) === 'none', + 'Image has no filter left' ); + assert.strictEqual( parseInt( canvas.$image.css( 'opacity' ), 10 ), 1, + 'Image is fully opaque' ); + assert.ok( !canvas.$image.hasClass( 'blurred' ), 'Image has no "blurred" class' ); + + $.fn.animate = oldAnimate; + } ); + +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvasButtons.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvasButtons.test.js new file mode 100644 index 00000000..09e5ab9d --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvasButtons.test.js @@ -0,0 +1,36 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.ui.CanvasButtons', QUnit.newMwEnvironment() ); + + QUnit.test( 'Prev/Next', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + buttons = new mw.mmv.ui.CanvasButtons( $qf, $( '<div>' ), $( '<div>' ) ); + + buttons.on( 'next', function () { + assert.ok( true, 'Switched to next image' ); + } ); + + buttons.on( 'prev', function () { + assert.ok( true, 'Switched to prev image' ); + } ); + + buttons.$next.click(); + buttons.$prev.click(); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.description.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.description.test.js new file mode 100644 index 00000000..bcb2b322 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.description.test.js @@ -0,0 +1,42 @@ +( function ( mw, $ ) { + QUnit.module( 'mmv.ui.description', QUnit.newMwEnvironment() ); + + QUnit.test( 'Sanity test, object creation and UI construction', function ( assert ) { + var description = new mw.mmv.ui.Description( $( '#qunit-fixture' ) ); + + assert.ok( description, 'Image description UI element is created' ); + assert.strictEqual( description.$imageDescDiv.length, 1, 'Image description div is created' ); + assert.strictEqual( description.$imageDesc.length, 1, 'Image description element is created' ); + } ); + + QUnit.test( 'Setting data in different combinations works well', function ( assert ) { + var description = new mw.mmv.ui.Description( $( '#qunit-fixture' ) ); + + description.set( null, null ); + assert.ok( description.$imageDescDiv.hasClass( 'empty' ), + 'Image description div is marked empty when neither description nor caption is available' ); + + description.set( null, 'foo' ); + assert.ok( description.$imageDescDiv.hasClass( 'empty' ), + 'Image description div is marked empty when there is no description' ); + + description.set( 'blah', null ); + assert.ok( description.$imageDescDiv.hasClass( 'empty' ), + 'Image description div is marked empty when there is no caption (description will be shown in title)' ); + + description.set( 'foo', 'bar' ); + assert.ok( !description.$imageDescDiv.hasClass( 'empty' ), + 'Image description div is not marked empty when both description and caption are available' ); + assert.strictEqual( description.$imageDesc.text(), 'foo', + 'Image description text is set correctly, caption is ignored' ); + } ); + + QUnit.test( 'Emptying data works as expected', function ( assert ) { + var description = new mw.mmv.ui.Description( $( '#qunit-fixture' ) ); + + description.set( 'foo', 'bar' ); + description.empty(); + assert.strictEqual( description.$imageDescDiv.hasClass( 'empty' ), true, 'Image description div is marked empty when emptied' ); + assert.strictEqual( description.$imageDesc.text(), '', 'Image description text is emptied correctly' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.download.pane.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.download.pane.test.js new file mode 100644 index 00000000..8cc6008f --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.download.pane.test.js @@ -0,0 +1,164 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MultimediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.ui.download.pane', QUnit.newMwEnvironment() ); + + QUnit.test( 'Sanity test, object creation and UI construction', function ( assert ) { + var download = new mw.mmv.ui.download.Pane( $( '#qunit-fixture' ) ); + + assert.ok( download, 'download UI element is created.' ); + assert.strictEqual( download.$pane.length, 1, 'Pane div created.' ); + assert.ok( download.$downloadButton && download.$selectionArrow, 'Download button created.' ); + assert.ok( download.downloadSizeMenu, 'Image size pulldown menu created.' ); + assert.ok( download.$previewLink, 'Preview link created.' ); + assert.ok( download.defaultItem, 'Default item set.' ); + + assert.strictEqual( download.$downloadButton.html(), '', 'Button has empty content.' ); + assert.strictEqual( download.$downloadButton.attr( 'href' ), undefined, 'Button href is empty.' ); + assert.strictEqual( download.$previewLink.attr( 'href' ), undefined, 'Preview link href is empty.' ); + } ); + + QUnit.test( 'set()/empty():', function ( assert ) { + var download = new mw.mmv.ui.download.Pane( $( '#qunit-fixture' ) ), + src = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg', + image = { // fake mw.mmv.model.Image + title: new mw.Title( 'File:Foobar.jpg' ), + url: src + }; + + assert.strictEqual( download.imageExtension, undefined, 'Image extension is not set.' ); + + download.utils.updateMenuOptions = function () { + assert.ok( true, 'Menu options updated.' ); + }; + download.downloadSizeMenu.getMenu().selectItem = function () { + assert.ok( true, 'Default item selected to update the labels.' ); + }; + + download.set( image ); + + assert.strictEqual( download.imageExtension, 'jpg', 'Image extension is set correctly.' ); + + download.empty(); + + assert.strictEqual( download.imageExtension, undefined, 'Image extension is not set.' ); + } ); + + QUnit.test( 'attach()/unattach():', function ( assert ) { + var hsstub, tstub, + download = new mw.mmv.ui.download.Pane( $( '#qunit-fixture' ) ), + image = { + title: new mw.Title( 'File:Foobar.jpg' ), + url: 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg' + }; + + download.set( image ); + + hsstub = this.sandbox.stub( download, 'handleSizeSwitch' ); + tstub = this.sandbox.stub( download.downloadSizeMenu.getMenu(), 'toggle' ); + + // Triggering action events before attaching should do nothing + download.downloadSizeMenu.getMenu().emit( + 'choose', download.downloadSizeMenu.getMenu().findSelectedItem() ); + download.$selectionArrow.click(); + + assert.ok( !hsstub.called, 'handleSizeSwitch not called' ); + assert.ok( !tstub.called, 'Menu selection did not happen' ); + + hsstub.reset(); + tstub.reset(); + + download.attach(); + + // Action events should be handled now + download.downloadSizeMenu.getMenu().emit( + 'choose', download.downloadSizeMenu.getMenu().findSelectedItem() ); + download.$selectionArrow.click(); + + assert.ok( hsstub.called, 'handleSizeSwitch was called' ); + assert.ok( tstub.called, 'Menu selection happened' ); + + hsstub.reset(); + tstub.reset(); + + download.unattach(); + + // Triggering action events now that we are unattached should do nothing + download.downloadSizeMenu.getMenu().emit( + 'choose', download.downloadSizeMenu.getMenu().findSelectedItem() ); + download.$selectionArrow.click(); + + assert.ok( !hsstub.called, 'handleSizeSwitch not called' ); + assert.ok( !tstub.called, 'Menu selection did not happen' ); + } ); + + QUnit.test( 'handleSizeSwitch():', function ( assert ) { + var download = new mw.mmv.ui.download.Pane( $( '#qunit-fixture' ) ), + newImageUrl = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/NewFoobar.jpg'; + + download.utils.getThumbnailUrlPromise = function () { + return $.Deferred().resolve( { url: newImageUrl } ).promise(); + }; + + download.setDownloadUrl = function ( url ) { + assert.strictEqual( url, newImageUrl, 'URL passed to setDownloadUrl is correct' ); + }; + + download.handleSizeSwitch( download.downloadSizeMenu.getMenu().findSelectedItem() ); + + assert.ok( download.$downloadButton.html().match( /original.*/ ), 'Button message updated.' ); + + download.image = { url: newImageUrl }; + + download.utils.getThumbnailUrlPromise = function () { + assert.ok( false, 'Should not fetch the thumbnail if the image is original size.' ); + }; + + download.handleSizeSwitch( download.downloadSizeMenu.getMenu().findSelectedItem() ); + } ); + + QUnit.test( 'setButtonText() sanity check:', function ( assert ) { + var download = new mw.mmv.ui.download.Pane( $( '#qunit-fixture' ) ), + message; + + download.setButtonText( 'large', 'jpg', 100, 200 ); + assert.ok( true, 'Setting the text did not cause any errors' ); + + message = download.$downloadButton.html(); + download.setButtonText( 'small', 'png', 1000, 2000 ); + assert.notStrictEqual( download.$downloadButton.html(), message, 'Button text was updated' ); + } ); + + QUnit.test( 'getExtensionFromUrl():', function ( assert ) { + var download = new mw.mmv.ui.download.Pane( $( '#qunit-fixture' ) ); + + assert.strictEqual( download.getExtensionFromUrl( 'http://example.com/bing/foo.bar.png' ), + 'png', 'Extension is parsed correctly' ); + } ); + + QUnit.test( 'setDownloadUrl', function ( assert ) { + var download = new mw.mmv.ui.download.Pane( $( '#qunit-fixture' ) ), + imageUrl = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/NewFoobar.jpg'; + + download.setDownloadUrl( imageUrl ); + + assert.strictEqual( download.$downloadButton.attr( 'href' ), imageUrl + '?download', 'Download link is set correctly.' ); + assert.strictEqual( download.$previewLink.attr( 'href' ), imageUrl, 'Preview link is set correctly.' ); + assert.ok( !download.$downloadButton.hasClass( 'disabledLink' ), 'Download link is enabled.' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js new file mode 100644 index 00000000..c10c6dc9 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js @@ -0,0 +1,207 @@ +( function ( mw, $ ) { + var thingsShouldBeEmptied = [ + '$license', + '$title', + '$location', + '$datetime' + ], + + thingsShouldHaveEmptyClass = [ + '$licenseLi', + '$credit', + '$locationLi', + '$datetimeLi' + ]; + + QUnit.module( 'mmv.ui.metadataPanel', QUnit.newMwEnvironment() ); + + QUnit.test( 'The panel is emptied properly when necessary', function ( assert ) { + var i, + $qf = $( '#qunit-fixture' ), + panel = new mw.mmv.ui.MetadataPanel( $qf, $( '<div>' ).appendTo( $qf ), mw.storage, new mw.mmv.Config( {}, mw.config, mw.user, new mw.Api(), mw.storage ) ); + + panel.empty(); + + assert.expect( thingsShouldBeEmptied.length + thingsShouldHaveEmptyClass.length ); + + for ( i = 0; i < thingsShouldBeEmptied.length; i++ ) { + assert.strictEqual( panel[ thingsShouldBeEmptied[ i ] ].text(), '', 'We successfully emptied the ' + thingsShouldBeEmptied[ i ] + ' element' ); + } + + for ( i = 0; i < thingsShouldHaveEmptyClass.length; i++ ) { + assert.strictEqual( panel[ thingsShouldHaveEmptyClass[ i ] ].hasClass( 'empty' ), true, 'We successfully applied the empty class to the ' + thingsShouldHaveEmptyClass[ i ] + ' element' ); + } + } ); + + QUnit.test( 'Setting location information works as expected', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + panel = new mw.mmv.ui.MetadataPanel( $qf, $( '<div>' ).appendTo( $qf ), mw.storage, new mw.mmv.Config( {}, mw.config, mw.user, new mw.Api(), mw.storage ) ), + fileName = 'Foobar.jpg', + latitude = 12.3456789, + longitude = 98.7654321, + imageData = { + latitude: latitude, + longitude: longitude, + hasCoords: function () { return true; }, + title: mw.Title.newFromText( 'File:Foobar.jpg' ) + }; + + panel.setLocationData( imageData ); + + assert.strictEqual( + panel.$location.text(), + 'Location: 12° 20′ 44.44″ N, 98° 45′ 55.56″ E', + 'Location text is set as expected - if this fails it may be due to i18n issues.' + ); + + assert.strictEqual( + panel.$location.prop( 'href' ), + 'http://tools.wmflabs.org/geohack/geohack.php?pagename=File:' + fileName + '¶ms=' + latitude + '_N_' + longitude + '_E_&language=en', + 'Location URL is set as expected' + ); + + latitude = -latitude; + longitude = -longitude; + imageData.latitude = latitude; + imageData.longitude = longitude; + panel.setLocationData( imageData ); + + assert.strictEqual( + panel.$location.text(), + 'Location: 12° 20′ 44.44″ S, 98° 45′ 55.56″ W', + 'Location text is set as expected - if this fails it may be due to i18n issues.' + ); + + assert.strictEqual( + panel.$location.prop( 'href' ), + 'http://tools.wmflabs.org/geohack/geohack.php?pagename=File:' + fileName + '¶ms=' + ( -latitude ) + '_S_' + ( -longitude ) + '_W_&language=en', + 'Location URL is set as expected' + ); + + latitude = 0; + longitude = 0; + imageData.latitude = latitude; + imageData.longitude = longitude; + panel.setLocationData( imageData ); + + assert.strictEqual( + panel.$location.text(), + 'Location: 0° 0′ 0″ N, 0° 0′ 0″ E', + 'Location text is set as expected - if this fails it may be due to i18n issues.' + ); + + assert.strictEqual( + panel.$location.prop( 'href' ), + 'http://tools.wmflabs.org/geohack/geohack.php?pagename=File:' + fileName + '¶ms=' + latitude + '_N_' + longitude + '_E_&language=en', + 'Location URL is set as expected' + ); + } ); + + QUnit.test( 'Setting image information works as expected', function ( assert ) { + var creditPopupText, + $qf = $( '#qunit-fixture' ), + panel = new mw.mmv.ui.MetadataPanel( $qf, $( '<div>' ).appendTo( $qf ), mw.storage, new mw.mmv.Config( {}, mw.config, mw.user, new mw.Api(), mw.storage ) ), + title = 'Foo bar', + image = { + filePageTitle: mw.Title.newFromText( 'File:' + title + '.jpg' ) + }, + imageData = { + title: image.filePageTitle, + url: 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg', + descriptionUrl: 'https://commons.wikimedia.org/wiki/File:Foobar.jpg', + hasCoords: function () { return false; } + }, + repoData = { + getArticlePath: function () { return 'Foo'; }, + isCommons: function () { return false; } + }, + oldMoment = window.moment, + // custom clock will give MPP.formatDate some time to load moment.js + clock = this.sandbox.useFakeTimers(); + + /* window.moment = function ( date ) { + // This has no effect for now, since writing this test revealed that our moment.js + // doesn't have any language configuration + return oldMoment( date ).lang( 'fr' ); + };*/ + + panel.setImageInfo( image, imageData, repoData ); + + assert.strictEqual( panel.$title.text(), title, 'Title is correctly set' ); + assert.ok( panel.$credit.text(), 'Default credit is shown' ); + assert.strictEqual( panel.$license.prop( 'href' ), imageData.descriptionUrl, + 'User is directed to file page for license information' ); + assert.ok( !panel.$license.prop( 'target' ), 'License information opens in same window' ); + assert.ok( panel.$datetimeLi.hasClass( 'empty' ), 'Date/Time is empty' ); + assert.ok( panel.$locationLi.hasClass( 'empty' ), 'Location is empty' ); + + imageData.creationDateTime = '2013-08-26T14:41:02Z'; + imageData.uploadDateTime = '2013-08-25T14:41:02Z'; + imageData.source = '<b>Lost</b><a href="foo">Bar</a>'; + imageData.author = 'Bob'; + imageData.license = new mw.mmv.model.License( 'CC-BY-2.0', 'cc-by-2.0', + 'Creative Commons Attribution - Share Alike 2.0', + 'http://creativecommons.org/licenses/by-sa/2.0/' ); + imageData.restrictions = [ 'trademarked', 'default', 'insignia' ]; + + panel.setImageInfo( image, imageData, repoData ); + creditPopupText = panel.creditField.$element.attr( 'original-title' ); + clock.tick( 10 ); + + assert.strictEqual( panel.$title.text(), title, 'Title is correctly set' ); + assert.ok( !panel.$credit.hasClass( 'empty' ), 'Credit is not empty' ); + assert.ok( !panel.$datetimeLi.hasClass( 'empty' ), 'Date/Time is not empty' ); + assert.strictEqual( panel.creditField.$element.find( '.mw-mmv-author' ).text(), imageData.author, 'Author text is correctly set' ); + assert.strictEqual( panel.creditField.$element.find( '.mw-mmv-source' ).html(), '<b>Lost</b><a href="foo">Bar</a>', 'Source text is correctly set' ); + // Either multimediaviewer-credit-popup-text or multimediaviewer-credit-popup-text-more. + assert.ok( creditPopupText === 'Author and source information' || creditPopupText === 'View full author and source', 'Source tooltip is correctly set' ); + assert.ok( panel.$datetime.text().indexOf( '26 August 2013' ) > 0, 'Correct date is displayed' ); + assert.strictEqual( panel.$license.text(), 'CC BY 2.0', 'License is correctly set' ); + assert.ok( panel.$license.prop( 'target' ), 'License information opens in new window' ); + assert.ok( panel.$restrictions.children().last().children().hasClass( 'mw-mmv-restriction-default' ), 'Default restriction is correctly displayed last' ); + + imageData.creationDateTime = undefined; + panel.setImageInfo( image, imageData, repoData ); + clock.tick( 10 ); + + assert.ok( panel.$datetime.text().indexOf( '25 August 2013' ) > 0, 'Correct date is displayed' ); + + window.moment = oldMoment; + clock.restore(); + } ); + + QUnit.test( 'Setting permission information works as expected', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + panel = new mw.mmv.ui.MetadataPanel( $qf, $( '<div>' ).appendTo( $qf ), mw.storage, new mw.mmv.Config( {}, mw.config, mw.user, new mw.Api(), mw.storage ) ); + + panel.setLicense( null, 'http://example.com' ); // make sure license is visible as it contains the permission + panel.setPermission( 'Look at me, I am a permission!' ); + assert.ok( panel.$permissionLink.is( ':visible' ) ); + } ); + + QUnit.test( 'Date formatting', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + panel = new mw.mmv.ui.MetadataPanel( $qf, $( '<div>' ).appendTo( $qf ), mw.storage, new mw.mmv.Config( {}, mw.config, mw.user, new mw.Api(), mw.storage ) ), + date1 = 'Garbage', + promise = panel.formatDate( date1 ); + + return promise.then( function ( result ) { + assert.strictEqual( result, date1, 'Invalid date is correctly ignored' ); + } ); + } ); + + QUnit.test( 'About links', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + oldWgMediaViewerIsInBeta = mw.config.get( 'wgMediaViewerIsInBeta' ); + + this.sandbox.stub( mw.user, 'isAnon' ); + mw.config.set( 'wgMediaViewerIsInBeta', false ); + // eslint-disable-next-line no-new + new mw.mmv.ui.MetadataPanel( $qf.empty(), $( '<div>' ).appendTo( $qf ), mw.storage, new mw.mmv.Config( {}, mw.config, mw.user, new mw.Api(), mw.storage ) ); + + assert.strictEqual( $qf.find( '.mw-mmv-about-link' ).length, 1, 'About link is created.' ); + assert.strictEqual( $qf.find( '.mw-mmv-discuss-link' ).length, 1, 'Discuss link is created.' ); + assert.strictEqual( $qf.find( '.mw-mmv-help-link' ).length, 1, 'Help link is created.' ); + mw.config.set( 'wgMediaViewerIsInBeta', oldWgMediaViewerIsInBeta ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanelScroller.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanelScroller.test.js new file mode 100644 index 00000000..7de5aef0 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanelScroller.test.js @@ -0,0 +1,232 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.ui.metadataPanelScroller', QUnit.newMwEnvironment( { + setup: function () { + this.clock = this.sandbox.useFakeTimers(); + } + } ) ); + + QUnit.test( 'empty()', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + localStorage = mw.mmv.testHelpers.getFakeLocalStorage(), + scroller = new mw.mmv.ui.MetadataPanelScroller( $qf, $( '<div>' ).appendTo( $qf ), localStorage ); + + scroller.empty(); + assert.ok( !scroller.$container.hasClass( 'invite' ), 'We successfully reset the invite' ); + } ); + + QUnit.test( 'Metadata div is only animated once', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + displayCount = null, // pretend it doesn't exist at first + localStorage = mw.mmv.testHelpers.createLocalStorage( { + // We simulate localStorage to avoid test side-effects + getItem: function () { return displayCount; }, + setItem: function ( _, val ) { displayCount = val; } + } ), + scroller = new mw.mmv.ui.MetadataPanelScroller( $qf, $( '<div>' ).appendTo( $qf ), localStorage ); + + scroller.attach(); + + scroller.animateMetadataOnce(); + + assert.ok( scroller.hasAnimatedMetadata, + 'The first call to animateMetadataOnce set hasAnimatedMetadata to true' ); + assert.ok( $qf.hasClass( 'invite' ), + 'The first call to animateMetadataOnce led to an animation' ); + + $qf.removeClass( 'invite' ); + + scroller.animateMetadataOnce(); + + assert.strictEqual( scroller.hasAnimatedMetadata, true, 'The second call to animateMetadataOnce did not change the value of hasAnimatedMetadata' ); + assert.ok( !$qf.hasClass( 'invite' ), + 'The second call to animateMetadataOnce did not lead to an animation' ); + + scroller.unattach(); + + scroller.attach(); + + scroller.animateMetadataOnce(); + assert.ok( $qf.hasClass( 'invite' ), + 'After closing and opening the viewer, the panel is animated again' ); + + scroller.unattach(); + } ); + + QUnit.test( 'No localStorage', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + localStorage = mw.mmv.testHelpers.getUnsupportedLocalStorage(), + scroller = new mw.mmv.ui.MetadataPanelScroller( $qf, $( '<div>' ).appendTo( $qf ), localStorage ); + + this.sandbox.stub( $.fn, 'scrollTop', function () { return 10; } ); + + scroller.scroll(); + + assert.strictEqual( scroller.hasOpenedMetadata, true, 'We store hasOpenedMetadata flag for the session' ); + } ); + + QUnit.test( 'localStorage is full', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + localStorage = mw.mmv.testHelpers.createLocalStorage( { + getItem: this.sandbox.stub().returns( null ), + setItem: this.sandbox.stub().throwsException( 'I am full' ) + } ), + scroller = new mw.mmv.ui.MetadataPanelScroller( $qf, $( '<div>' ).appendTo( $qf ), localStorage ); + + this.sandbox.stub( $.fn, 'scrollTop', function () { return 10; } ); + + scroller.attach(); + + scroller.scroll(); + + assert.strictEqual( scroller.hasOpenedMetadata, true, 'We store hasOpenedMetadata flag for the session' ); + + scroller.scroll(); + + assert.ok( localStorage.store.setItem.calledOnce, 'localStorage only written once' ); + + scroller.unattach(); + } ); + + /** + * We need to set up a proxy on the jQuery scrollTop function and the jQuery.scrollTo plugin, + * that will let us pretend that the document really scrolled and that will return values + * as if the scroll happened. + * + * @param {sinon.sandbox} sandbox + * @param {mw.mmv.ui.MetadataPanelScroller} scroller + */ + function stubScrollFunctions( sandbox, scroller ) { + var memorizedScrollTop = 0; + + sandbox.stub( $.fn, 'scrollTop', function ( scrollTop ) { + if ( scrollTop !== undefined ) { + memorizedScrollTop = scrollTop; + scroller.scroll(); + return this; + } else { + return memorizedScrollTop; + } + } ); + sandbox.stub( $.fn, 'animate', function ( props ) { + if ( 'scrollTop' in props ) { + memorizedScrollTop = props.scrollTop; + scroller.scroll(); + } + return this; + } ); + } + + QUnit.test( 'Metadata scrolling', function ( assert ) { + var $window = $( window ), + $qf = $( '#qunit-fixture' ), + $container = $( '<div>' ).css( 'height', 100 ).appendTo( $qf ), + $aboveFold = $( '<div>' ).css( 'height', 50 ).appendTo( $container ), + fakeLocalStorage = mw.mmv.testHelpers.createLocalStorage( { + getItem: this.sandbox.stub().returns( null ), + setItem: $.noop + } ), + scroller = new mw.mmv.ui.MetadataPanelScroller( $container, $aboveFold, fakeLocalStorage ), + keydown = $.Event( 'keydown' ); + + stubScrollFunctions( this.sandbox, scroller ); + + this.sandbox.stub( fakeLocalStorage.store, 'setItem' ); + + // First phase of the test: up and down arrows + + scroller.hasAnimatedMetadata = false; + + scroller.attach(); + + assert.strictEqual( $window.scrollTop(), 0, 'scrollTop should be set to 0' ); + + assert.ok( !fakeLocalStorage.store.setItem.called, 'The metadata hasn\'t been open yet, no entry in localStorage' ); + + keydown.which = 38; // Up arrow + scroller.keydown( keydown ); + + assert.ok( fakeLocalStorage.store.setItem.calledWithExactly( 'mmv.hasOpenedMetadata', '1' ), 'localStorage knows that the metadata has been open' ); + + keydown.which = 40; // Down arrow + scroller.keydown( keydown ); + + assert.strictEqual( $window.scrollTop(), 0, + 'scrollTop should be set to 0 after pressing down arrow' ); + + // Unattach lightbox from document + scroller.unattach(); + + // Second phase of the test: scroll memory + + scroller.attach(); + + // To make sure that the details are out of view, the lightbox is supposed to scroll to the top when open + assert.strictEqual( $window.scrollTop(), 0, 'Page scrollTop should be set to 0' ); + + // Scroll down to check that the scrollTop memory doesn't affect prev/next (bug 59861) + $window.scrollTop( 20 ); + this.clock.tick( 100 ); + + // This extra attach() call simulates the effect of prev/next seen in bug 59861 + scroller.attach(); + + // The lightbox was already open at this point, the scrollTop should be left untouched + assert.strictEqual( $window.scrollTop(), 20, 'Page scrollTop should be set to 20' ); + + scroller.unattach(); + } ); + + QUnit.test( 'Metadata scroll logging', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + $container = $( '<div>' ).css( 'height', 100 ).appendTo( $qf ), + $aboveFold = $( '<div>' ).css( 'height', 50 ).appendTo( $container ), + localStorage = mw.mmv.testHelpers.getFakeLocalStorage(), + scroller = new mw.mmv.ui.MetadataPanelScroller( $container, $aboveFold, localStorage ), + keydown = $.Event( 'keydown' ); + + stubScrollFunctions( this.sandbox, scroller ); + + this.sandbox.stub( mw.mmv.actionLogger, 'log' ); + + keydown.which = 38; // Up arrow + scroller.keydown( keydown ); + + assert.ok( mw.mmv.actionLogger.log.calledWithExactly( 'metadata-open' ), 'Opening keypress logged' ); + mw.mmv.actionLogger.log.reset(); + + keydown.which = 38; // Up arrow + scroller.keydown( keydown ); + + assert.ok( mw.mmv.actionLogger.log.calledWithExactly( 'metadata-close' ), 'Closing keypress logged' ); + mw.mmv.actionLogger.log.reset(); + + keydown.which = 40; // Down arrow + scroller.keydown( keydown ); + + assert.ok( mw.mmv.actionLogger.log.calledWithExactly( 'metadata-open' ), 'Opening keypress logged' ); + mw.mmv.actionLogger.log.reset(); + + keydown.which = 40; // Down arrow + scroller.keydown( keydown ); + + assert.ok( mw.mmv.actionLogger.log.calledWithExactly( 'metadata-close' ), 'Closing keypress logged' ); + mw.mmv.actionLogger.log.reset(); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.permission.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.permission.test.js new file mode 100644 index 00000000..319642e3 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.permission.test.js @@ -0,0 +1,112 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mw.mmv.ui.Permission', QUnit.newMwEnvironment( { + setup: function () { + // animation would keep running, conflict with other tests + this.sandbox.stub( $.fn, 'animate' ).returnsThis(); + } + } ) ); + + QUnit.test( 'Constructor sanity check', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + permission = new mw.mmv.ui.Permission( $qf ); + + assert.ok( permission, 'constructor does not throw error' ); + } ); + + QUnit.test( 'set()', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + permission = new mw.mmv.ui.Permission( $qf ), + text = 'Nothing to see here.'; + + permission.set( text ); + + // FIXME get rid of "view more" - this is temporary + assert.strictEqual( permission.$text.children().remove().end().text(), + text, 'permission text is set' ); + assert.strictEqual( permission.$html.text(), text, 'permission html is set' ); + assert.ok( permission.$text.is( ':visible' ), 'permission text is visible' ); + assert.ok( !permission.$html.is( ':visible' ), 'permission html is not visible' ); + assert.ok( !permission.$close.is( ':visible' ), 'close button is not visible' ); + } ); + + QUnit.test( 'set() with html', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + permission = new mw.mmv.ui.Permission( $qf ), + text = '<b>Nothing</b> to see here.'; + + permission.set( text ); + + assert.ok( !permission.$text.find( 'b' ).length, 'permission text has no html' ); + assert.ok( permission.$html.find( 'b' ), 'permission html has html' ); + } ); + + QUnit.test( 'empty()', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + permission = new mw.mmv.ui.Permission( $qf ), + text = 'Nothing to see here.'; + + permission.set( text ); + permission.empty(); + + assert.ok( !permission.$text.is( ':visible' ), 'permission text is not visible' ); + assert.ok( !permission.$html.is( ':visible' ), 'permission html is not visible' ); + assert.ok( !permission.$close.is( ':visible' ), 'close button is not visible' ); + } ); + + QUnit.test( 'grow()', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + permission = new mw.mmv.ui.Permission( $qf ), + text = 'Nothing to see here.'; + + permission.set( text ); + permission.grow(); + + assert.ok( !permission.$text.is( ':visible' ), 'permission text is not visible' ); + assert.ok( permission.$html.is( ':visible' ), 'permission html is visible' ); + assert.ok( permission.$close.is( ':visible' ), 'close button is visible' ); + } ); + + QUnit.test( 'shrink()', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + permission = new mw.mmv.ui.Permission( $qf ), + text = 'Nothing to see here.'; + + permission.set( text ); + permission.grow(); + permission.shrink(); + + assert.ok( permission.$text.is( ':visible' ), 'permission text is visible' ); + assert.ok( !permission.$html.is( ':visible' ), 'permission html is not visible' ); + assert.ok( !permission.$close.is( ':visible' ), 'close button is not visible' ); + } ); + + QUnit.test( 'isFullSize()', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + permission = new mw.mmv.ui.Permission( $qf ), + text = 'Nothing to see here.'; + + permission.set( text ); + assert.ok( !permission.isFullSize(), 'permission is not full-size' ); + permission.grow(); + assert.ok( permission.isFullSize(), 'permission is full-size' ); + permission.shrink(); + assert.ok( !permission.isFullSize(), 'permission is not full-size again' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.progressBar.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.progressBar.test.js new file mode 100644 index 00000000..5b3bd3d0 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.progressBar.test.js @@ -0,0 +1,77 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.ui.ProgressBar', QUnit.newMwEnvironment() ); + + QUnit.test( 'Constructor sanity check', function ( assert ) { + var progressBar = new mw.mmv.ui.ProgressBar( $( '<div>' ) ); + assert.ok( progressBar, 'ProgressBar created sccessfully' ); + assert.ok( progressBar.$progress.hasClass( 'empty' ), 'ProgressBar starts empty' ); + } ); + + QUnit.test( 'animateTo()', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + $div = $( '<div>' ).css( { width: 250, position: 'relative' } ).appendTo( $qf ), + progress = new mw.mmv.ui.ProgressBar( $div ); + + assert.ok( progress.$progress.hasClass( 'empty' ), 'Progress bar is hidden' ); + assert.strictEqual( progress.$percent.width(), 0, 'Progress bar\'s indicator is at 0' ); + + this.sandbox.stub( $.fn, 'animate', function ( target ) { + $( this ).css( target ); + assert.strictEqual( target.width, '50%', 'Animation should go to 50%' ); + } ); + progress.animateTo( 50 ); + assert.ok( !progress.$progress.hasClass( 'empty' ), 'Progress bar is visible' ); + + assert.strictEqual( progress.$percent.width(), 125, 'Progress bar\'s indicator is at half' ); + + $.fn.animate.restore(); + this.sandbox.stub( $.fn, 'animate', function ( target, duration, transition, callback ) { + $( this ).css( target ); + + assert.strictEqual( target.width, '100%', 'Animation should go to 100%' ); + + if ( callback !== undefined ) { + callback(); + } + } ); + progress.animateTo( 100 ); + assert.ok( progress.$progress.hasClass( 'empty' ), 'Progress bar is hidden' ); + assert.strictEqual( progress.$percent.width(), 0, 'Progress bar\'s indicator is at 0' ); + } ); + + QUnit.test( 'jumpTo()/hide()', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + $div = $( '<div>' ).css( { width: 250, position: 'relative' } ).appendTo( $qf ), + progress = new mw.mmv.ui.ProgressBar( $div ); + + assert.ok( progress.$progress.hasClass( 'empty' ), 'Progress bar is hidden' ); + assert.strictEqual( progress.$percent.width(), 0, 'Progress bar\'s indicator is at 0' ); + + progress.jumpTo( 50 ); + + assert.ok( !progress.$progress.hasClass( 'empty' ), 'Progress bar is visible' ); + assert.strictEqual( progress.$percent.width(), 125, 'Progress bar\'s indicator is at half' ); + + progress.hide(); + + assert.ok( progress.$progress.hasClass( 'empty' ), 'Progress bar is hidden' ); + assert.strictEqual( progress.$percent.width(), 0, 'Progress bar\'s indicator is at 0' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.dialog.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.dialog.test.js new file mode 100644 index 00000000..01322125 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.dialog.test.js @@ -0,0 +1,250 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MultimediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + function makeReuseDialog( sandbox ) { + var $fixture = $( '#qunit-fixture' ), + config = { getFromLocalStorage: sandbox.stub(), setInLocalStorage: sandbox.stub() }; + return new mw.mmv.ui.reuse.Dialog( $fixture, $( '<div>' ).appendTo( $fixture ), config ); + } + + QUnit.module( 'mmv.ui.reuse.Dialog', QUnit.newMwEnvironment() ); + + QUnit.test( 'Sanity test, object creation and UI construction', function ( assert ) { + var reuseDialog = makeReuseDialog( this.sandbox ); + + assert.ok( reuseDialog, 'Reuse UI element is created.' ); + assert.strictEqual( reuseDialog.$dialog.length, 1, 'Reuse dialog div created.' ); + } ); + + QUnit.test( 'handleOpenCloseClick():', function ( assert ) { + var reuseDialog = makeReuseDialog( this.sandbox ); + + reuseDialog.openDialog = function () { + assert.ok( true, 'openDialog called.' ); + }; + reuseDialog.closeDialog = function () { + assert.ok( false, 'closeDialog should not have been called.' ); + }; + + // Dialog is closed by default, we should open it + reuseDialog.handleOpenCloseClick(); + + reuseDialog.openDialog = function () { + assert.ok( false, 'openDialog should not have been called.' ); + }; + reuseDialog.closeDialog = function () { + assert.ok( true, 'closeDialog called.' ); + }; + reuseDialog.isOpen = true; + + // Dialog open now, we should close it. + reuseDialog.handleOpenCloseClick(); + } ); + + QUnit.test( 'handleTabSelection():', function ( assert ) { + var reuseDialog = makeReuseDialog( this.sandbox ); + + reuseDialog.initTabs(); + + // Share pane is selected + reuseDialog.handleTabSelection( { getData: function () { return 'share'; } } ); + assert.ok( reuseDialog.tabs.share.$pane.hasClass( 'active' ), 'Share tab shown.' ); + assert.ok( !reuseDialog.tabs.embed.$pane.hasClass( 'active' ), 'Embed tab hidden.' ); + assert.ok( reuseDialog.config.setInLocalStorage.calledWith( 'mmv-lastUsedTab', 'share' ), + 'Tab state saved in local storage.' ); + + // Embed pane is selected + reuseDialog.handleTabSelection( { getData: function () { return 'embed'; } } ); + assert.ok( !reuseDialog.tabs.share.$pane.hasClass( 'active' ), 'Share tab hidden.' ); + assert.ok( reuseDialog.tabs.embed.$pane.hasClass( 'active' ), 'Embed tab shown.' ); + } ); + + QUnit.test( 'default tab:', function ( assert ) { + var reuseDialog; + + reuseDialog = makeReuseDialog( this.sandbox ); + reuseDialog.initTabs(); + assert.strictEqual( reuseDialog.selectedTab, 'share', 'Share tab is default' ); + + reuseDialog = makeReuseDialog( this.sandbox ); + reuseDialog.config.getFromLocalStorage.withArgs( 'mmv-lastUsedTab' ).returns( 'share' ); + reuseDialog.initTabs(); + assert.strictEqual( reuseDialog.selectedTab, 'share', 'Default can be overridden' ); + } ); + + QUnit.test( 'attach()/unattach():', function ( assert ) { + var reuseDialog = makeReuseDialog( this.sandbox ); + + reuseDialog.initTabs(); + + reuseDialog.handleOpenCloseClick = function () { + assert.ok( false, 'handleOpenCloseClick should not have been called.' ); + }; + reuseDialog.handleTabSelection = function () { + assert.ok( false, 'handleTabSelection should not have been called.' ); + }; + + // Triggering action events before attaching should do nothing + $( document ).trigger( 'mmv-reuse-open' ); + reuseDialog.reuseTabs.emit( 'select' ); + + reuseDialog.handleOpenCloseClick = function () { + assert.ok( true, 'handleOpenCloseClick called.' ); + }; + reuseDialog.handleTabSelection = function () { + assert.ok( true, 'handleTabSelection called.' ); + }; + + reuseDialog.attach(); + + // Action events should be handled now + $( document ).trigger( 'mmv-reuse-open' ); + reuseDialog.reuseTabs.emit( 'select' ); + + // Test the unattach part + reuseDialog.handleOpenCloseClick = function () { + assert.ok( false, 'handleOpenCloseClick should not have been called.' ); + }; + reuseDialog.handleTabSelection = function () { + assert.ok( false, 'handleTabSelection should not have been called.' ); + }; + + reuseDialog.unattach(); + + // Triggering action events now that we are unattached should do nothing + $( document ).trigger( 'mmv-reuse-open' ); + reuseDialog.reuseTabs.emit( 'select' ); + } ); + + QUnit.test( 'start/stopListeningToOutsideClick():', function ( assert ) { + var reuseDialog = makeReuseDialog( this.sandbox ), + realCloseDialog = reuseDialog.closeDialog; + + reuseDialog.initTabs(); + + function clickOutsideDialog() { + var event = new $.Event( 'click', { target: reuseDialog.$container[ 0 ] } ); + reuseDialog.$container.trigger( event ); + return event; + } + function clickInsideDialog() { + var event = new $.Event( 'click', { target: reuseDialog.$dialog[ 0 ] } ); + reuseDialog.$dialog.trigger( event ); + return event; + } + + function assertDialogDoesNotCatchClicks() { + var event; + reuseDialog.closeDialog = function () { assert.ok( false, 'Dialog is not affected by click' ); }; + event = clickOutsideDialog(); + assert.ok( !event.isDefaultPrevented(), 'Dialog does not affect click' ); + assert.ok( !event.isPropagationStopped(), 'Dialog does not affect click propagation' ); + } + function assertDialogCatchesOutsideClicksOnly() { + var event; + reuseDialog.closeDialog = function () { assert.ok( false, 'Dialog is not affected by inside click' ); }; + event = clickInsideDialog(); + assert.ok( !event.isDefaultPrevented(), 'Dialog does not affect inside click' ); + assert.ok( !event.isPropagationStopped(), 'Dialog does not affect inside click propagation' ); + reuseDialog.closeDialog = function () { assert.ok( true, 'Dialog is closed by outside click' ); }; + event = clickOutsideDialog(); + assert.ok( event.isDefaultPrevented(), 'Dialog catches outside click' ); + assert.ok( event.isPropagationStopped(), 'Dialog stops outside click propagation' ); + } + + assertDialogDoesNotCatchClicks(); + reuseDialog.openDialog(); + assertDialogCatchesOutsideClicksOnly(); + realCloseDialog.call( reuseDialog ); + assertDialogDoesNotCatchClicks(); + reuseDialog.openDialog(); + reuseDialog.unattach(); + assertDialogDoesNotCatchClicks(); + } ); + + QUnit.test( 'set()/empty() sanity check:', function ( assert ) { + var reuseDialog = makeReuseDialog( this.sandbox ), + title = mw.Title.newFromText( 'File:Foobar.jpg' ), + src = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg', + url = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg', + image = { // fake mw.mmv.model.Image + title: title, + url: src, + descriptionUrl: url, + width: 100, + height: 80 + }, + embedFileInfo = new mw.mmv.model.EmbedFileInfo( title, src, url ); + + reuseDialog.set( image, embedFileInfo ); + reuseDialog.empty(); + + assert.ok( true, 'Set/empty did not cause an error.' ); + } ); + + QUnit.test( 'openDialog()/closeDialog():', function ( assert ) { + var reuseDialog = makeReuseDialog( this.sandbox ), + title = mw.Title.newFromText( 'File:Foobar.jpg' ), + src = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg', + url = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg', + image = { // fake mw.mmv.model.Image + title: title, + url: src, + descriptionUrl: url, + width: 100, + height: 80 + }, + repoInfo = new mw.mmv.model.Repo( 'Wikipedia', '//wikipedia.org/favicon.ico', true ); + + reuseDialog.initTabs(); + + reuseDialog.set( image, repoInfo ); + + assert.ok( !reuseDialog.isOpen, 'Dialog closed by default.' ); + + reuseDialog.openDialog(); + + assert.ok( reuseDialog.isOpen, 'Dialog open now.' ); + + reuseDialog.closeDialog(); + + assert.ok( !reuseDialog.isOpen, 'Dialog closed now.' ); + } ); + + QUnit.test( 'getImageWarnings():', function ( assert ) { + var reuseDialog = makeReuseDialog( this.sandbox ), + title = mw.Title.newFromText( 'File:Foobar.jpg' ), + src = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg', + url = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg', + image = { // fake mw.mmv.model.Image + title: title, + url: src, + descriptionUrl: url, + width: 100, + height: 80 + }, + imageDeleted = $.extend( { deletionReason: 'deleted file test' }, image ); + + // Test that the lack of license is picked up + assert.equal( 1, reuseDialog.getImageWarnings( image ).length, 'Lack of license detected' ); + + // Test that deletion supersedes other warnings and only that one is reported + assert.equal( 1, reuseDialog.getImageWarnings( imageDeleted ).length, 'Deletion detected' ); + } ); + +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.embed.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.embed.test.js new file mode 100644 index 00000000..69ca2466 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.embed.test.js @@ -0,0 +1,398 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MultimediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + var $qf = $( '#qunit-fixture' ); + + QUnit.module( 'mmv.ui.reuse.Embed', QUnit.newMwEnvironment() ); + + QUnit.test( 'Sanity test, object creation and UI construction', function ( assert ) { + var embed = new mw.mmv.ui.reuse.Embed( $qf ); + + assert.ok( embed, 'Embed UI element is created.' ); + assert.strictEqual( embed.$pane.length, 1, 'Pane div is created.' ); + assert.ok( embed.embedTextHtml, 'Html snipped text area created.' ); + assert.ok( embed.embedTextWikitext, 'Wikitext snipped text area created.' ); + assert.ok( embed.embedSwitch, 'Snipped selection buttons created.' ); + assert.ok( embed.embedSizeSwitchWikitext, 'Size selection menu for wikitext created.' ); + assert.ok( embed.embedSizeSwitchHtml, 'Size selection menu for html created.' ); + assert.strictEqual( embed.$currentMainEmbedText.length, 1, 'Size selection menu for html created.' ); + assert.strictEqual( embed.isSizeMenuDefaultReset, false, 'Reset flag intialized correctly.' ); + assert.ok( embed.defaultHtmlItem, 'Default item for html size selection intialized.' ); + assert.ok( embed.defaultWikitextItem, 'Default item for wikitext size selection intialized.' ); + assert.ok( embed.currentSizeMenu, 'Current size menu intialized.' ); + assert.ok( embed.currentDefaultItem, 'Current default item intialized.' ); + } ); + + QUnit.test( 'changeSize(): Skip if no item selected.', function ( assert ) { + var embed = new mw.mmv.ui.reuse.Embed( $qf ), + width = 10, + height = 20; + + assert.expect( 0 ); + + // deselect items + embed.embedSwitch.selectItem(); + + embed.updateEmbedHtml = function () { + assert.ok( false, 'No item selected, this should not have been called.' ); + }; + embed.updateEmbedWikitext = function () { + assert.ok( false, 'No item selected, this should not have been called.' ); + }; + + embed.changeSize( width, height ); + } ); + + QUnit.test( 'changeSize(): HTML size menu item selected.', function ( assert ) { + var embed = new mw.mmv.ui.reuse.Embed( $qf ), + width = 10, + height = 20; + + embed.embedSwitch.findSelectedItem = function () { + return { getData: function () { return 'html'; } }; + }; + embed.updateEmbedHtml = function ( thumb, w, h ) { + assert.strictEqual( thumb.url, undefined, 'Empty thumbnail passed.' ); + assert.strictEqual( w, width, 'Correct width passed.' ); + assert.strictEqual( h, height, 'Correct height passed.' ); + }; + embed.updateEmbedWikitext = function () { + assert.ok( false, 'Dealing with HTML menu, this should not have been called.' ); + }; + embed.select = function () { + assert.ok( true, 'Item selected after update.' ); + }; + + embed.changeSize( width, height ); + } ); + + QUnit.test( 'changeSize(): Wikitext size menu item selected.', function ( assert ) { + var embed = new mw.mmv.ui.reuse.Embed( $qf ), + width = 10, + height = 20; + + embed.embedSwitch.findSelectedItem = function () { + return { getData: function () { return 'wikitext'; } }; + }; + embed.updateEmbedHtml = function () { + assert.ok( false, 'Dealing with wikitext menu, this should not have been called.' ); + }; + embed.updateEmbedWikitext = function ( w ) { + assert.strictEqual( w, width, 'Correct width passed.' ); + }; + embed.select = function () { + assert.ok( true, 'Item selected after update.' ); + }; + + embed.changeSize( width, height ); + } ); + + QUnit.test( 'updateEmbedHtml(): Do nothing if set() not called before.', function ( assert ) { + var embed = new mw.mmv.ui.reuse.Embed( $qf ), + width = 10, + height = 20; + + assert.expect( 0 ); + + embed.formatter.getThumbnailHtml = function () { + assert.ok( false, 'formatter.getThumbnailHtml() should not have been called.' ); + }; + embed.updateEmbedHtml( {}, width, height ); + } ); + + QUnit.test( 'updateEmbedHtml():', function ( assert ) { + var embed = new mw.mmv.ui.reuse.Embed( $qf ), + url = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg', + thumbUrl = 'https://upload.wikimedia.org/wikipedia/thumb/Foobar.jpg', + imageInfo = { url: url }, + repoInfo = {}, + caption = '-', + info = new mw.mmv.model.EmbedFileInfo( imageInfo, repoInfo, caption ), + width = 10, + height = 20; + + embed.set( imageInfo, repoInfo, caption ); + + // Small image, no thumbnail info is passed + embed.formatter.getThumbnailHtml = function ( i, u, w, h ) { + assert.deepEqual( i, info, 'Info passed correctly.' ); + assert.strictEqual( u, url, 'Image URL passed correctly.' ); + assert.strictEqual( w, width, 'Correct width passed.' ); + assert.strictEqual( h, height, 'Correct height passed.' ); + }; + embed.updateEmbedHtml( {}, width, height ); + + // Small image, thumbnail info present + embed.formatter.getThumbnailHtml = function ( i, u ) { + assert.strictEqual( u, thumbUrl, 'Image src passed correctly.' ); + }; + embed.updateEmbedHtml( { url: thumbUrl }, width, height ); + + // Big image, thumbnail info present + embed.formatter.getThumbnailHtml = function ( i, u ) { + assert.strictEqual( u, url, 'Image src passed correctly.' ); + }; + width = 1300; + embed.updateEmbedHtml( { url: thumbUrl }, width, height ); + } ); + + QUnit.test( 'updateEmbedWikitext(): Do nothing if set() not called before.', function ( assert ) { + var embed = new mw.mmv.ui.reuse.Embed( $qf ), + width = 10; + + assert.expect( 0 ); + + embed.formatter.getThumbnailWikitext = function () { + assert.ok( false, 'formatter.getThumbnailWikitext() should not have been called.' ); + }; + embed.updateEmbedWikitext( width ); + } ); + + QUnit.test( 'updateEmbedWikitext():', function ( assert ) { + var embed = new mw.mmv.ui.reuse.Embed( $qf ), + imageInfo = {}, + repoInfo = {}, + caption = '-', + info = new mw.mmv.model.EmbedFileInfo( imageInfo, repoInfo, caption ), + width = 10; + + embed.set( imageInfo, repoInfo, caption ); + + embed.formatter.getThumbnailWikitextFromEmbedFileInfo = function ( i, w ) { + assert.deepEqual( i, info, 'EmbedFileInfo passed correctly.' ); + assert.strictEqual( w, width, 'Width passed correctly.' ); + }; + embed.updateEmbedWikitext( width ); + } ); + + QUnit.test( 'getPossibleImageSizesForWikitext()', function ( assert ) { + var embed = new mw.mmv.ui.reuse.Embed( $qf ), + exampleSizes = [ + // Big wide image + { + width: 2048, height: 1536, + expected: { + small: { width: 300, height: 225 }, + medium: { width: 400, height: 300 }, + large: { width: 500, height: 375 }, + 'default': { width: null, height: null } + } + }, + + // Big tall image + { + width: 201, height: 1536, + expected: { + 'default': { width: null, height: null } + } + }, + + // Very small image + { + width: 15, height: 20, + expected: { + 'default': { width: null, height: null } + } + } + ], + i, cursize, opts; + for ( i = 0; i < exampleSizes.length; i++ ) { + cursize = exampleSizes[ i ]; + opts = embed.getPossibleImageSizesForWikitext( cursize.width, cursize.height ); + assert.deepEqual( opts, cursize.expected, 'We got the expected results out of the size calculation function.' ); + } + } ); + + QUnit.test( 'set():', function ( assert ) { + var embed = new mw.mmv.ui.reuse.Embed( $qf ), + title = mw.Title.newFromText( 'File:Foobar.jpg' ), + src = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg', + url = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg', + embedFileInfo = new mw.mmv.model.EmbedFileInfo( title, src, url ), + calledSelect = false, + width = 15, + height = 20; + + embed.utils.updateMenuOptions = function ( sizes, options ) { + assert.strictEqual( options.length, 4, 'Options passed correctly.' ); + }; + embed.resetCurrentSizeMenuToDefault = function () { + assert.ok( true, 'resetCurrentSizeMenuToDefault() is called.' ); + }; + embed.utils.getThumbnailUrlPromise = function () { + return $.Deferred().resolve().promise(); + }; + embed.updateEmbedHtml = function () { + assert.ok( true, 'updateEmbedHtml() is called after data is collected.' ); + }; + embed.select = function () { + calledSelect = true; + }; + + assert.ok( !embed.embedFileInfo, 'embedFileInfo not set yet.' ); + + embed.set( { width: width, height: height }, embedFileInfo ); + + assert.ok( embed.embedFileInfo, 'embedFileInfo correctly set.' ); + assert.strictEqual( embed.isSizeMenuDefaultReset, false, 'Reset flag cleared.' ); + assert.strictEqual( calledSelect, true, 'select() is called' ); + } ); + + QUnit.test( 'empty():', function ( assert ) { + var embed = new mw.mmv.ui.reuse.Embed( $qf ), + width = 15, + height = 20; + + embed.formatter = { + getThumbnailWikitextFromEmbedFileInfo: function () { return 'wikitext'; }, + getThumbnailHtml: function () { return 'html'; } + }; + + embed.set( {}, {} ); + embed.updateEmbedHtml( { url: 'x' }, width, height ); + embed.updateEmbedWikitext( width ); + + assert.notStrictEqual( embed.embedTextHtml.getValue(), '', 'embedTextHtml is not empty.' ); + assert.notStrictEqual( embed.embedTextWikitext.getValue(), '', 'embedTextWikitext is not empty.' ); + + embed.empty(); + + assert.strictEqual( embed.embedTextHtml.getValue(), '', 'embedTextHtml is empty.' ); + assert.strictEqual( embed.embedTextWikitext.getValue(), '', 'embedTextWikitext is empty.' ); + assert.ok( !embed.embedSizeSwitchHtml.getMenu().isVisible(), 'Html size menu should be hidden.' ); + assert.ok( !embed.embedSizeSwitchWikitext.getMenu().isVisible(), 'Wikitext size menu should be hidden.' ); + } ); + + QUnit.test( 'attach()/unattach():', function ( assert ) { + var embed = new mw.mmv.ui.reuse.Embed( $qf ), + title = mw.Title.newFromText( 'File:Foobar.jpg' ), + src = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg', + url = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg', + embedFileInfo = new mw.mmv.model.EmbedFileInfo( title, src, url ), + width = 15, + height = 20; + + embed.set( { width: width, height: height }, embedFileInfo ); + + embed.selectAllOnEvent = function () { + assert.ok( false, 'selectAllOnEvent should not have been called.' ); + }; + embed.handleTypeSwitch = function () { + assert.ok( false, 'handleTypeSwitch should not have been called.' ); + }; + embed.handleSizeSwitch = function () { + assert.ok( false, 'handleTypeSwitch should not have been called.' ); + }; + + // Triggering action events before attaching should do nothing + // use of focus() would run into jQuery bug #14740 and similar issues + embed.embedTextHtml.$element.find( 'textarea' ).triggerHandler( 'focus' ); + embed.embedTextWikitext.$element.find( 'textarea' ).triggerHandler( 'focus' ); + embed.embedSwitch.emit( 'select' ); + embed.embedSizeSwitchHtml.getMenu().emit( + 'choose', embed.embedSizeSwitchHtml.getMenu().findSelectedItem() ); + embed.embedSizeSwitchWikitext.getMenu().emit( + 'choose', embed.embedSizeSwitchWikitext.getMenu().findSelectedItem() ); + + embed.selectAllOnEvent = function () { + assert.ok( true, 'selectAllOnEvent was called.' ); + }; + embed.handleTypeSwitch = function () { + assert.ok( true, 'handleTypeSwitch was called.' ); + }; + embed.handleSizeSwitch = function () { + assert.ok( true, 'handleTypeSwitch was called.' ); + }; + + embed.attach(); + + // Action events should be handled now + embed.embedTextHtml.$element.find( 'textarea' ).triggerHandler( 'focus' ); + embed.embedTextWikitext.$element.find( 'textarea' ).triggerHandler( 'focus' ); + embed.embedSwitch.emit( 'select' ); + embed.embedSizeSwitchHtml.getMenu().emit( + 'choose', embed.embedSizeSwitchHtml.getMenu().findSelectedItem() ); + embed.embedSizeSwitchWikitext.getMenu().emit( + 'choose', embed.embedSizeSwitchWikitext.getMenu().findSelectedItem() ); + + // Test the unattach part + embed.selectAllOnEvent = function () { + assert.ok( false, 'selectAllOnEvent should not have been called.' ); + }; + embed.handleTypeSwitch = function () { + assert.ok( false, 'handleTypeSwitch should not have been called.' ); + }; + embed.handleSizeSwitch = function () { + assert.ok( false, 'handleTypeSwitch should not have been called.' ); + }; + + embed.unattach(); + + // Triggering action events now that we are unattached should do nothing + embed.embedTextHtml.$element.find( 'textarea' ).triggerHandler( 'focus' ); + embed.embedTextWikitext.$element.find( 'textarea' ).triggerHandler( 'focus' ); + embed.embedSwitch.emit( 'select' ); + embed.embedSizeSwitchHtml.getMenu().emit( + 'choose', embed.embedSizeSwitchHtml.getMenu().findSelectedItem() ); + embed.embedSizeSwitchWikitext.getMenu().emit( + 'choose', embed.embedSizeSwitchWikitext.getMenu().findSelectedItem() ); + } ); + + QUnit.test( 'handleTypeSwitch():', function ( assert ) { + var embed = new mw.mmv.ui.reuse.Embed( $qf ); + + assert.strictEqual( embed.isSizeMenuDefaultReset, false, 'Reset flag intialized correctly.' ); + + embed.resetCurrentSizeMenuToDefault = function () { + assert.ok( true, 'resetCurrentSizeMenuToDefault() called.' ); + }; + + // HTML selected + embed.handleTypeSwitch( { getData: function () { return 'html'; } } ); + + assert.strictEqual( embed.isSizeMenuDefaultReset, true, 'Reset flag updated correctly.' ); + assert.ok( !embed.embedSizeSwitchWikitext.getMenu().isVisible(), 'Wikitext size menu should be hidden.' ); + + embed.resetCurrentSizeMenuToDefault = function () { + assert.ok( false, 'resetCurrentSizeMenuToDefault() should not have been called.' ); + }; + + // Wikitext selected, we are done resetting defaults + embed.handleTypeSwitch( { getData: function () { return 'wikitext'; } } ); + + assert.strictEqual( embed.isSizeMenuDefaultReset, true, 'Reset flag updated correctly.' ); + assert.ok( !embed.embedSizeSwitchHtml.getMenu().isVisible(), 'HTML size menu should be hidden.' ); + } ); + + QUnit.test( 'Logged out', function ( assert ) { + var embed, + oldUserIsAnon = mw.user.isAnon; + + mw.user.isAnon = function () { return true; }; + + embed = new mw.mmv.ui.reuse.Embed( $qf ); + + assert.ok( !embed.embedSizeSwitchWikitext.$element.hasClass( 'active' ), 'Wikitext widget should be hidden.' ); + assert.ok( embed.embedSizeSwitchHtml.$element.hasClass( 'active' ), 'HTML widget should be visible.' ); + assert.ok( !embed.embedTextWikitext.$element.hasClass( 'active' ), 'Wikitext input should be hidden.' ); + assert.ok( embed.embedTextHtml.$element.hasClass( 'active' ), 'HTML input should be visible.' ); + + mw.user.isAnon = oldUserIsAnon; + } ); + +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.share.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.share.test.js new file mode 100644 index 00000000..5757d35b --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.share.test.js @@ -0,0 +1,95 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MultimediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + function makeShare() { + return new mw.mmv.ui.reuse.Share( $( '#qunit-fixture' ) ); + } + + QUnit.module( 'mmv.ui.reuse.share', QUnit.newMwEnvironment() ); + + QUnit.test( 'Sanity test, object creation and UI construction', function ( assert ) { + var share = makeShare(); + + assert.ok( share, 'Share UI element is created.' ); + assert.strictEqual( share.$pane.length, 1, 'Pane div created.' ); + assert.ok( share.pageInput, 'Text field created.' ); + assert.ok( share.$pageLink, 'Link created.' ); + } ); + + QUnit.test( 'set()/empty():', function ( assert ) { + var share = makeShare(), + image = { // fake mw.mmv.model.Image + title: new mw.Title( 'File:Foobar.jpg' ), + url: 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg', + descriptionUrl: '//commons.wikimedia.org/wiki/File:Foobar.jpg' + }; + + assert.notStrictEqual( !share.pageInput.getValue(), '', 'pageInput is empty.' ); + + share.select = function () { + assert.ok( true, 'Text has been selected after data is set.' ); + }; + + share.set( image ); + + assert.notStrictEqual( share.pageInput.getValue(), '', 'pageInput is not empty.' ); + + share.empty(); + + assert.notStrictEqual( !share.pageInput.getValue(), '', 'pageInput is empty.' ); + } ); + + QUnit.test( 'attach()/unattach():', function ( assert ) { + var share = makeShare(), + image = { + title: new mw.Title( 'File:Foobar.jpg' ), + url: 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg', + descriptionUrl: '//commons.wikimedia.org/wiki/File:Foobar.jpg' + }; + + share.set( image ); + + share.selectAllOnEvent = function () { + assert.ok( false, 'selectAllOnEvent should not have been called.' ); + }; + + // Triggering action events before attaching should do nothing + // use of focus() would run into jQuery bug #14740 and similar issues + share.pageInput.$element.find( 'input' ).triggerHandler( 'focus' ); + + share.selectAllOnEvent = function () { + assert.ok( true, 'selectAllOnEvent was called.' ); + }; + + share.attach(); + + // Action events should be handled now + share.pageInput.$element.find( 'input' ).triggerHandler( 'focus' ); + + // Test the unattach part + share.selectAllOnEvent = function () { + assert.ok( false, 'selectAllOnEvent should not have been called.' ); + }; + + share.unattach(); + + // Triggering action events now that we are unattached should do nothing + share.pageInput.$element.find( 'input' ).triggerHandler( 'focus' ); + } ); + +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.tab.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.tab.test.js new file mode 100644 index 00000000..9bc7c4fa --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.tab.test.js @@ -0,0 +1,43 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MultimediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + var $fixture = $( '#qunit-fixture' ); + + function makeReuseTab() { + return new mw.mmv.ui.reuse.Tab( $( '<div>' ).appendTo( $fixture ), $fixture ); + } + + QUnit.module( 'mmv.ui.reuse.Tab', QUnit.newMwEnvironment() ); + + QUnit.test( 'Object creation, UI construction and basic funtionality', function ( assert ) { + var reuseTab = makeReuseTab(); + + assert.ok( reuseTab, 'Reuse UI element is created.' ); + assert.strictEqual( reuseTab.$pane.length, 1, 'Pane created.' ); + + assert.ok( !reuseTab.$pane.hasClass( 'active' ), 'Tab is not active.' ); + + reuseTab.show(); + + assert.ok( reuseTab.$pane.hasClass( 'active' ), 'Tab is active.' ); + + reuseTab.hide(); + + assert.ok( !reuseTab.$pane.hasClass( 'active' ), 'Tab is not active.' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.utils.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.utils.test.js new file mode 100644 index 00000000..07967dd9 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.utils.test.js @@ -0,0 +1,117 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MultimediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mw.mmv.ui.reuse.utils', QUnit.newMwEnvironment() ); + + QUnit.test( 'Sanity test, object creation and UI construction', function ( assert ) { + var utils = new mw.mmv.ui.Utils(); + + assert.ok( utils, 'ReuseUtils object is created.' ); + } ); + + QUnit.test( 'createPulldownMenu():', function ( assert ) { + var utils = new mw.mmv.ui.Utils(), + menuItems = [ 'original', 'small', 'medium', 'large' ], + def = 'large', + menu = utils.createPulldownMenu( + menuItems, + [ 'mw-mmv-download-size' ], + def + ), + options = menu.getMenu().getItems(), + i, data; + + assert.strictEqual( options.length, 4, 'Menu has correct number of items.' ); + + for ( i = 0; i < menuItems.length; i++ ) { + data = options[ i ].getData(); + + assert.strictEqual( data.name, menuItems[ i ], 'Correct item name on the list.' ); + assert.strictEqual( data.height, null, 'Correct item height on the list.' ); + assert.strictEqual( data.width, null, 'Correct item width on the list.' ); + } + + assert.strictEqual( menu.getMenu().findSelectedItem(), options[ 3 ], 'Default set correctly.' ); + } ); + + QUnit.test( 'updateMenuOptions():', function ( assert ) { + var utils = new mw.mmv.ui.Utils(), + menu = utils.createPulldownMenu( + [ 'original', 'small', 'medium', 'large' ], + [ 'mw-mmv-download-size' ], + 'original' + ), + options = menu.getMenu().getItems(), + width = 700, + height = 500, + sizes = utils.getPossibleImageSizesForHtml( width, height ), + oldMessage = mw.message; + + mw.message = function ( messageKey ) { + assert.ok( messageKey.match( /^multimediaviewer-(small|medium|original|embed-dimensions)/ ), 'messageKey passed correctly.' ); + + return { text: $.noop }; + }; + + utils.updateMenuOptions( sizes, options ); + + mw.message = oldMessage; + } ); + + QUnit.test( 'getPossibleImageSizesForHtml()', function ( assert ) { + var utils = new mw.mmv.ui.Utils(), + exampleSizes = [ + // Big wide image + { + width: 2048, height: 1536, + expected: { + small: { width: 193, height: 145 }, + medium: { width: 640, height: 480 }, + large: { width: 1200, height: 900 }, + original: { width: 2048, height: 1536 } + } + }, + + // Big tall image + { + width: 201, height: 1536, + expected: { + small: { width: 19, height: 145 }, + medium: { width: 63, height: 480 }, + large: { width: 118, height: 900 }, + original: { width: 201, height: 1536 } + } + }, + + // Very small image + { + width: 15, height: 20, + expected: { + original: { width: 15, height: 20 } + } + } + ], + i, cursize, opts; + for ( i = 0; i < exampleSizes.length; i++ ) { + cursize = exampleSizes[ i ]; + opts = utils.getPossibleImageSizesForHtml( cursize.width, cursize.height ); + assert.deepEqual( opts, cursize.expected, 'We got the expected results out of the size calculation function.' ); + } + } ); + +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.stripeButtons.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.stripeButtons.test.js new file mode 100644 index 00000000..7ebe04e9 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.stripeButtons.test.js @@ -0,0 +1,76 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.ui.StripeButtons', QUnit.newMwEnvironment() ); + + function createStripeButtons() { + var fixture = $( '#qunit-fixture' ); + return new mw.mmv.ui.StripeButtons( fixture ); + } + + QUnit.test( 'Sanity test, object creation and UI construction', function ( assert ) { + var buttons, + oldMwUserIsAnon = mw.user.isAnon; + + // first pretend we are anonymous + mw.user.isAnon = function () { return true; }; + buttons = createStripeButtons(); + + assert.ok( buttons, 'UI element is created.' ); + assert.ok( buttons.buttons.$descriptionPage, 'File page button created for anon.' ); + + // now pretend we are logged in + mw.user.isAnon = function () { return false; }; + buttons = createStripeButtons(); + + assert.strictEqual( buttons.buttons.$descriptionPage.length, 1, 'File page button created for logged in.' ); + + mw.user.isAnon = oldMwUserIsAnon; + } ); + + QUnit.test( 'set()/empty() sanity test:', function ( assert ) { + var buttons = createStripeButtons(), + fakeImageInfo = { descriptionUrl: '//commons.wikimedia.org/wiki/File:Foo.jpg' }, + fakeRepoInfo = { displayName: 'Wikimedia Commons', isCommons: function () { return true; } }; + + buttons.set( fakeImageInfo, fakeRepoInfo ); + buttons.empty(); + + assert.ok( true, 'No error on set()/empty().' ); + } ); + + QUnit.test( 'Description page button', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + buttons = new mw.mmv.ui.StripeButtons( $qf ), + button = buttons.buttons.$descriptionPage, + descriptionUrl = 'http://example.com/desc', + imageInfo = { descriptionUrl: descriptionUrl }, + repoInfo = { isCommons: function () { return false; } }; + + buttons.setDescriptionPageButton( imageInfo, repoInfo ); + + assert.ok( !button.hasClass( 'mw-mmv-repo-button-commons' ), 'Button does not have commons class non-Commons files' ); + assert.strictEqual( button.find( 'a' ).addBack().filter( 'a' ).attr( 'href' ), descriptionUrl, 'Description page link is correct' ); + + repoInfo.isCommons = function () { return true; }; + buttons.setDescriptionPageButton( imageInfo, repoInfo ); + + assert.ok( button.hasClass( 'mw-mmv-repo-button-commons' ), 'Button commons class for Commons files' ); + } ); + +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.test.js new file mode 100644 index 00000000..7f78a060 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.test.js @@ -0,0 +1,109 @@ +( function ( mw, $ ) { + QUnit.module( 'mmv.ui', QUnit.newMwEnvironment( { + setup: function () { + this.clock = this.sandbox.useFakeTimers(); + } + } ) ); + + QUnit.test( 'handleEvent()', function ( assert ) { + var element = new mw.mmv.ui.Element( $( '<div>' ) ); + + element.handleEvent( 'mmv-foo', function () { + assert.ok( true, 'Event is handled' ); + } ); + + $( document ).trigger( new $.Event( 'mmv-foo' ) ); + + element.clearEvents(); + + $( document ).trigger( new $.Event( 'mmv-foo' ) ); + } ); + + QUnit.test( 'setInlineStyle()', function ( assert ) { + var element = new mw.mmv.ui.Element( $( '<div>' ) ), + $testDiv = $( '<div id="mmv-testdiv">!!!</div>' ).appendTo( '#qunit-fixture' ); + + assert.ok( $testDiv.is( ':visible' ), 'Test div is visible' ); + + element.setInlineStyle( 'test', '#mmv-testdiv { display: none; }' ); + + assert.ok( !$testDiv.is( ':visible' ), 'Test div is hidden by inline style' ); + + element.setInlineStyle( 'test', null ); + + assert.ok( $testDiv.is( ':visible' ), 'Test div is visible again' ); + } ); + + QUnit.test( 'setTimer()/clearTimer()/resetTimer()', function ( assert ) { + var element = new mw.mmv.ui.Element( $( '<div>' ) ), + element2 = new mw.mmv.ui.Element( $( '<div>' ) ), + spy = this.sandbox.spy(), + spy2 = this.sandbox.spy(); + + element.setTimer( 'foo', spy, 10 ); + this.clock.tick( 100 ); + assert.ok( spy.called, 'Timeout callback was called' ); + assert.ok( spy.calledOnce, 'Timeout callback was called once' ); + assert.ok( spy.calledOn( element ), 'Timeout callback was called on the element' ); + + spy = this.sandbox.spy(); + element.setTimer( 'foo', spy, 10 ); + element.setTimer( 'foo', spy2, 20 ); + this.clock.tick( 100 ); + assert.ok( !spy.called, 'Old timeout callback was not called after update' ); + assert.ok( spy2.called, 'New timeout callback was called after update' ); + + spy = this.sandbox.spy(); + spy2 = this.sandbox.spy(); + element.setTimer( 'foo', spy, 10 ); + element.setTimer( 'bar', spy2, 20 ); + this.clock.tick( 100 ); + assert.ok( spy.called && spy2.called, 'Timeouts with different names do not conflict' ); + + spy = this.sandbox.spy(); + spy2 = this.sandbox.spy(); + element.setTimer( 'foo', spy, 10 ); + element2.setTimer( 'foo', spy2, 20 ); + this.clock.tick( 100 ); + assert.ok( spy.called && spy2.called, 'Timeouts in different elements do not conflict' ); + + spy = this.sandbox.spy(); + element.setTimer( 'foo', spy, 10 ); + element.clearTimer( 'foo' ); + this.clock.tick( 100 ); + assert.ok( !spy.called, 'Timeout is invalidated by clearing' ); + + spy = this.sandbox.spy(); + element.setTimer( 'foo', spy, 100 ); + this.clock.tick( 80 ); + element.resetTimer( 'foo' ); + this.clock.tick( 80 ); + assert.ok( !spy.called, 'Timeout is reset' ); + this.clock.tick( 80 ); + assert.ok( spy.called, 'Timeout works after reset' ); + + spy = this.sandbox.spy(); + element.setTimer( 'foo', spy, 100 ); + this.clock.tick( 80 ); + element.resetTimer( 'foo', 200 ); + this.clock.tick( 180 ); + assert.ok( !spy.called, 'Timeout is reset to the designated delay' ); + this.clock.tick( 80 ); + assert.ok( spy.called, 'Timeout works after changing the delay' ); + } ); + + QUnit.test( 'correctEW()', function ( assert ) { + var element = new mw.mmv.ui.Element( $( '<div>' ) ); + + element.isRTL = this.sandbox.stub().returns( true ); + + assert.strictEqual( element.correctEW( 'e' ), 'w', 'e (east) is flipped' ); + assert.strictEqual( element.correctEW( 'ne' ), 'nw', 'ne (northeast) is flipped' ); + assert.strictEqual( element.correctEW( 'W' ), 'E', 'uppercase is flipped' ); + assert.strictEqual( element.correctEW( 's' ), 's', 'non-horizontal directions are ignored' ); + + element.isRTL.returns( false ); + + assert.strictEqual( element.correctEW( 'e' ), 'e', 'no flipping in LTR documents' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.tipsyDialog.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.tipsyDialog.test.js new file mode 100644 index 00000000..3d4044ec --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.tipsyDialog.test.js @@ -0,0 +1,68 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.ui.tipsyDialog', QUnit.newMwEnvironment( { + setup: function () { + // remove tipsy elements left behind by other tests so these tests don't find them by accident + // tipsy puts its elements to the end of the body so clearing the fixture doesn't get rid of them + $( '.mw-mmv-tipsy-dialog' ).remove(); + } + } ) ); + + QUnit.test( 'Open/close', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + $anchor = $( '<div>' ).appendTo( $qf ), + dialog = new mw.mmv.ui.TipsyDialog( $anchor ); + + assert.ok( !$( '.mw-mmv-tipsy-dialog' ).length, 'dialog is not shown' ); + dialog.open(); + assert.ok( $( '.mw-mmv-tipsy-dialog' ).length, 'dialog is shown' ); + dialog.close(); + assert.ok( !$( '.mw-mmv-tipsy-dialog' ).length, 'dialog is not shown' ); + } ); + + QUnit.test( 'setContent', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + $anchor = $( '<div>' ).appendTo( $qf ), + titleText = 'This is a title', + bodyText = 'This is the <b class="typsyDialogTest-123">body</b>', + dialog = new mw.mmv.ui.TipsyDialog( $anchor ); + + dialog.setContent( titleText, bodyText ); + dialog.open(); + assert.ok( $( '.mw-mmv-tipsy-dialog' ).text().match( titleText ), 'Title is included' ); + assert.ok( $( '.mw-mmv-tipsy-dialog' ).html().match( bodyText ), 'Body is included' ); + assert.ok( $( '.mw-mmv-tipsy-dialog' ).find( '.typsyDialogTest-123' ).length, 'Body is HTML' ); + } ); + + QUnit.test( 'Close on click', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + $anchor = $( '<div>' ).appendTo( $qf ), + dialog = new mw.mmv.ui.TipsyDialog( $anchor ); + + dialog.open(); + assert.ok( $( '.mw-mmv-tipsy-dialog' ).length, 'dialog is shown initially' ); + dialog.getPopup().click(); + assert.ok( $( '.mw-mmv-tipsy-dialog' ).length, 'dialog is not hidden when clicked' ); + dialog.getPopup().find( '.mw-mmv-tipsy-dialog-disable' ).click(); + assert.ok( !$( '.mw-mmv-tipsy-dialog' ).length, 'dialog is hidden when close icon is clicked' ); + dialog.open(); + $qf.click(); + assert.ok( !$( '.mw-mmv-tipsy-dialog' ).length, 'dialog is hidden when clicked outside' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.truncatableTextField.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.truncatableTextField.test.js new file mode 100644 index 00000000..6516b2b6 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.truncatableTextField.test.js @@ -0,0 +1,64 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $ ) { + QUnit.module( 'mmv.ui.TruncatableTextField', QUnit.newMwEnvironment() ); + + /** + * Create a textfield that can contain exactly width x height characters + * + * @param {number} width + * @param {number} height + * @param {jQuery} $qf fixture element + * @param {Object} sandbox sinon instance + * @return {TruncatableTextField} + */ + function getField( width, height, $qf, sandbox ) { + var $container = $( '<div>' ).appendTo( $qf ), + $element = $( '<span>' ), + ttf = new mw.mmv.ui.TruncatableTextField( $container, $element, {} ); + + ttf.htmlUtils.htmlToTextWithLinks = sandbox.stub().returnsArg( 0 ); + + $container.css( { + fontFamily: 'monospace', + lineHeight: 1, + width: width + 'ch', + height: height + 'em' + } ); + + return ttf; + } + + QUnit.test( 'Normal constructor', function ( assert ) { + var $container = $( '#qunit-fixture' ), + $element = $( '<div>' ).appendTo( $container ).text( 'This is a unique string.' ), + ttf = new mw.mmv.ui.TruncatableTextField( $container, $element ); + + assert.strictEqual( ttf.$element.text(), 'This is a unique string.', 'The constructor set the element to the right thing.' ); + assert.strictEqual( ttf.$element.closest( '#qunit-fixture' ).length, 1, 'The constructor put the element into the container.' ); + } ); + + QUnit.test( 'Set method', function ( assert ) { + var $qf = $( '#qunit-fixture' ), + ttf = getField( 3, 2, $qf, this.sandbox ); + + ttf.shrink = this.sandbox.stub(); + ttf.set( 'abc' ); + assert.strictEqual( ttf.$element.text(), 'abc', 'Text is set accurately.' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.viewingOptions.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.viewingOptions.test.js new file mode 100644 index 00000000..ee1a9e29 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.viewingOptions.test.js @@ -0,0 +1,139 @@ +( function ( mw, $ ) { + function makeDialog( initialise ) { + var $qf = $( '#qunit-fixture' ), + $button = $( '<div>' ).appendTo( $qf ), + dialog = new mw.mmv.ui.OptionsDialog( $qf, $button, { setMediaViewerEnabledOnClick: $.noop } ); + + if ( initialise ) { + dialog.initPanel(); + } + + return dialog; + } + + QUnit.module( 'mmv.ui.viewingOptions', QUnit.newMwEnvironment() ); + + QUnit.test( 'Constructor sanity test', function ( assert ) { + var dialog = makeDialog(); + assert.ok( dialog, 'Dialog is created successfully' ); + } ); + + QUnit.test( 'Initialisation functions', function ( assert ) { + var dialog = makeDialog( true ); + + assert.ok( dialog.$disableDiv, 'Disable div is created.' ); + assert.ok( dialog.$enableDiv, 'Enable div is created.' ); + assert.ok( dialog.$disableConfirmation, 'Disable confirmation is created.' ); + assert.ok( dialog.$enableConfirmation, 'Enable confirmation is created.' ); + } ); + + QUnit.test( 'Disable', function ( assert ) { + var $header, $icon, $text, $textHeader, $textBody, + $submitButton, $cancelButton, $aboutLink, + dialog = makeDialog(), + deferred = $.Deferred(); + + this.sandbox.stub( dialog.config, 'setMediaViewerEnabledOnClick', function () { + return deferred; + } ); + + dialog.initDisableDiv(); + + $header = dialog.$disableDiv.find( 'h3.mw-mmv-options-dialog-header' ); + $icon = dialog.$disableDiv.find( 'div.mw-mmv-options-icon' ); + + $text = dialog.$disableDiv.find( 'div.mw-mmv-options-text' ); + $textHeader = $text.find( 'p.mw-mmv-options-text-header' ); + $textBody = $text.find( 'p.mw-mmv-options-text-body' ); + $aboutLink = $text.find( 'a.mw-mmv-project-info-link' ); + $submitButton = dialog.$disableDiv.find( 'button.mw-mmv-options-submit-button' ); + $cancelButton = dialog.$disableDiv.find( 'button.mw-mmv-options-cancel-button' ); + + assert.strictEqual( $header.length, 1, 'Disable header created successfully.' ); + assert.strictEqual( $header.text(), 'Disable Media Viewer?', 'Disable header has correct text (if this fails, it may be due to i18n differences)' ); + + assert.strictEqual( $icon.length, 1, 'Icon created successfully.' ); + assert.strictEqual( $icon.html(), ' ', 'Icon has a blank space in it.' ); + + assert.ok( $text, 'Text div created successfully.' ); + assert.strictEqual( $textHeader.length, 1, 'Text header created successfully.' ); + assert.strictEqual( $textHeader.text(), 'Skip this viewing feature for all files.', 'Text header has correct text (if this fails, it may be due to i18n differences)' ); + + assert.strictEqual( $textBody.length, 1, 'Text body created successfully.' ); + assert.strictEqual( $textBody.text(), 'You can enable it later through the file details page.', 'Text body has correct text (if this fails, it may be due to i18n differences)' ); + + assert.strictEqual( $aboutLink.length, 1, 'About link created successfully.' ); + assert.strictEqual( $aboutLink.text(), 'Learn more', 'About link has correct text (if this fails, it may be due to i18n differences)' ); + + assert.strictEqual( $submitButton.length, 1, 'Disable button created successfully.' ); + assert.strictEqual( $submitButton.text(), 'Disable Media Viewer', 'Disable button has correct text (if this fails, it may be due to i18n differences)' ); + + assert.strictEqual( $cancelButton.length, 1, 'Cancel button created successfully.' ); + assert.strictEqual( $cancelButton.text(), 'Cancel', 'Cancel button has correct text (if this fails, it may be due to i18n differences)' ); + + $submitButton.click(); + + assert.ok( !dialog.$disableConfirmation.hasClass( 'mw-mmv-shown' ), 'Disable confirmation not shown yet' ); + assert.ok( !dialog.$dialog.hasClass( 'mw-mmv-disable-confirmation-shown' ), 'Disable confirmation not shown yet' ); + + // Pretend that the async call in mmv.js succeeded + deferred.resolve(); + + // The confirmation should appear + assert.ok( dialog.$disableConfirmation.hasClass( 'mw-mmv-shown' ), 'Disable confirmation shown' ); + assert.ok( dialog.$dialog.hasClass( 'mw-mmv-disable-confirmation-shown' ), 'Disable confirmation shown' ); + } ); + + QUnit.test( 'Enable', function ( assert ) { + var $header, $icon, $text, $textHeader, $aboutLink, + $submitButton, $cancelButton, + dialog = makeDialog(), + deferred = $.Deferred(); + + this.sandbox.stub( dialog.config, 'setMediaViewerEnabledOnClick', function () { + return deferred; + } ); + + dialog.initEnableDiv(); + + $header = dialog.$enableDiv.find( 'h3.mw-mmv-options-dialog-header' ); + $icon = dialog.$enableDiv.find( 'div.mw-mmv-options-icon' ); + + $text = dialog.$enableDiv.find( 'div.mw-mmv-options-text' ); + $textHeader = $text.find( 'p.mw-mmv-options-text-header' ); + $aboutLink = $text.find( 'a.mw-mmv-project-info-link' ); + $submitButton = dialog.$enableDiv.find( 'button.mw-mmv-options-submit-button' ); + $cancelButton = dialog.$enableDiv.find( 'button.mw-mmv-options-cancel-button' ); + + assert.strictEqual( $header.length, 1, 'Enable header created successfully.' ); + assert.strictEqual( $header.text(), 'Enable Media Viewer?', 'Enable header has correct text (if this fails, it may be due to i18n differences)' ); + + assert.strictEqual( $icon.length, 1, 'Icon created successfully.' ); + assert.strictEqual( $icon.html(), ' ', 'Icon has a blank space in it.' ); + + assert.ok( $text, 'Text div created successfully.' ); + assert.strictEqual( $textHeader.length, 1, 'Text header created successfully.' ); + assert.strictEqual( $textHeader.text(), 'Enable this media viewing feature for all files by default.', 'Text header has correct text (if this fails, it may be due to i18n differences)' ); + + assert.strictEqual( $aboutLink.length, 1, 'About link created successfully.' ); + assert.strictEqual( $aboutLink.text(), 'Learn more', 'About link has correct text (if this fails, it may be due to i18n differences)' ); + + assert.strictEqual( $submitButton.length, 1, 'Enable button created successfully.' ); + assert.strictEqual( $submitButton.text(), 'Enable Media Viewer', 'Enable button has correct text (if this fails, it may be due to i18n differences)' ); + + assert.strictEqual( $cancelButton.length, 1, 'Cancel button created successfully.' ); + assert.strictEqual( $cancelButton.text(), 'Cancel', 'Cancel button has correct text (if this fails, it may be due to i18n differences)' ); + + $submitButton.click(); + + assert.ok( !dialog.$enableConfirmation.hasClass( 'mw-mmv-shown' ), 'Enable confirmation not shown yet' ); + assert.ok( !dialog.$dialog.hasClass( 'mw-mmv-enable-confirmation-shown' ), 'Enable confirmation not shown yet' ); + + // Pretend that the async call in mmv.js succeeded + deferred.resolve(); + + // The confirmation should appear + assert.ok( dialog.$enableConfirmation.hasClass( 'mw-mmv-shown' ), 'Enable confirmation shown' ); + assert.ok( dialog.$dialog.hasClass( 'mw-mmv-enable-confirmation-shown' ), 'Enable confirmation shown' ); + } ); +}( mediaWiki, jQuery ) ); |