summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/MultimediaViewer/tests
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/MultimediaViewer/tests
first commit
Diffstat (limited to 'www/wiki/extensions/MultimediaViewer/tests')
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/LocalSettings.php2
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/README.md1
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/ci.yml28
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/environments.yml42
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.download.feature59
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.navigation.feature23
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.options.feature44
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.performance.feature42
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_download_steps.rb101
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_navigation_steps.rb44
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_options_steps.rb69
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_performance_steps.rb48
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_steps.rb174
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/support/env.rb3
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/commons_page.rb51
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/e2e_test_page.rb87
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/samples/MediaViewerE2ETest.wikitext14
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/phan/config.php19
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ActionLogger.test.js48
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.AttributionLogger.test.js22
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DimensionLogger.test.js17
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DurationLogger.test.js218
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.PerformanceLogger.test.js341
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ViewLogger.test.js87
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.Config.test.js203
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.EmbedFileFormatter.test.js293
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.HtmlUtils.test.js192
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.ThumbnailWidthCalculator.test.js149
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.bootstrap.test.js582
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboximage.test.js10
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboxinterface.test.js306
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.test.js706
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.testhelpers.js174
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.EmbedFileInfo.test.js40
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Image.test.js151
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.IwTitle.test.js43
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.License.test.js161
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Repo.test.js100
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.TaskQueue.test.js276
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.test.js58
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Api.test.js270
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.FileRepoInfo.test.js126
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.GuessedThumbnailInfo.test.js280
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Image.test.js200
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ImageInfo.test.js241
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ThumbnailInfo.test.js165
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.MainFileRoute.test.js24
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.Router.test.js232
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.ThumbnailRoute.test.js32
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvas.test.js287
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvasButtons.test.js36
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.description.test.js42
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.download.pane.test.js164
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js207
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanelScroller.test.js232
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.permission.test.js112
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.progressBar.test.js77
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.dialog.test.js250
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.embed.test.js398
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.share.test.js95
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.tab.test.js43
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.utils.test.js117
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.stripeButtons.test.js76
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.test.js109
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.tipsyDialog.test.js68
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.truncatableTextField.test.js64
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.viewingOptions.test.js139
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 + '&params=' + 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 + '&params=' + ( -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 + '&params=' + 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(), '&nbsp;', '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(), '&nbsp;', '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 ) );