diff --git a/test/selenium/README.md b/test/selenium/README.md new file mode 100644 index 000000000..052f17b3c --- /dev/null +++ b/test/selenium/README.md @@ -0,0 +1,26 @@ +# Prerequisites + +- pyenv: https://github.com/pyenv/pyenv +- python 3: `pyenv install 3.6.0` +- virtualenv: `pyenv shell 3.6.0 ; pip install virtualenv` + +# Installation + +- Change directory to where this file is +- `pyenv local 3.6.0` +- `virtualenv .` +- `source bin/activate` +- `pip install -r requirements.txt` + + +# Running + +The arguments that tell the script what site to point to and how to talk to Browserstack are fed in with environment variables to avoid messing with the `unittest` framework. Most of them have sane defaults for DPLA, but the password is required. The browserstack password can be found by logging in to the Browserstack, going to `automate`, and clicking on `Username and Access Keys`. Note that this password is not the same as the Browserstack webapp login. + +An invocation looks like: + +`TEST_SITE_ROOT="https://dplabeta.org" TEST_PASSWORD="$browserstack-password" python integration.py` + +Currently there are issues with Selenium not waiting around long enough for the page to load on both mobile and desktop Safari. + +The test suite will report failures and successes on the console. A recording of the session for each browser will also be available in Browserstack Automate, but that will only report failures with the test suite execution, not failed expectations. diff --git a/test/selenium/integration.py b/test/selenium/integration.py new file mode 100644 index 000000000..d0fd47db9 --- /dev/null +++ b/test/selenium/integration.py @@ -0,0 +1,426 @@ +from selenium import webdriver +import datetime +import os +import unittest +import traceback + +class ProductTestSuite(unittest.TestCase): + + driver = None + + def test_homepage(self): + self.check({ + '': { + 'title': 'Digital Public Library of America', + 'strings': [ + "Online Exhibitions", + "Primary Source Sets", + "How can I use DPLA?", + "Stay informed", + "DPLA News" + ] + } + }) + + def test_exhibitions_homepage(self): + self.check({ + '/exhibitions': { + 'title': 'Exhibitions | DPLA', + 'strings': [ + "Exhibitions", + "Stories of national significance", + "Featured", + "American Empire", + "Race to the Moon", + "Activism in the US", + "Exhibitions curated by DPLA staff" + ] + } + }) + + def test_exhibition(self): + default_strings = [ + 'In This Exhibition', + 'Citation Information', + 'Explore Exhibition' + ] + self.check({ + '/exhibitions/radio-golden-age': { + 'title': 'Golden Age of Radio in the US | DPLA', + 'strings': default_strings + [ + 'Golden Age of Radio in the US', + 'This is America... where you can listen to your radio in your living room,', + 'history-making events as they happened.', + 'This exhibition was created by Hillary Brady,' + ] + }, + '/exhibitions/outsiders-president-elections': { + 'title': 'Battle on the Ballot: Political Outsiders in US Presidential Elections | DPLA', + 'strings': default_strings + [ + 'Battle on the Ballot: Political Outsiders in US Presidential Elections', + 'Congresswoman Shirley Chisholm', + 'These stories share common themes', + 'This exhibition was curated by DPLA staff members Samantha Gibson and Franky Abbott' + ] + }, + '/exhibitions/new-deal': { + 'title': 'America\'s Great Depression and Roosevelt\'s New Deal | DPLA', + 'strings': default_strings + [ + 'America\'s Great Depression and Roosevelt\'s New Deal', + 'Franklin Delano Roosevelt. This material is protected by copyright law', + 'The stock market crash on October 29', + 'Digital Public Library of America and partners', + ] + } + }) + + def test_exhibition_page(self): + default_strings = [ + 'Exhibitions', + 'Close Exhibition' + ] + self.check({ + '/exhibitions/new-deal/recovery-programs': { + 'title': 'Recovery Programs | DPLA', + 'strings': default_strings + [ + 'Introduction', + 'Roosevelt’s New Deal Recovery', + 'View item information', + 'Poster for the National Recovery Administration' + ] + }, + '/exhibitions/new-deal/recovery-programs/farm-security-administration': { + 'title': 'Recovery Programs | DPLA', + 'strings': default_strings + [ + 'Farm Security Administration (FSA)', + 'created in 1937 under the Department of Agriculture', + 'Fiddlin\' Bill Hensley' + ] + }, + '/exhibitions/shoe-industry-massachusetts/early-shoemaking': { + 'title': 'Early Shoemaking | DPLA', + 'strings': default_strings + [ + 'Introduction', + 'Until America’s Colonial era', + 'View item information', + 'Courtesy of The New York Public Library' + ] + }, + '/exhibitions/shoe-industry-massachusetts/selling-shoes/stores': { + 'title': 'Selling Shoes | DPLA', + 'strings': default_strings + [ + 'Stores', + 'As was the case for other retail establishments', + 'Courtesy of Weymouth Public Libraries' + ] + }, + '/exhibitions/history-us-public-libraries/segregated-libraries/case-study-atlanta': { + 'title': 'Segregated Libraries | DPLA', + 'strings': default_strings + [ + 'At the turn of the twentieth century', + 'Case Study: Atlanta', + 'Photograph of Hallie B. Broker', + ] + } + }) + + def test_browse_by_partner(self): + self.check({ + '/browse-by-partner': { + 'title': 'Browse by Partner | DPLA', + 'strings' : [ + 'HathiTrust', + 'California Digital Library', + 'OKHub' + ] + } + }) + + def test_browse_by_topic_homepage(self): + self.check({ + '/browse-by-topic' : { + 'title' : 'Browse by Topic | DPLA', + 'strings' : [ + 'Browse by Topic', + 'Highlights of collections from libraries', + 'This is a new feature designed', + 'Food', + 'Aviation', + 'Enjoying these topics', + 'Visit Online Exhibitions' + ] + } + }) + + def test_browse_topic(self): + self.check({ + '/browse-by-topic/baseball' : { + 'title': 'Baseball | DPLA', + 'strings' : [ + 'Baseball', + 'African Americans', + 'Discover the big-name players' + ] + }, + '/browse-by-topic/aviation' : { + 'title': 'Aviation | DPLA', + 'strings' : [ + 'Aviation', + 'Sky-High Entertainment', + 'Explore the history of the rise', + 'Aviation on the Ground', + 'Find out more about the aviation' + ] + } + }) + + def test_browse_subtopic(self): + self.check({ + '/browse-by-topic/aviation/pioneering-pilots-1900-1940': { + 'title' : 'Pioneering Pilots, 1900-1940 | DPLA', + 'strings' : [ + 'Pioneering Pilots, 1900-1940', + 'accomplishments propelled aviation forward' + ] + }, + '/browse-by-topic/immigration-since-1840/discrimination-and-reform': { + 'title': 'Discrimination and Reform | DPLA', + 'strings': [ + 'Discrimination and Reform', + 'illustrate how xenophobia' + ] + }, + '/browse-by-topic/women-in-science/botany': { + 'title': 'Botany | DPLA', + 'strings': [ + 'Botany', + 'pioneering work in the field and the lab' + ] + } + }) + + def test_pss_homepage(self): + self.check({ + '/primary-source-sets': { + 'title': 'Primary Source Sets | DPLA', + 'strings' : [ + 'Primary Source Sets', + 'collections exploring', + 'Treaty of Versailles and the End of World War I', + 'African Americans', + 'Victorian Era', + 'Space Race', + 'Postwar United States', + 'The American Whaling Industry', + 'US History' + ] + } + }) + + def test_pss_set(self): + default_strings = [ + 'Primary Source Sets', + 'Created By', + 'Time Period', + 'Subjects', + 'Cite this', + 'Teaching Guide', + 'Additional Resources', + 'Source Set', + 'Related Primary Source Sets', + 'These sets were created and reviewed', + 'To give feedback' + ] + self.check({ + '/primary-source-sets/cotton-gin-and-the-expansion-of-slavery': { + 'title': 'Cotton Gin and the Expansion of Slavery | DPLA', + 'strings' : default_strings + [ + 'Cotton Gin and the Expansion of Slavery', + 'recent college graduate Eli Whitney', + 'Franky Abbott, Digital Public Library of America', + 'US History', + 'Revolution and the New Nation', + 'A photograph of a cotton plant in bloom.', + 'An illustration of enslaved people laboring on a cotton plantation, 1859.', + 'National Constitution Center,' + ] + }, + '/primary-source-sets/fake-news-in-the-1890s-yellow-journalism': { + 'title': 'Fake News in the 1890s: Yellow Journalism | DPLA', + 'strings': default_strings + [ + 'Fake News in the 1890s: Yellow Journalism', + 'post-truth have become common', + 'Melissa Jacobs', + 'The Emergence of Modern America', + 'A cover of an issue', + 'Ervin Wardman' + ] + } + }) + + def test_news(self): + self.check({ + '/news' : { + 'title': 'News | DPLA', + 'strings': [ + 'News Archive', + 'All News', + 'Showing page', + 'Next' + ] + } + }) + + def test_about(self): + self.check({ + '/about/frequently-asked-questions': { + 'title': 'Frequently Asked Questions | DPLA', + 'strings': [ + 'What is DPLA?', + 'Do you answer reference questions?' + ] + }, + '/about/search-tips': { + 'title': 'Search Tips | DPLA', + 'strings': [ + 'Searching for items in DPLA', + 'Wildcards' + ] + }, + '/guides': { + 'title': 'Guides | DPLA', + 'strings': [ + 'Education', + 'Lifelong Learning' + ] + }, + '/guides/the-family-research-guide-to-dpla': { + 'title': 'The Family Research Guide to DPLA | DPLA', + 'strings': [ + 'Explore DPLA', + 'What else can you use?' + ] + }, + '/guides/the-scholarly-research-guide-to-dpla': { + 'title': 'The Scholarly Research Guide to DPLA | DPLA', + 'strings': [ + 'Other great features for scholarly research', + 'Find out more information' + ] + } + }) + + def test_clicks(self): + pass + + + def check(self, data): + for path, info in data.items(): + self.driver.get(site_root + path) + if 'title' in info: + self.assertEqual(self.driver.title, info['title'], "Title for %s is not %s." % (path, info['title'])) + if 'strings' in info: + page = self.driver.page_source + for string in info['strings']: + self.assertTrue(string in page, "Couldn't find string %s in %s." % (string, path)) + + + +if __name__ == "__main__": + + site_root = os.environ.get('TEST_SITE_ROOT', 'https://dp.la') + username = os.environ.get('TEST_USERNAME', 'dpla1') + password = os.environ.get('TEST_PASSWORD') + + connection_string = "http://%s:%s@hub.browserstack.com:80/wd/hub" \ + % (username, password) + + browser_versions = [ + { + 'os': 'Windows', + 'os_version': '10', + 'browser': 'IE', + 'browser_version': '11.0', + 'browserstack.debug': 'true', + }, + { + 'os_version': '10.3', + 'device': 'iPhone 7', + 'real_mobile': 'true' + }, + { + 'os_version': '7.0', + 'device': 'Samsung Galaxy S8', + 'real_mobile': 'true' + }, + { + 'os': 'Windows', + 'os_version': '10', + 'browser': 'Edge', + 'browser_version': '16.0' + }, + { + 'os': 'Windows', + 'os_version': '10', + 'browser': 'Chrome', + 'browser_version': '64.0' + }, + { + 'os': 'Windows', + 'os_version': '10', + 'browser': 'Firefox', + 'browser_version': '57.0' + }, + { + 'os': 'OS X', + 'os_version': 'High Sierra', + 'browser': 'Safari', + 'browser_version': '11.0' + }, + { + 'os': 'OS X', + 'os_version': 'High Sierra', + 'browser': 'Chrome', + 'browser_version': '64.0' + }, + { + 'os': 'OS X', + 'os_version': 'High Sierra', + 'browser': 'Firefox', + 'browser_version': '56.0', + } + ] + + datetime = datetime.datetime.utcnow().isoformat() + label = "Hostname: %s | Username: %s | Datetime: %s" \ + % (site_root, username, datetime) + + generic_capabilities = { + 'build': label, + 'project': 'DPLA Frontend', + 'name': 'Website Smoke Test', + 'browserstack.debug' : 'true', + 'browserstack.selenium_version': '3.5.2' + } + + for browser_version in browser_versions: + print("Testing " + str(browser_version)) + try: + driver = webdriver.Remote( + command_executor=connection_string, + desired_capabilities={ + **generic_capabilities, + **browser_version + } + ) + if 'browser' not in browser_version \ + or browser_version['browser'] != 'Firefox': + driver.implicitly_wait(60) + ProductTestSuite.driver = driver + ProductTestSuite.root = site_root + unittest.main(exit=False) + except: + print("EXECUTION FAILED") + print(print(traceback.format_exc())) + finally: + driver.quit() \ No newline at end of file diff --git a/test/selenium/requirements.txt b/test/selenium/requirements.txt new file mode 100644 index 000000000..1a74862b5 --- /dev/null +++ b/test/selenium/requirements.txt @@ -0,0 +1 @@ +selenium==3.11.0 diff --git a/test/thumbp.js b/test/thumbp.js new file mode 100644 index 000000000..9a669730c --- /dev/null +++ b/test/thumbp.js @@ -0,0 +1,339 @@ +const assert = require("chai").assert; +const expect = require("expect.js"); +const sinon = require("sinon"); +const http = require("http"); +const libRequest = require("request"); +const thumbp = require("../utilFunctions/thumbp.js"); + +describe("Connection", function() { + var c, request, response, returnErrorStub, consoleErrorStub, libRequestStub; + + before(function() { + consoleErrorStub = sinon.stub(console, "error"); + libRequestStub = sinon.stub(libRequest, "Request"); + libRequestStub.on = function() { + return libRequestStub; + }; + libRequestStub.pipe = function() { + return true; + }; + }); + + beforeEach(function() { + request = { + method: "GET", + on: function() { + return request; + } + }; + response = { + setHeader: function() {} + }; + c = new thumbp.Connection(request, response); + returnErrorStub = sinon.stub(c, "returnError"); + consoleErrorStub.reset(); + libRequestStub.reset(); + }); + + describe("Connection()", function() { + it("can be instantiated", function() { + expect(new thumbp.Connection(request, response)).to.be.ok; + }); + + it("assigns request and response", function() { + var c = new thumbp.Connection(request, response); + expect(c.request).to.equal(request); + expect(c.response).to.equal(response); + }); + }); + + describe("handleReqEnd()", function() { + var lookUpImageStub; + + beforeEach(function() { + lookUpImageStub = sinon.stub(c, "lookUpImage"); + }); + + it("returns a 400 error if the URL path is no good", function() { + request.url = "/bad/path"; + c.handleReqEnd(); + assert(returnErrorStub.calledWith(400)); + }); + + it("returns a 400 error if given an invalid ID", function() { + request.url = "/thumb/1"; + c.handleReqEnd(); + assert(returnErrorStub.calledWith(400)); + }); + + it("proceeds if given a valid ID", function() { + itemID = "223ea5040640813b6c8204d1e0778d30"; + request.url = `/thumb/${itemID}`; + c.handleReqEnd(); + assert(lookUpImageStub.called); + expect(c.itemID).to.equal(itemID); + }); + + it("returns a 405 error if the method is not GET", function() { + request.method = "PUT"; + c.handleReqEnd(); + assert(returnErrorStub.calledWith(405)); + }); + }); + + // describe("lookUpImage()", function() { + // it("makes an Elasticsearch request with the correct URL", function() { + // var itemID = "223ea5040640813b6c8204d1e0778d30"; + // var goodURL = + // "http://localhost:9200/dpla_alias/item" + + // `/_search?q=id:${itemID}&fields=id,object`; + // c.itemID = itemID; + // c.lookUpImage(); + // expect(libRequestStub.args[0][0].uri).to.equal(goodURL); + // }); + // }); + + describe("checkSearchResponse()", function() { + var error, response, itemID, imageURL, okBody, proxyImageStub; + + beforeEach(function() { + error = false; + response = { + statusCode: 200 + }; + itemID = "f8f8a58a5a5ef34ffd4f8e399eaeb740"; + imageURL = "http://example.org/image1.jpg"; + okBody = JSON.stringify({ + hits: { + total: 1, + hits: [ + { + fields: { + id: itemID, + object: imageURL + } + } + ] + } + }); + proxyImageStub = sinon.stub(c, "proxyImage"); + }); + + it("calls proxyImage() with a successful query result", function() { + c.itemID = itemID; + c.checkSearchResponse(error, response, okBody); + assert(proxyImageStub.called); + expect(c.imageURL).to.equal(imageURL); + }); + + it("gives a 404 if the ID could not be found", function() { + var notFoundBody = JSON.stringify({ + hits: { + total: 0, + hits: [] + } + }); + c.itemID = itemID; + c.checkSearchResponse(error, response, notFoundBody); + assert(returnErrorStub.calledWith(404)); + }); + + it("gives a 500 and logs if there is a parsing problem", function() { + var badBody = JSON.stringify({}); + c.checkSearchResponse(error, response, badBody, itemID); + assert(returnErrorStub.calledWith(500)); + assert(consoleErrorStub.called); + }); + + it("gives a 500 and logs if there was a connection error", function() { + c.checkSearchResponse("the error", {}, {}, itemID); + assert(returnErrorStub.calledWith(500)); + assert(consoleErrorStub.called); + }); + + it('takes 1st element of "object" field that is an array', function() { + bodyWithObjArray = JSON.stringify({ + hits: { + total: 1, + hits: [ + { + fields: { + id: itemID, + object: [imageURL, "x"] + } + } + ] + } + }); + c.itemID = itemID; + c.checkSearchResponse(error, response, bodyWithObjArray); + expect(c.imageURL).to.equal(imageURL); + }); + + describe("... with no object property", function() { + var noObjectData, errMsg; + + beforeEach(function() { + noObjectData = { + hits: { + total: 1, + hits: [ + { + fields: { + id: itemID + } + } + ] + } + }; + errMsg = `empty / invalid object property for item ${itemID}`; + c.itemID = itemID; + }); + + it("returns 404 & logs if no object property", function() { + var noObjectBody = JSON.stringify(noObjectData); + c.checkSearchResponse(error, response, noObjectBody); + assert(returnErrorStub.calledWith(404)); + assert(consoleErrorStub.calledWith(errMsg)); + }); + + it("returns 404 & logs if object is empty / wrong", function() { + empties = [[], "", { x: "is wrong type" }]; + for (var i = 0; i < empties.length; i++) { + var obj = empties[i]; + noObjectData["hits"]["hits"][0]["fields"]["object"] = obj; + var noObjectBody = JSON.stringify(noObjectData); + c.checkSearchResponse(error, response, noObjectBody); + assert(returnErrorStub.calledWith(404)); + assert(consoleErrorStub.calledWith(errMsg)); + consoleErrorStub.reset(); + } + }); + }); + }); // checkSearchResponse + + describe("proxyImage()", function() { + it("issues the request with the correct options", function() { + var url = "http://example.org/image1.jpg"; + var ua = ""; + c.imageURL = url; + c.proxyImage(); + var opts = libRequestStub.args[0][0]; + + expect(opts["uri"]).to.equal(url); + expect(opts["method"]).to.equal("GET"); + expect(opts["timeout"]).to.equal(10000); // currently hardcoded + expect(opts["headers"]["User-Agent"]).to.be.a("string"); + }); + }); + + describe("handleImageResponse() with success status", function() { + var imgResponse; + + beforeEach(function() { + imgResponse = { + headers: { + date: "Tue, 19 Apr 2016 18:58:52 GMT", + "content-type": "image/jpeg", + "content-length": "4833", + server: "Apache", + connection: "keep-alive", + "last-modified": "Mon, 18 Apr 2016 12:00:00 GMT" + }, + statusCode: 200 + }; + }); + + it("cleans up the response headers", function() { + var goodKeys = [ + "Content-Length", + "Content-Type", + "Date", + "Last-Modified" + ]; + c.handleImageResponse(imgResponse); + expect(imgResponse.headers).to.only.have.keys(goodKeys); + }); + + it("leaves a 200 response code alone", function() { + c.handleImageResponse(imgResponse); + expect(imgResponse.statusCode).to.equal(200); + }); + }); // handleImageResponse with success + + describe("handleImageResponse() with error status", function() { + var imgResponse; + + beforeEach(function() { + imgResponse = { + headers: { + date: "Tue, 19 Apr 2016 18:58:52 GMT", + server: "Apache" + }, + statusCode: 404 + }; + }); + + it("leaves Content-Length out if it is not given", function() { + c.handleImageResponse(imgResponse); + expect(imgResponse.headers["Content-Length"]).to.be.undefined; + }); + + it("leaves Content-Type out if it is not given", function() { + c.handleImageResponse(imgResponse); + expect(imgResponse.headers["Content-Type"]).to.be.undefined; + }); + + it("logs 404s for images", function() { + c.imageURL = "http://example.org/bad.jpg"; + var msg = `${c.imageURL} not found`; + c.handleImageResponse(imgResponse); + assert(consoleErrorStub.calledWith(msg)); + }); + + it("leaves a 404 response code alone", function() { + c.handleImageResponse(imgResponse); + expect(imgResponse.statusCode).to.equal(404); + }); + + it("changes a 410 to a 404", function() { + imgResponse.statusCode = 410; + c.handleImageResponse(imgResponse); + expect(imgResponse.statusCode).to.equal(404); + }); + + it("turns other errors into 502s", function() { + imgResponse.statusCode = 503; + c.handleImageResponse(imgResponse); + expect(imgResponse.statusCode).to.equal(502); + }); + }); // handleImageResponse() with 4xx status + + describe("handleImageConnectionError()", function() { + var error; + + beforeEach(function() { + c.imageURL = "http://example.org/img.jpg"; + error = { + code: "ECONNREFUSED" + }; + }); + + it("logs errors", function() { + c.handleImageConnectionError(error); + msg = "Error (ECONNREFUSED) for http://example.org/img.jpg"; + assert(consoleErrorStub.calledWith(msg)); + }); + + it("returns a 504 Gateway Timeout for a timeout", function() { + error.code = "ETIMEDOUT"; + c.handleImageConnectionError(error); + assert(returnErrorStub.calledWith(504)); + }); + + it("returns a 502 Bad Gateway for any other error", function() { + c.handleImageConnectionError(error); + assert(returnErrorStub.calledWith(502)); + }); + }); +});