Repository: bfirsh/needle Branch: master Commit: c2d28ee07278 Files: 32 Total size: 68.9 KB Directory structure: gitextract_ixilxv5m/ ├── .gitignore ├── .travis.yml ├── CHANGES.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs/ │ ├── Makefile │ ├── conf.py │ ├── index.txt │ ├── make.bat │ └── requirements.txt ├── needle/ │ ├── __init__.py │ ├── cases.py │ ├── driver.py │ ├── engines/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── imagemagick_engine.py │ │ ├── perceptualdiff_engine.py │ │ └── pil_engine.py │ ├── js/ │ │ └── __init__.py │ └── plugin.py ├── setup.py ├── tests/ │ ├── __init__.py │ ├── plugin_test_cases/ │ │ └── red_box.py │ ├── test_diff.py │ ├── test_driver.py │ ├── test_imagemagick_engine.py │ ├── test_in_memory.py │ ├── test_perceptualdiff_engine.py │ ├── test_pil_engine.py │ └── test_plugin.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.pyc *.egg-info .DS_Store *~ .*.swo .*.swp .*.swn .*.swm /.tox/ /build/ /dist/ selenium-server-standalone-*.jar /docs/_build tests/screenshots/*.png geckodriver.log ghostdriver.log __pycache__ ================================================ FILE: .travis.yml ================================================ language: python sudo: required dist: trusty python: - "2.7" - "3.2" - "3.3" - "3.4" - "3.5" - "3.6" # Firefox 48+ only works with geckodriver, first supported properly in selenium 3.0.0.b3 # See https://github.com/SeleniumHQ/selenium/issues/2739#issuecomment-249482533 env: - SELENIUM_VERSION='<3' NEEDLE_BROWSER=chrome - SELENIUM_VERSION='<4' NEEDLE_BROWSER=chrome - SELENIUM_VERSION='<4' NEEDLE_BROWSER=firefox - SELENIUM_VERSION='<3' NEEDLE_BROWSER=phantomjs - SELENIUM_VERSION='<4' NEEDLE_BROWSER=phantomjs before_install: - if [[ "$NEEDLE_BROWSER" != "phantomjs" ]]; then export DISPLAY=:99.0; fi - if [[ "$NEEDLE_BROWSER" != "phantomjs" ]]; then sh -e /etc/init.d/xvfb start; fi - if [[ "$NEEDLE_BROWSER" != "phantomjs" ]]; then sleep 3; fi # give xvfb some time to start - if [[ "$NEEDLE_BROWSER" == "firefox" ]]; then wget https://github.com/mozilla/geckodriver/releases/download/v0.15.0/geckodriver-v0.15.0-linux64.tar.gz; fi - if [[ "$NEEDLE_BROWSER" == "firefox" ]]; then mkdir geckodriver; fi - if [[ "$NEEDLE_BROWSER" == "firefox" ]]; then tar -xzf geckodriver-v0.15.0-linux64.tar.gz -C geckodriver; fi - if [[ "$NEEDLE_BROWSER" == "firefox" ]]; then export PATH=$PATH:$PWD/geckodriver; fi - sudo apt-get update -qq - sudo apt-get install -y perceptualdiff imagemagick - if [[ "$NEEDLE_BROWSER" == "chrome" ]]; then sudo apt-get install -y chromium-chromedriver; fi - if [[ "$NEEDLE_BROWSER" == "chrome" ]]; then export PATH=$PATH:/usr/lib/chromium-browser/; fi addons: firefox: '52.0' install: - pip install "selenium ${SELENIUM_VERSION}" - pip install -e . script: - nosetests -v ================================================ FILE: CHANGES.md ================================================ Change log ========== Upcoming 0.5.0 -------------- - Dropped Python 2.6 support. - Verified support for Python 3.2-3.6. - Firefox improvements: use FirefoxWebElement and geckodriver by default. 0.4.1 (2017-01-04) ------------------ - Fixed a race condition in creating baseline and output directories. 0.4.0 (2016-12-21) ------------------ - Added support for Selenium 3. - Added ImageMagick engine. - Fixed an issue with false positives yielded by the Perceptual engine. 0.3.0 (2015-02-22) ------------------ - Added the cleanup_on_success option for deleting screenshots after successful test runs. 0.2.4 (2015-02-08) ------------------ - Use selenium native element location instead of javascript call in order to assure crossplatform compatability. 0.2.3 (2015-01-17) ------------------ - Fixed a unicode error (issue #23). 0.2.2 (2014-08-16) ------------------ - Fixed an issue with saving screenshots on Windows. 0.2.1 (2014-04-10) ------------------ - Fixed a regression in the PIL engine. 0.2.0 (2014-04-07) ------------------ - Added support for Python 3. - Changed default threshold from 0.1 to 0. - Added configurable way of plugging external diff engines like PerceptualDiff. - Removed the necessity to run the Selenium server by using a Firefox web driver instance by default. This is slightly backwards-incompatible if you relied on the now-removed `driver_command_executor`, `driver_desired_capabilities` and `driver_browser_profile` attributes. To control the logic for selecting the proper web driver, you may simply override the `get_web_driver()` method. - The `--with-needle-capture` and `NeedleTestCase.capture` options were deprecated and will be removed in version 0.4.0. Instead, you should now respectively use the new, more explicit `--with-save-baseline` and `NeedleTestCase.save_baseline` options. Note that those new options will systematically cause the baseline image files to be saved on disk, overwriting potentially existing baseline files. - Removed the `NeedleWebElement.get_computed_property()` method. Instead, you may use Selenium's built-in `value_of_css_property()` method. - Upgraded vendored jQuery to version 11.0. 0.1.0 (2014-02-20) ------------------ - Add `set_viewport_size()` method to `NeedleTestCase` - Calculate the dimensions of elements more accurately with jQuery - Only load jQuery if it hasn't already been loaded Thanks @jphalip! 0.0.2 (2013-10-24) ------------------ - Allow needle to be used with custom web driver - Replace PIL with pillow Thanks @treyhunner! 0.0.1 (2013-05-07) ------------------ Initial release. ================================================ FILE: LICENSE ================================================ Copyright (c) 2011, Ben Firshman All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of its contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: MANIFEST.in ================================================ include LICENSE include *.md recursive-include needle/js * ================================================ FILE: README.md ================================================ Needle ====== [![Build Status](https://travis-ci.org/python-needle/needle.png?branch=master)](https://travis-ci.org/python-needle/needle) Needle is a tool for testing visuals with [Selenium](http://seleniumhq.org/) and [nose](https://nose.readthedocs.io/). It checks that visuals (CSS/fonts/images/SVG/etc.) render correctly by taking screenshots of portions of a website and comparing them against known good screenshots. It also provides tools for testing calculated CSS values and the position of HTML elements. Example ------- This is what a Needle test case looks like: ```python from needle.cases import NeedleTestCase class BBCNewsTest(NeedleTestCase): def test_masthead(self): self.driver.get('http://www.bbc.co.uk/news/') self.assertScreenshot('#blq-mast', 'bbc-masthead') ``` This example checks for regressions in the appearance of the BBC's masthead. Documentation ------------- Full documentation available on [Read the Docs](https://needle.readthedocs.io/). If you'd like to build the documentation yourself, first install ``sphinx``: pip install sphinx Then run: cd docs make html The documentation will then be available browsable from ``docs/_build/index.html``. Running Needle's test suite --------------------------- First install tox (usually via ``pip install tox``). Then: $ tox ================================================ FILE: docs/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Needle.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Needle.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Needle" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Needle" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ================================================ FILE: docs/conf.py ================================================ # -*- coding: utf-8 -*- # # Needle documentation build configuration file, created by # sphinx-quickstart on Tue Apr 5 19:53:10 2011. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.txt' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Needle' copyright = u'2011, Ben Firshman' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.1a1' # The full version, including alpha/beta/rc tags. release = '0.1a1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'nature' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Needledoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Needle.tex', u'Needle Documentation', u'Ben Firshman', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'needle', u'Needle Documentation', [u'Ben Firshman'], 1) ] ================================================ FILE: docs/index.txt ================================================ Needle: Automated tests for your visuals ======================================== Needle is a tool for testing your CSS and visuals with `Selenium `_ and `nose `_. It checks that visuals (CSS/fonts/images/SVG/etc.) render correctly by taking screenshots of portions of a website and comparing them against known good screenshots. It also provides tools for testing calculated CSS values and the position of HTML elements. Installation ------------ If you haven't got `pip `_ installed:: $ sudo easy_install pip As root, or in a `virtualenv `_:: $ pip install selenium $ pip install needle Getting started --------------- Create ``test_bbc.py`` in an empty directory:: from needle.cases import NeedleTestCase class BBCNewsTest(NeedleTestCase): def test_masthead(self): self.driver.get('http://www.bbc.co.uk/news/') self.assertScreenshot('#blq-mast', 'bbc-masthead') This is a test case which tells the Selenium web driver (by default Firefox) to open BBC News and check the bar across the top of the page looks correct. :py:meth:`~needle.cases.NeedleTestCase.assertScreenshot` take two arguments: a CSS selector for the element we are capturing and a filename for the image. To create an initial screenshot of the logo, we need to run Needle in 'baseline saving' mode:: $ nosetests test_bbc.py --with-save-baseline This will create ``screenshots/baseline/bbc-masthead.png``. Open it up and check it looks okay. Now if we run our tests, it will take the same screenshot and check it against the screenshot on disk:: $ nosetests test_bbc.py If a regression in your CSS causes them to become significantly different, the test will fail. Selecting a WebDriver --------------------- You may control which browser is used by Needle by overriding the ``get_web_driver()`` method:: from needle.cases import NeedleTestCase from needle.driver import NeedlePhantomJS class MyTests(NeedleTestCase): @classmethod def get_web_driver(cls): return NeedlePhantomJS() def test_something(self): ... By default Needle uses ``NeedleFirefox``, which is a wrapper of Selenium's built-in ``selenium.webdriver.firefox.webdriver.WebDriver`` class. You may use any of the following WebDrivers: ``NeedleRemote``, ``NeedlePhantomJS``, ``NeedleFirefox``, ``NeedleChrome``, ``NeedleIe``, ``NeedleOpera`` and ``NeedleSafari``. Refer to Selenium's documentation to learn how to install and configure any of those WebDrivers. Setting the viewport's size --------------------------- You may set the size of the browser's viewport using the ``set_viewport_size()`` method:: from needle.cases import NeedleTestCase class MyTests(NeedleTestCase): def test_something(self): self.set_viewport_size(width=1024, height=768) ... This is particularly useful to predict the size of the resulting screenshots when taking fullscreen captures, or to test responsive sites. You may also set the default viewport size for all your tests with the ``viewport_width`` and ``viewport_height`` class attributes:: from needle.cases import NeedleTestCase class MyTests(NeedleTestCase): viewport_width = 1024 viewport_height = 768 ... Engines ------- By default Needle uses the PIL engine (``needle.engines.pil_engine.Engine``) to take screenshots. Instead of PIL, you may also use PerceptualDiff or ImageMagick. Example with PerceptualDiff:: from needle.cases import NeedleTestCase class MyTests(NeedleTestCase): engine_class = 'needle.engines.perceptualdiff_engine.Engine' def test_something(self): ... Example with ImageMagick:: from needle.cases import NeedleTestCase class MyTests(NeedleTestCase): engine_class = 'needle.engines.imagemagick_engine.Engine' def test_something(self): ... Besides being much faster than PIL, PerceptualDiff and ImageMagick also generate a diff PNG file when a test fails, highlighting the differences between the baseline image and the new screenshot. Note that to use the PerceptualDiff engine you will first need to `download `_ the ``perceptualdiff`` binary and place it in your ``PATH``. To use the ImageMagick engine you will need to install a package on your machine (e.g. ``sudo apt-get install imagemagick`` on Ubuntu or ``brew install imagemagick`` on OSX). File cleanup ------------ Each time you run tests, Needle will create new screenshot images on disk, for comparison with the baseline screenshots. It's then up to you whether you want to delete them or archive them. Set the ``cleanup_on_success`` class attribute to ``True`` to delete these files for all successful tests. Any screenshots that differ from the baseline will remain on disk for your inspection. Example:: from needle.cases import NeedleTestCase class MyTests(NeedleTestCase): cleanup_on_success = True def test_something(self): ... By default, ``cleanup_on_success`` is ``False``. You may also activate the file cleanup from the command line by using the ``--with-needle-cleanup-on-success`` nose plugin: $ nosetests --with-needle-cleanup-on-success Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` ================================================ FILE: docs/make.bat ================================================ @ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Needle.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Needle.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end ================================================ FILE: docs/requirements.txt ================================================ # Used by ReadTheDocs for documentation builds Sphinx==1.5.3 ================================================ FILE: needle/__init__.py ================================================ __version__ = '0.5.0' __author__ = 'Ben Firshman' __contact__ = 'ben@firshman.co.uk' __homepage__ = 'https://github.com/python-needle/needle' ================================================ FILE: needle/cases.py ================================================ # encoding: utf-8 from __future__ import absolute_import from __future__ import print_function from warnings import warn from contextlib import contextmanager from errno import EEXIST import os import sys import time if sys.version_info > (2, 7): from unittest import TestCase else: from unittest2 import TestCase if sys.version_info >= (3, 0): basestring = str from PIL import Image from selenium.common.exceptions import WebDriverException from needle.engines.pil_engine import ImageDiff from needle.driver import (NeedleFirefox, NeedleChrome, NeedleIe, NeedleOpera, NeedleSafari, NeedlePhantomJS, NeedleWebElementMixin) DRIVER_ACQUISITION_TIMEOUT = 5 # seconds def _object_filename(obj): return os.path.abspath(sys.modules[type(obj).__module__].__file__) def import_from_string(path): """ Utility function to dynamically load a class specified by a string, e.g. 'path.to.my.Class'. """ module_name, klass = path.rsplit('.', 1) module = __import__(module_name, fromlist=[klass]) return getattr(module, klass) class NeedleTestCase(TestCase): """ A `unittest2 `_ test case which provides tools for testing CSS with Selenium. """ driver = None capture = False # Deprecated save_baseline = False cleanup_on_success = False viewport_width = 1024 viewport_height = 768 output_directory = None baseline_directory = None engine_class = 'needle.engines.pil_engine.Engine' @classmethod def setUpClass(cls): if os.environ.get('NEEDLE_CAPTURE'): cls.capture = True if os.environ.get('NEEDLE_SAVE_BASELINE'): cls.save_baseline = True if os.environ.get('NEEDLE_CLEANUP_ON_SUCCESS'): cls.cleanup_on_success = True # Instantiate the diff engine klass = import_from_string(cls.engine_class) cls.engine = klass() cls.driver = cls.get_web_driver() cls.driver.set_window_position(0, 0) cls.set_viewport_size(cls.viewport_width, cls.viewport_height) super(NeedleTestCase, cls).setUpClass() @classmethod def tearDownClass(cls): if isinstance(cls.driver, NeedlePhantomJS): # Workaround for https://github.com/SeleniumHQ/selenium/issues/767 cls.driver.service.send_remote_shutdown_command() cls.driver.service._cookie_temp_file = None cls.driver.quit() super(NeedleTestCase, cls).tearDownClass() @classmethod def get_web_driver(cls): """ Returns the WebDriver instance to be used. Defaults to `NeedleFirefox()`. Override this method if you'd like to control the logic for choosing the proper WebDriver instance. """ browser_name = os.environ.get('NEEDLE_BROWSER') browser_map = { 'firefox': NeedleFirefox, 'chrome': NeedleChrome, 'ie': NeedleIe, 'opera': NeedleOpera, 'safari': NeedleSafari, 'phantomjs': NeedlePhantomJS, } browser_class = browser_map.get(browser_name, NeedleFirefox) # Allow a few retries to get the driver, in case it isn't quite ready yet start_time = time.time() while True: try: browser = browser_class() break except Exception as e: if (not isinstance(e, WebDriverException)) and e.__class__.__name__ != 'WebDriverException': # nose likes to change selenium's WebDriverException to "nose.proxy.WebDriverException" raise if time.time() - start_time >= DRIVER_ACQUISITION_TIMEOUT: raise time.sleep(1) return browser def __init__(self, *args, **kwargs): super(NeedleTestCase, self).__init__(*args, **kwargs) # TODO: should output directory be timestamped? if self.output_directory is None: self.output_directory = os.environ.get('NEEDLE_OUTPUT_DIR', os.path.realpath(os.path.join(os.getcwd(), 'screenshots'))) # TODO: Should baseline be a top-level peer to output_directory? if self.baseline_directory is None: self.baseline_directory = os.environ.get('NEEDLE_BASELINE_DIR', os.path.realpath(os.path.join(os.getcwd(), 'screenshots', 'baseline'))) # Create the output and baseline directories if they do not yet exist. for dirname in (self.baseline_directory, self.output_directory): # Recursively create the directory, handling its # prior existence as a valid exception. # This will guard against race conditions. # E.g. when running tests in multithreaded mode # they likely have the same directories specified # and might encounter this block at the same time. try: os.makedirs(dirname) except OSError as err: if err.errno == EEXIST and os.path.isdir(dirname): pass else: raise @classmethod def set_viewport_size(cls, width, height): cls.driver.set_window_size(width, height) # Measure the difference between the actual document width and the # desired viewport width so we can account for scrollbars: measured = cls.driver.execute_script("return {width: document.body.clientWidth, height: document.body.clientHeight};") delta = width - measured['width'] cls.driver.set_window_size(width + delta, height) def assertScreenshot(self, element_or_selector, file, threshold=0): """assert-style variant of compareScreenshot context manager compareScreenshot() can be considerably more efficient for recording baselines by avoiding the need to load pages before checking whether we're actually going to save them. This function allows you to continue using normal unittest-style assertions if you don't need the efficiency benefits """ with self.compareScreenshot(element_or_selector, file, threshold=threshold): pass @contextmanager def compareScreenshot(self, element_or_selector, file, threshold=0): """ Assert that a screenshot of an element is the same as a screenshot on disk, within a given threshold. :param element_or_selector: Either a CSS selector as a string or a :py:class:`~needle.driver.NeedleWebElementMixin` object that represents the element to capture. :param file: If a string, then assumed to be the filename for the screenshot, which will be appended with ``.png``. Otherwise assumed to be a file object for the baseline image. :param threshold: The threshold for triggering a test failure. """ yield # To allow using this method as a context manager if not isinstance(element_or_selector, NeedleWebElementMixin): element = self.driver.find_element_by_css_selector(element_or_selector) else: element = element_or_selector if not isinstance(file, basestring): # Comparing in-memory files instead of on-disk files baseline_image = Image.open(file).convert('RGB') fresh_screenshot = element.get_screenshot() diff = ImageDiff(fresh_screenshot, baseline_image) distance = abs(diff.get_distance()) if distance > threshold: raise AssertionError("The new screenshot did not match " "the baseline (by a distance of %.2f)" % distance) else: baseline_file = os.path.join(self.baseline_directory, '%s.png' % file) output_file = os.path.join(self.output_directory, '%s.png' % file) # Determine whether we should save the baseline image save_baseline = False if self.save_baseline: save_baseline = True elif self.capture: warn("The 'NeedleTestCase.capture' attribute and '--with-save-baseline' nose option " "are deprecated since version 0.2.0. Use 'save_baseline' and '--with-save-baseline' " "instead. See the changelog for more information.", PendingDeprecationWarning) if os.path.exists(baseline_file): self.skipTest('Not capturing %s, its baseline image already exists. If you ' 'want to capture this element again, delete %s' % (file, baseline_file)) else: save_baseline = True if save_baseline: # Save the baseline screenshot and bail out element.get_screenshot().save(baseline_file) return else: if not os.path.exists(baseline_file): raise IOError('The baseline screenshot %s does not exist. ' 'You might want to re-run this test in baseline-saving mode.' % baseline_file) # Save the new screenshot element.get_screenshot().save(output_file) try: self.engine.assertSameFiles(output_file, baseline_file, threshold) except: raise else: if self.cleanup_on_success: os.remove(output_file) ================================================ FILE: needle/driver.py ================================================ # encoding: utf-8 from __future__ import absolute_import import base64 import os import sys if sys.version_info >= (3, 0): from urllib.parse import quote from io import BytesIO as IOClass else: from urllib import quote try: from cStringIO import StringIO as IOClass except ImportError: from StringIO import StringIO as IOClass from PIL import Image from selenium.common.exceptions import WebDriverException from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.firefox.webdriver import WebDriver as Firefox from selenium.webdriver.chrome.webdriver import WebDriver as Chrome from selenium.webdriver.ie.webdriver import WebDriver as Ie from selenium.webdriver.opera.webdriver import WebDriver as Opera from selenium.webdriver.safari.webdriver import WebDriver as Safari from selenium.webdriver.phantomjs.webdriver import WebDriver as PhantomJS from selenium.webdriver.remote.webdriver import WebDriver as Remote try: # Added in selenium 3.0.0.b3 from selenium.webdriver.firefox.webelement import FirefoxWebElement except ImportError: from selenium.webdriver.remote.webelement import WebElement as FirefoxWebElement class NeedleWebElementMixin(object): """ An element on a page that Selenium has opened. It is a Selenium :py:class:`~selenium.webdriver.remote.webelement.WebElement` object with some extra methods for testing CSS. """ def get_dimensions(self): """ Returns a dictionary containing, in pixels, the element's ``width`` and ``height``, and it's ``left`` and ``top`` position relative to the document. """ location = self.location size = self.size return { "top": location['y'], "left": location['x'], "width": size['width'], "height": size['height'] } def get_screenshot(self): """ Returns a screenshot of this element as a PIL image. """ d = self.get_dimensions() # Cast values to int in order for _ImageCrop not to break d['left'] = int(d['left']) d['top'] = int(d['top']) d['width'] = int(d['width']) d['height'] = int(d['height']) try: # For selenium >= 2.46.1, W3C WebDriver spec drivers (like geckodriver) fh = IOClass(self.screenshot_as_png) image = Image.open(fh).convert('RGB') # Make sure it isn't actually a full-page screenshot (PhantomJS) if image.size == (d['width'], d['height']): return image except (AttributeError, WebDriverException): # Fall back to cropping a full page screenshot image = self._parent.get_screenshot_as_image() return image.crop(( d['left'], d['top'], d['left'] + d['width'], d['top'] + d['height'], )) class NeedleWebDriverMixin(object): """ Selenium WebDriver mixin with some extra methods for testing CSS. """ def load_html(self, html): """ Similar to :py:meth:`get`, but instead of passing a URL to load in the browser, the HTML for the page is provided. """ self.get('data:text/html,' + quote(html)) def get_screenshot_as_image(self): """ Returns a screenshot of the current page as an RGB `PIL image `_. """ fh = IOClass(base64.b64decode(self.get_screenshot_as_base64().encode('ascii'))) return Image.open(fh).convert('RGB') def load_jquery(self): """ Loads jQuery onto the current page so calls to :py:meth:`execute_script` have access to it. """ if (self.execute_script('return typeof(jQuery)') == 'undefined'): self.execute_script(open( os.path.join(self._get_js_path(), 'jquery-1.11.0.min.js') ).read() + '\nreturn "";') def _get_js_path(self): return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'js') def create_web_element(self, element_id, *args, **kwargs): if isinstance(self, NeedleFirefox): return NeedleFirefoxWebElement(self, element_id, w3c=self.w3c, *args, **kwargs) else: return NeedleWebElement(self, element_id, w3c=self.w3c, *args, **kwargs) class NeedleRemote(NeedleWebDriverMixin, Remote): """ The same as Selenium's remote WebDriver, but with NeedleWebDriverMixin's functionality. """ class NeedlePhantomJS(NeedleWebDriverMixin, PhantomJS): """ The same as Selenium's PhantomJS WebDriver, but with NeedleWebDriverMixin's functionality. """ class NeedleFirefox(NeedleWebDriverMixin, Firefox): """ The same as Selenium's Firefox WebDriver, but with NeedleWebDriverMixin's functionality. """ class NeedleChrome(NeedleWebDriverMixin, Chrome): """ The same as Selenium's Chrome WebDriver, but with NeedleWebDriverMixin's functionality. """ class NeedleIe(NeedleWebDriverMixin, Ie): """ The same as Selenium's Internet Explorer WebDriver, but with NeedleWebDriverMixin's functionality. """ class NeedleOpera(NeedleWebDriverMixin, Opera): """ The same as Selenium's Opera WebDriver, but with NeedleWebDriverMixin's functionality. """ class NeedleSafari(NeedleWebDriverMixin, Safari): """ The same as Selenium's Safari WebDriver, but with NeedleWebDriverMixin's functionality. """ class NeedleWebElement(NeedleWebElementMixin, WebElement): """ The same as Selenium's WebElement, but with NeedleWebElementMixin's functionality. """ class NeedleFirefoxWebElement(NeedleWebElementMixin, FirefoxWebElement): """ The same as Selenium's FirefoxWebElement, but with NeedleWebElementMixin's functionality. """ ================================================ FILE: needle/engines/__init__.py ================================================ ================================================ FILE: needle/engines/base.py ================================================ class EngineBase(object): """ Base class for diff engines. """ def assertSameFiles(self, output_file, baseline_file, threshold): raise NotImplementedError ================================================ FILE: needle/engines/imagemagick_engine.py ================================================ import os import subprocess from needle.engines.base import EngineBase class Engine(EngineBase): compare_path = "compare" compare_command = ("{compare} -metric RMSE -subimage-search -dissimilarity-threshold 1.0 {baseline} " "{new} {diff}") def assertSameFiles(self, output_file, baseline_file, threshold=0): diff_file = output_file.replace('.png', '.diff.png') compare_cmd = self.compare_command.format( compare=self.compare_path, baseline=baseline_file, new=output_file, diff=diff_file) process = subprocess.Popen(compare_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) compare_stdout, compare_stderr = process.communicate() difference = float(compare_stderr.split()[1][1:-1]) if difference <= threshold: os.remove(diff_file) return raise AssertionError("The new screenshot '{new}' did not match " "the baseline '{baseline}' (See {diff}):\n" "{stdout}{stderr}" .format(new=output_file, baseline=baseline_file, diff=diff_file, stdout=compare_stdout, stderr=compare_stderr)) ================================================ FILE: needle/engines/perceptualdiff_engine.py ================================================ import subprocess import os from PIL import Image from needle.engines.base import EngineBase class Engine(EngineBase): perceptualdiff_path = 'perceptualdiff' perceptualdiff_output_png = True def assertSameFiles(self, output_file, baseline_file, threshold): # Calculate threshold value as a pixel number instead of percentage. width, height = Image.open(output_file).size threshold = int(width * height * threshold) diff_ppm = output_file.replace(".png", ".diff.ppm") cmd = "%s -threshold %d -output %s %s %s" % ( self.perceptualdiff_path, threshold, diff_ppm, baseline_file, output_file) process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) perceptualdiff_stdout, _ = process.communicate() # Sometimes perceptualdiff returns a false positive with this exact message: # 'FAIL: Images are visibly different\n0 pixels are different\n\n' # We catch that here. if process.returncode == 0 or b'\n0 pixels are different' in perceptualdiff_stdout: # No differences found, but make sure to clean up the .ppm in case it was created. if os.path.exists(diff_ppm): os.remove(diff_ppm) return else: if os.path.exists(diff_ppm): if self.perceptualdiff_output_png: # Convert the .ppm output to .png diff_png = diff_ppm.replace("diff.ppm", "diff.png") Image.open(diff_ppm).save(diff_png) os.remove(diff_ppm) diff_file_msg = ' (See %s)' % diff_png else: diff_file_msg = ' (See %s)' % diff_ppm else: diff_file_msg = '' raise AssertionError("The new screenshot '%s' did not match " "the baseline '%s'%s:\n%s" % (output_file, baseline_file, diff_file_msg, perceptualdiff_stdout)) ================================================ FILE: needle/engines/pil_engine.py ================================================ import sys from itertools import chain import math if sys.version_info >= (3, 0): izip = zip else: from itertools import izip from PIL import Image from needle.engines.base import EngineBase class Engine(EngineBase): def assertSameFiles(self, output_file, baseline_file, threshold): output_image = Image.open(output_file).convert('RGB') baseline_image = Image.open(baseline_file).convert('RGB') diff = ImageDiff(output_image, baseline_image) distance = abs(diff.get_distance()) if distance > threshold: raise AssertionError("The new screenshot '%s' did not match " "the baseline '%s' (by a distance of %.2f)" % (output_file, baseline_file, distance)) class ImageDiff(object): """ Utility class for performing image comparisons using PIL. """ def __init__(self, image_a, image_b): assert image_a.size == image_b.size assert image_a.getbands() == image_b.getbands() self.image_a = image_a self.image_b = image_b def get_nrmsd(self): """ Returns the normalised root mean squared deviation of the two images. """ a_values = chain(*self.image_a.getdata()) b_values = chain(*self.image_b.getdata()) rmsd = 0 for a, b in izip(a_values, b_values): rmsd += (a - b) ** 2 rmsd = math.sqrt(float(rmsd) / ( self.image_a.size[0] * self.image_a.size[1] * len(self.image_a.getbands()) )) return rmsd / 255 def get_distance(self): """ Returns the distance between the two images in pixels. """ a_values = chain(*self.image_a.getdata()) b_values = chain(*self.image_b.getdata()) band_len = len(self.image_a.getbands()) distance = 0 for a, b in izip(a_values, b_values): distance += abs(float(a) / band_len - float(b) / band_len) / 255 return distance ================================================ FILE: needle/js/__init__.py ================================================ ================================================ FILE: needle/plugin.py ================================================ from nose.plugins import Plugin class NeedleCapturePlugin(Plugin): """ A nose plugin which causes all calls to ``NeedleTestCase.assertScreenshot`` to save a baseline screenshot to disk, unless the baseline file already exists. """ name = 'needle-capture' def wantClass(self, cls): # Only gather classes which are a needle test case return hasattr(cls, 'assertScreenshot') def wantFunction(self, f): return False def beforeTest(self, test): if hasattr(test, 'test'): test.test.capture = True class SaveBaselinePlugin(Plugin): """ A nose plugin which causes all calls to ``NeedleTestCase.assertScreenshot`` to save the baseline screenshot to disk. """ name = 'save-baseline' def add_options(self, parser, env=None): super(SaveBaselinePlugin, self).add_options(parser, env) def wantClass(self, cls): # Only gather classes which are a needle test case return hasattr(cls, 'assertScreenshot') def wantFunction(self, f): return False def beforeTest(self, test): if hasattr(test, 'test'): test.test.save_baseline = True class CleanUpOnSuccessPlugin(Plugin): """ A nose plugin that causes all successful ``NeedleTestCase.assertScreenshot`` calls to delete the non-baseline file from disk. """ name = 'needle-cleanup-on-success' def add_options(self, parser, env=None): super(CleanUpOnSuccessPlugin, self).add_options(parser, env) def wantClass(self, cls): # Only gather classes which are a needle test case return hasattr(cls, 'assertScreenshot') def wantFunction(self, f): return False def beforeTest(self, test): if hasattr(test, 'test'): test.test.cleanup_on_success = True ================================================ FILE: setup.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- from setuptools import setup, find_packages import sys import os import codecs from needle import __version__ # Borrowed from # https://github.com/jezdez/django_compressor/blob/develop/setup.py def read(*parts): return codecs.open(os.path.join(os.path.dirname(__file__), *parts)).read() install_requires = [ 'nose>=1.0.0', 'selenium>=2,<4', 'pillow', ] if sys.version_info < (2, 7, 0): # Install backport of unittest2 only if needed. install_requires.append('unittest2>=0.5.1') setup( name='needle', version=__version__, description='Automated testing for your CSS.', author='Ben Firshman', author_email='ben@firshman.co.uk', url='https://github.com/python-needle/needle', packages=find_packages(exclude=['scripts', 'tests']), package_data={'needle': ['js/*']}, test_suite='nose.collector', entry_points = { 'nose.plugins.0.10': [ 'needle-capture = needle.plugin:NeedleCapturePlugin', 'save-baseline = needle.plugin:SaveBaselinePlugin', 'needle-cleanup-on-success = needle.plugin:CleanUpOnSuccessPlugin', ] }, install_requires=install_requires, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Browsers', 'Topic :: Multimedia :: Graphics :: Capture :: Screen Capture', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Testing', ], ) ================================================ FILE: tests/__init__.py ================================================ import sys from PIL import Image, ImageDraw if sys.version_info >= (3, 0): from io import BytesIO as IOClass else: try: from cStringIO import StringIO as IOClass except ImportError: from StringIO import StringIO as IOClass class ImageTestCaseMixin(object): def get_image(self, colour): return Image.new('RGB', (100, 100), colour) def get_black_image(self): return self.get_image((0, 0, 0)) def get_white_image(self): return self.get_image((255, 255, 255)) def get_half_filled_image(self): im = self.get_black_image() draw = ImageDraw.Draw(im) draw.rectangle( ((0, 0), (49, 100)), fill=(255, 255, 255) ) return im def load_black_div(self, text=''): self.driver.load_html('''
%s
''' % text) def save_image_to_fh(self, im): fh = IOClass() im.save(fh, 'PNG') fh.seek(0) return fh ================================================ FILE: tests/plugin_test_cases/red_box.py ================================================ from needle.cases import NeedleTestCase from tests import ImageTestCaseMixin class RedBoxTestCase(ImageTestCaseMixin, NeedleTestCase): def test_red_box(self): self.driver.load_html("""
""") self.assertScreenshot(self.driver.find_element_by_id('test'), 'red_box') ================================================ FILE: tests/test_diff.py ================================================ import math import sys if sys.version_info > (2, 7): from unittest import TestCase else: from unittest2 import TestCase from needle.engines.pil_engine import ImageDiff from . import ImageTestCaseMixin class TestImageDiff(ImageTestCaseMixin, TestCase): def test_nrmsd_all_channels(self): diff = ImageDiff(self.get_white_image(), self.get_black_image()) self.assertEqual(diff.get_nrmsd(), 1) def test_nrmsd_one_channel(self): diff = ImageDiff(self.get_image((255, 0, 0)), self.get_black_image()) self.assertEqual(diff.get_nrmsd(), math.sqrt(1.0 / 3)) def test_nrmsd_half_filled(self): diff = ImageDiff(self.get_black_image(), self.get_half_filled_image()) self.assertEqual(diff.get_nrmsd(), math.sqrt(0.5)) def test_distance_all_channels(self): diff = ImageDiff(self.get_white_image(), self.get_black_image()) self.assertAlmostEqual(diff.get_distance(), 100 * 100, delta=0.001) def test_distance_one_channel(self): diff = ImageDiff(self.get_image((255, 0, 0)), self.get_black_image()) self.assertAlmostEqual(diff.get_distance(), 10000.0 / 3, delta=0.001) def test_distance_half_filled(self): diff = ImageDiff(self.get_black_image(), self.get_half_filled_image()) self.assertAlmostEqual(diff.get_distance(), 10000.0 / 2, delta=0.001) ================================================ FILE: tests/test_driver.py ================================================ from needle.cases import NeedleTestCase class TestWebDriver(NeedleTestCase): def test_load_html(self): self.driver.load_html('
foo
') e = self.driver.find_element_by_id('test') self.assertEqual(e.text, 'foo') def test_load_html_works_with_large_pages(self): div = '
' + 'a' * 1000 + '
' html = ''.join(div for _ in range(500)) + '
hello
' self.driver.load_html(html) self.assertEqual( self.driver.execute_script( 'return document.getElementsByTagName("div").length' ), 501 ) e = self.driver.find_element_by_id('test') self.assertEqual(e.text, 'hello') def test_load_jquery(self): self.driver.load_html('
') self.driver.load_jquery() self.assertTrue(self.driver.execute_script(""" return jQuery !== undefined; """)) class TestWebElement(NeedleTestCase): def test_get_dimensions(self): self.driver.load_html("""
Test
""") e = self.driver.find_element_by_id('test') self.assertEqual(e.get_dimensions(), { 'left': 50, 'top': 100, 'width': 150, 'height': 200, }) def test_get_screenshot(self): self.driver.load_html("""
""") e = self.driver.find_element_by_id('test') im = e.get_screenshot() self.assertEqual(im.size, (150, 200)) for pixel in im.getdata(): self.assertEqual(pixel, (255, 0, 0)) ================================================ FILE: tests/test_imagemagick_engine.py ================================================ from __future__ import absolute_import import subprocess import unittest from os import devnull from tests.test_perceptualdiff_engine import PerceptualdiffEngineTests class ImageMagickEngineTests(PerceptualdiffEngineTests): engine_class = 'needle.engines.imagemagick_engine.Engine' @classmethod def setUpClass(cls): try: subprocess.call(['compare', '--version'], stdout=open(devnull), stderr=open(devnull)) except OSError: raise unittest.SkipTest('ImageMagick is not installed') super(ImageMagickEngineTests, cls).setUpClass() # Delete the imported module so it doesn't get executed here. del PerceptualdiffEngineTests ================================================ FILE: tests/test_in_memory.py ================================================ from __future__ import with_statement from needle.cases import NeedleTestCase from . import ImageTestCaseMixin class InMemoryTests(ImageTestCaseMixin, NeedleTestCase): def create_div(self): self.driver.load_html("""
""") def test_assertScreenshot(self): self.create_div() self.assertScreenshot( self.driver.find_element_by_id('test'), self.save_image_to_fh(self.get_black_image()) ) def test_assertScreenshot_with_css_selector(self): self.create_div() self.assertScreenshot( '#test', self.save_image_to_fh(self.get_black_image()) ) def test_assertScreenshot_fails(self): self.create_div() im = self.get_black_image() # Create one red pixel im.putpixel((0, 0), (255, 0, 0)) with self.assertRaises(AssertionError): # Default threshold for error is 0 self.assertScreenshot( self.driver.find_element_by_id('test'), self.save_image_to_fh(im) ) def test_assertScreenshot_does_not_fail_with_threshold(self): self.create_div() im = self.get_black_image() # Create one red pixel im.putpixel((0, 0), (255, 0, 0)) self.assertScreenshot( self.driver.find_element_by_id('test'), self.save_image_to_fh(im), threshold=1 ) def test_assertScreenshot_fails_with_threshold(self): self.create_div() im = self.get_black_image() # Create two white pixels im.putpixel((0, 0), (255, 255, 255)) im.putpixel((1, 0), (255, 255, 255)) with self.assertRaises(AssertionError): self.assertScreenshot( self.driver.find_element_by_id('test'), self.save_image_to_fh(im), threshold=1 ) ================================================ FILE: tests/test_perceptualdiff_engine.py ================================================ from __future__ import absolute_import import subprocess import unittest from os import path, devnull from tests.test_pil_engine import PILEngineTests class PerceptualdiffEngineTests(PILEngineTests): engine_class = 'needle.engines.perceptualdiff_engine.Engine' @classmethod def setUpClass(cls): try: subprocess.call(['perceptualdiff', '--version'], stdout=open(devnull), stderr=open(devnull)) except OSError: raise unittest.SkipTest('perceptualdiff is not installed') super(PerceptualdiffEngineTests, cls).setUpClass() def test_assertScreenshot_failure(self): super(PerceptualdiffEngineTests, self).test_assertScreenshot_failure() # Check that the diff file was created diff_file = path.join(path.dirname(__file__), 'screenshots', 'black-box.diff.png') self.assertTrue(path.isfile(diff_file)) # Delete the imported module so it doesn't get executed here. del PILEngineTests ================================================ FILE: tests/test_pil_engine.py ================================================ from __future__ import absolute_import import os from os import path from needle.cases import NeedleTestCase from tests import ImageTestCaseMixin class PILEngineTests(ImageTestCaseMixin, NeedleTestCase): engine_class = 'needle.engines.pil_engine.Engine' output_directory = 'tests/screenshots/' baseline_directory = 'tests/screenshots/baseline/' def setUp(self): # Remove all screeshots from previous test runs screenshots_path = path.join(path.dirname(__file__), 'screenshots') screenshots = [ f for f in os.listdir(screenshots_path) if f.endswith(".png") ] for screenshot in screenshots: os.remove(path.join(screenshots_path, screenshot)) super(PILEngineTests, self).setUp() def test_assertScreenshot_success(self): self.load_black_div() self.assertScreenshot( self.driver.find_element_by_id('black-box'), 'black-box' ) def test_assertScreenshot_failure(self): with self.assertRaises(AssertionError): self.load_black_div('hello') self.assertScreenshot( self.driver.find_element_by_id('black-box'), 'black-box' ) ================================================ FILE: tests/test_plugin.py ================================================ import logging import sys import os from errno import EEXIST from needle.plugin import NeedleCapturePlugin, SaveBaselinePlugin, CleanUpOnSuccessPlugin from nose.plugins import PluginTester if sys.version_info > (2, 7): from unittest import TestCase else: from unittest2 import TestCase baseline_filename = 'screenshots/baseline/red_box.png' screenshot_filename = 'screenshots/red_box.png' dummy_baseline_content = b'abcd' log = logging.getLogger(__name__) def create_baseline_dir(): """ Create the baseline images directory if it doesn't already exist. """ dir_path = 'screenshots/baseline' try: os.makedirs(dir_path) except OSError as err: if err.errno == EEXIST and os.path.isdir(dir_path): pass else: raise class NeedlePluginTester(PluginTester): """ Base class for tests of needle's nose plugins. """ suitepath = 'tests/plugin_test_cases/red_box.py' def setUp(self): """ Run the wrapped test suite and log its output for use in debugging failures. """ super(NeedlePluginTester, self).setUp() log.debug(self.output) def tearDown(self): """ Remove the baseline image created by the test. """ os.remove(baseline_filename) class NeedleCaptureTest(NeedlePluginTester, TestCase): """ Check that the baseline file gets saved when using the --with-needle-capture option. """ activate = '--with-needle-capture' plugins = [NeedleCapturePlugin()] def setUp(self): self.assertFalse(os.path.exists(baseline_filename)) super(NeedleCaptureTest, self).setUp() def test_baseline_is_saved(self): self.assertTrue(os.path.exists(baseline_filename)) self.assertTrue(self.nose.success) class NeedleCaptureOverwriteTest(NeedlePluginTester, TestCase): """ Check that an existing baseline file does NOT get overwritten, when using the --with-needle-capture option. """ activate = '--with-needle-capture' plugins = [NeedleCapturePlugin()] def setUp(self): self.assertFalse(os.path.exists(baseline_filename)) # Create dummy baseline file create_baseline_dir() baseline = open(baseline_filename, 'wb') baseline.write(dummy_baseline_content) baseline.close() super(NeedleCaptureOverwriteTest, self).setUp() def test_existing_baseline_not_overwritten(self): baseline = open(baseline_filename, 'rb') self.assertEqual(baseline.read(), dummy_baseline_content) self.assertTrue(self.nose.success) class SaveBaselineTest(NeedlePluginTester, TestCase): """ Check that the baseline file gets saved when using the --with-save-baseline option. """ activate = '--with-save-baseline' plugins = [SaveBaselinePlugin()] def setUp(self): self.assertFalse(os.path.exists(baseline_filename)) super(SaveBaselineTest, self).setUp() def test_baseline_is_saved(self): self.assertTrue(os.path.exists(baseline_filename)) self.assertTrue(self.nose.success) class SaveBaselineOverwriteTest(NeedlePluginTester, TestCase): """ Check that an existing baseline file DOES get overwritten, when using the --with-save-baseline option. """ activate = '--with-save-baseline' plugins = [SaveBaselinePlugin()] def setUp(self): self.assertFalse(os.path.exists(baseline_filename)) # Create dummy baseline file create_baseline_dir() baseline = open(baseline_filename, 'wb') baseline.write(dummy_baseline_content) baseline.close() super(SaveBaselineOverwriteTest, self).setUp() def test_existing_baseline_is_overwritten(self): baseline = open(baseline_filename, 'rb') self.assertNotEqual(baseline.read(), dummy_baseline_content) self.assertTrue(self.nose.success) class NeedleCleanupOnSuccessTest(NeedlePluginTester, TestCase): """ Check that the screenshot gets removed when using the needle-cleanup-on-success option. """ activate = '--with-needle-cleanup-on-success' plugins = [CleanUpOnSuccessPlugin()] def setUp(self): # Create the baseline create_baseline_dir() baseline = open(baseline_filename, 'wb') baseline.write(open('tests/test_red_box.png', 'rb').read()) baseline.close() # Make sure the screenshot doesn't exist yet self.assertFalse(os.path.exists(screenshot_filename)) super(NeedleCleanupOnSuccessTest, self).setUp() def test_screenshot_is_cleanedup(self): # Make sure the screenshot has been cleaned up self.assertFalse(os.path.exists(screenshot_filename)) self.assertTrue(self.nose.success) ================================================ FILE: tox.ini ================================================ [tox] envlist = py{27,34,35,36}-{chrome,firefox,phantomjs} [testenv] setenv = chrome: NEEDLE_BROWSER=chrome firefox: NEEDLE_BROWSER=firefox phantomjs: NEEDLE_BROWSER=phantomjs commands = nosetests {posargs} -v [testenv:docs] deps = Sphinx==1.5.3 commands = sphinx-build -W -b {posargs:html} docs docs/_build