├── behat ├── .gitignore ├── build.sh ├── composer.json ├── contributed-drupal-module-subcontexts │ ├── chosen.behat.inc │ ├── bean.behat.inc │ └── panels.behat.inc └── README.md ├── visual-regression-testing ├── Gruntconfig.json ├── tests │ ├── fullwidth │ │ ├── global.js │ │ └── office_homepages.js │ └── tiny │ │ └── search.js ├── package.json ├── README.md ├── help.html ├── index.js ├── Gruntfile.js ├── style.css └── index.php ├── example └── behat │ ├── behat.yml │ └── default.feature └── README.md /behat/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /behat/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Install dependencies 4 | composer install 5 | bin/behat --init 6 | 7 | -------------------------------------------------------------------------------- /visual-regression-testing/Gruntconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "rootUrls": { 3 | "local": "http://", 4 | "dev": "http://", 5 | "stage": "http://", 6 | "prod": "http://" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /behat/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "drupal/drupal-extension": "~3.0", 4 | "drupal/drupal-driver": "dev-master" 5 | }, 6 | "config": { 7 | "bin-dir": "bin/" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /behat/contributed-drupal-module-subcontexts/chosen.behat.inc: -------------------------------------------------------------------------------- 1 | assertSession()->addressMatches("/^\/block\/([a-zA-Z0-9\-]*)\/view/"); 15 | $base_url = $this->getSession()->getCurrentUrl(); 16 | $edit_url = str_replace("/view", "/edit", $base_url); 17 | $this->getSession()->visit($edit_url); 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /visual-regression-testing/README.md: -------------------------------------------------------------------------------- 1 | # Visual Regression Test Suite - README # 2 | 3 | ## Setup ## 4 | 1. Confirm nodeJS and ruby are installed 5 | 2. npm install -g grunt-cli 6 | 3. npm install 7 | 4. Create Gruntconfig.json file to define rootUrls, with the following structure: 8 | { 9 | "rootUrls": { 10 | "local": "http://.energy.p2devcloud.gov/", 11 | "dev": "http://dev.cms.doe.gov/", 12 | "stage": "http://stage.cms.doe.gov/", 13 | "prod": "http://energy.gov/" 14 | } 15 | } 16 | 5. Configure Gruntfile phantomcss subtasks. By default they are set to categorize tests by 4 viewports. 17 | 18 | ## Commands ## 19 | * grunt - Run all tests for all breakpoints against prod 20 | * grunt phantom:stage - Run all tests for all breakpoints against stage 21 | * grunt phantom:dev:fullwidth - Run all fullwidth tests against dev 22 | -------------------------------------------------------------------------------- /visual-regression-testing/tests/tiny/search.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hypercare Visual Regression Testing - Search interactive test example. 3 | */ 4 | 5 | casper.thenOpen(phantom.rootUrl + 'search/site') 6 | .then(function() { 7 | this.evaluate(function() { 8 | // Override web fonts 9 | jQuery('*').css('font-family', 'arial, sans-serif'); 10 | // Outline search results, hide their contents, and set fixed height. 11 | jQuery('.search-result').css('background', 'gray').css('border', '1px solid black').css('overflow', 'hidden').css('height', '10px'); 12 | jQuery('#footer-legal-wrapper a').css('background', 'gray').css('border', '1px solid black').css('height', '20px').css('overflow', 'hidden'); 13 | jQuery('#footer-legal-wrapper a span').css('opacity', '0'); 14 | jQuery('.search-result *').css('opacity', '0'); 15 | // Open 'Filter by type' facet. 16 | jQuery('#block-facetapi-gi88xvjatwe8mxkhwyib2s2zjyaj1ida h4.block-title').click(); 17 | }); 18 | phantomcss.screenshot('#page-wrapper', 'search'); 19 | }); 20 | -------------------------------------------------------------------------------- /visual-regression-testing/tests/fullwidth/office_homepages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hypercare Visual Regression Testing - Multi-page test example. 3 | */ 4 | 5 | var offices = [ 6 | 'office_one', 7 | 'office_two', 8 | 'office_three' 9 | ]; 10 | 11 | offices.forEach(function(office) { 12 | var officeName = office.replace(/\//g, '-'); 13 | 14 | casper.thenOpen(phantom.rootUrl + office) 15 | .then(function() { 16 | this.evaluate(function() { 17 | // Override web fonts 18 | jQuery('*').css('font-family', 'arial, sans-serif'); 19 | // Outline containers. 20 | jQuery('#page-wrapper .block').css('background', 'gray').css('border', '1px solid black'); 21 | jQuery('#footer-nav-wrapper .menu a span').css('background', 'gray').css('border', '1px solid black').css('color', 'transparent').css('width', '75px').css('min-width', '75px').css('display', 'inline-block').css('height', '15px').css('overflow', 'hidden'); 22 | jQuery('#footer-legal-wrapper a').css('background', 'gray').css('border', '1px solid black').css('height', '20px').css('overflow', 'hidden'); 23 | 24 | // Hide content. 25 | jQuery('.block-bean > *').css('opacity', '0'); 26 | jQuery('.block-bean').css('height', '200px').css('overflow', 'hidden'); 27 | }); 28 | }) 29 | .then(function() { 30 | phantomcss.screenshot('#content-wrapper', 'office_homepage_' + officeName); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /behat/contributed-drupal-module-subcontexts/panels.behat.inc: -------------------------------------------------------------------------------- 1 | getSession()->wait(5000, 'jQuery(".panels-ipe-editing").length > 0'); 15 | } 16 | 17 | /** 18 | * @When /^(?:|I )wait for the Panels IPE to deactivate$/ 19 | * 20 | * Wait until the Panels IPE is deactivated. 21 | */ 22 | public function waitForIPEtoDeactivate() { 23 | $this->getSession()->wait(5000, 'jQuery(".panels-ipe-editing").length === 0'); 24 | } 25 | 26 | /** 27 | * @When /^(?:|I )customize this page with the Panels IPE$/ 28 | * 29 | * Enable the Panels IPE if it's available on the current page. 30 | */ 31 | public function customizeThisPageIPE() { 32 | $this->getSession()->getPage()->clickLink('Customize this page'); 33 | $this->waitForIPEtoActivate(); 34 | } 35 | 36 | /** 37 | * @When /^(?:|I )wait for live preview to finish$/ 38 | * 39 | * Wait until the live preview to finish. 40 | */ 41 | public function waitForLivePreview() { 42 | $this->getSession()->wait(5000, 'jQuery(".form-submit").value == "Save"'); 43 | } 44 | } -------------------------------------------------------------------------------- /visual-regression-testing/help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |

Visual Regression Help

8 | 9 |
10 |

The admin interface was built to allow for easy viewing of the PhantomCSS baselines and test results. When tests are run for the first time they will generate a list of baselines. These baselines are the screenshots that subsequent screenshots will compare themselves against and take the file naming of "[file].png". When the same tests are run and baselines already exist each test will compare it's screenshot against the corresponding baseline. Test results take on one of two file namings - "[file].diff.png" for passing test results and "[file].fail.png" for failing test results.

11 |

The interface can be split into two sections: the file listing on the left-hand side and the image viewer on the right-hand side. The file listing allows the user to easily view the screenshots that have been generated and by clicking on a screenshot, view it immediately within the image viewer on the right. Screenshots are categorized by baseline, passed test, and failed-test. Within each category tests are sorted by time, with the newest screenshots at the top. Lastly, environmental filters may be listed at the top of the file listing if screenshots from multiple environments have been generated. By default, screenshots for all environments are shown, but upon clicking a filter the user will only be shown baselines and test results from the specified environment.

12 |

Further documentation can be found on the Phase2 Wiki

13 | 14 | 15 | -------------------------------------------------------------------------------- /visual-regression-testing/index.js: -------------------------------------------------------------------------------- 1 | window.onload = function () { 2 | // Global vars so JS won't have to load more than once. 3 | IMAGE = document.getElementById('image_diff'); 4 | RESULTS = document.getElementsByClassName('result'); 5 | HEADERS = document.getElementsByTagName('h4'); 6 | FILTERS = document.getElementsByClassName("env"); 7 | RESULTSLEN = RESULTS.length; 8 | 9 | loadImage(); 10 | 11 | var resultClick = function() { 12 | document.location = this.dataset.href; 13 | }; 14 | 15 | // Filter by env. 16 | var filter = function() { 17 | for (var i=0; i -1) { 19 | RESULTS[i].style.display = 'table-row'; 20 | } else { 21 | RESULTS[i].style.display = 'none'; 22 | } 23 | } 24 | 25 | // Hide headers if no items within associated list by checking list's height. 26 | env = this.id.charAt(0).toUpperCase() + this.id.slice(1); 27 | for (var i=0; i * { 6 | display: inline-block; 7 | } 8 | .header .nav-link { 9 | float: right; 10 | margin: 20px 100px 20px 50px; 11 | } 12 | #menu, #image_diff_wrapper { 13 | display: inline-block; 14 | vertical-align: top; 15 | height: 600px; 16 | border: 1px solid #555; 17 | } 18 | #menu { 19 | width: 40%; 20 | overflow: hidden; 21 | } 22 | #file_list { 23 | overflow: scroll; 24 | height: 567px; 25 | } 26 | #file_list h4 { 27 | padding: 10px 5px; 28 | margin: 0; 29 | background: #999; 30 | color: #333; 31 | font-family: sans-serif; 32 | letter-spacing: .25px; 33 | } 34 | #file_list table { 35 | width: 100%; 36 | border-spacing: 0; 37 | margin-bottom: 20px; 38 | } 39 | #file_list table tr { 40 | border-bottom: 2px solid white; 41 | } 42 | #file_list table tr:hover { 43 | cursor: pointer; 44 | background: #BBB; 45 | } 46 | #file_list table td { 47 | vertical-align: top; 48 | border-top: 2px solid white; 49 | } 50 | #file_list table tr:nth-child(1) td { 51 | border-top: 0; 52 | } 53 | #file_list table td:nth-child(1) { 54 | padding-left: 10px; 55 | } 56 | #file_list table td:nth-child(2) { 57 | width: 20%; 58 | } 59 | #file_list table td:nth-child(3) { 60 | width: 10%; 61 | } 62 | #file_list #title_file_list_fail { 63 | background: #D46A6A; 64 | } 65 | #file_list #title_file_list_pass { 66 | background: #57A857; 67 | } 68 | #file_list #file_list_fail tr:hover { 69 | background: #FCA5A5; 70 | } 71 | #file_list #file_list_pass tr:hover { 72 | background: #6FB86F; 73 | } 74 | 75 | .tag { 76 | display: block; 77 | padding: 0 10px; 78 | margin: 1px 10px; 79 | background: #AAA; 80 | border-radius: 15px; 81 | color: #333; 82 | font-size: .8em; 83 | text-align: center; 84 | } 85 | .tag.tag-fullwidth { background: #6ac1d4; } 86 | .tag.tag-midwidth { background: #87e38d; } 87 | .tag.tag-narrow { background: #dfda68; } 88 | .tag.tag-tiny { background: #9174a9; } 89 | 90 | #env_menu ul { 91 | padding: 0; 92 | margin: 0; 93 | display: table; 94 | width: 100%; 95 | list-style: none; 96 | table-layout: fixed; 97 | } 98 | #env_menu li { 99 | display: table-cell; 100 | text-align: center; 101 | border: 1px solid #555; 102 | border-top: 0; 103 | border-left: 0; 104 | -moz-box-sizing: border-box; 105 | -webkit-box-sizing: border-box; 106 | -ms-box-sizing: border-box; 107 | box-sizing: border-box; 108 | } 109 | #env_menu li:last-child { 110 | border-right: 0; 111 | } 112 | #env_menu li a { 113 | padding: 8px; 114 | display: block; 115 | text-decoration: none; 116 | color: #333; 117 | font-family: sans-serif; 118 | cursor: pointer; 119 | } 120 | #env_menu li a.active { 121 | background-color: #AAA; 122 | color: #FFF; 123 | } 124 | #env_menu li a:hover, 125 | #env_menu li a:active { 126 | background-color: #555; 127 | color: #FFF; 128 | } 129 | #image_diff_wrapper { 130 | width: 59%; 131 | text-align: center; 132 | } 133 | #image_diff { 134 | max-width:100%; 135 | max-height:100%; 136 | vertical-align: middle; 137 | } 138 | #help a, #help a:active, #help a:visited { 139 | color: #000; 140 | } 141 | -------------------------------------------------------------------------------- /example/behat/default.feature: -------------------------------------------------------------------------------- 1 | @api 2 | Feature: Homepage 3 | In order to know the website is running 4 | As a website user 5 | I need to be able to view the site title and login 6 | 7 | @api @login 8 | Scenario: As a logged in user 9 | Given I am logged in as a user with the "administrator" role 10 | #And I wait for AJAX to finish 11 | When I visit "/admin" 12 | Then I should see the link "Content" 13 | 14 | Scenario: Viewing the site title 15 | Given I am on the homepage 16 | Then I should see "Welcome to TESTSUITE" 17 | 18 | Scenario: See "Add Content" 19 | Given I am logged in as a user with the "administrator" role 20 | And I am on the homepage 21 | When I follow "Add content" 22 | Then I should see "Basic Page" 23 | 24 | Scenario: Create many nodes 25 | Given "page" content: 26 | | title | 27 | | Testsuite Page one | 28 | | Testsuite Page two | 29 | And "article" content: 30 | | title | 31 | | Testsuite First article | 32 | | Testsuite Second article | 33 | And I am logged in as a user with the "administrator" role 34 | When I go to "admin/content" 35 | Then I should see "Testsuite Page one" 36 | And I should see "Testsuite Page two" 37 | And I should see "Testsuite First article" 38 | And I should see "Testsuite Second article" 39 | 40 | Scenario: Create nodes with fields 41 | Given "article" content: 42 | | title | promote | body | 43 | | Testsuite First article with fields | 1 | PLACEHOLDER BODY | 44 | When I am on the homepage 45 | And follow "Testsuite First article with fields" 46 | Then I should see the text "PLACEHOLDER BODY" 47 | 48 | Scenario: Create and view a node with fields 49 | Given I am viewing an "Article" content: 50 | | title | My article with fields! | 51 | | body | A placeholder | 52 | Then I should see the heading "My article with fields!" 53 | And I should see the text "A placeholder" 54 | 55 | Scenario: Create users 56 | Given users: 57 | | name | mail | status | 58 | | Testsuite User | testsuite@example.com | 1 | 59 | And I am logged in as a user with the "administrator" role 60 | When I visit "admin/people" 61 | Then I should see the link "Testsuite User" 62 | 63 | Scenario: Login as a user created during this scenario 64 | Given users: 65 | | name | status | 66 | | Testsuite user | 1 | 67 | When I am logged in as "Testsuite user" 68 | Then I should see the link "Log out" 69 | 70 | Scenario: Create a term 71 | Given I am logged in as a user with the "administrator" role 72 | When I am viewing a "tags" term with the name "Testsuite tag" 73 | Then I should see the heading "Testsuite tag" 74 | 75 | Scenario: Create many terms 76 | Given "tags" terms: 77 | | name | 78 | | Testsuite Tag one | 79 | | Testsuite Tag two | 80 | And I am logged in as a user with the "administrator" role 81 | When I go to "admin/structure/taxonomy/tags" 82 | Then I should see "Testsuite Tag one" 83 | And I should see "Testsuite Tag two" 84 | 85 | Scenario: Create nodes with specific authorship 86 | Given users: 87 | | name | mail | status | 88 | | Testsuite User | testsuite@example.com | 1 | 89 | And "article" content: 90 | | title | author | body | promote | 91 | | Article by Testsuite | Testsuite User | PLACEHOLDER BODY | 1 | 92 | When I am logged in as a user with the "administrator" role 93 | And I am on the homepage 94 | And I follow "Article by Testsuite" 95 | Then I should see the link "Testsuite User" 96 | 97 | Scenario: Create an article with multiple term references 98 | Given "tags" terms: 99 | | name | 100 | | Testsuite Tag one | 101 | | Testsuite Tag two | 102 | | Testsuite Tag three | 103 | | Testsuite Tag four | 104 | And "article" content: 105 | | title | body | promote | field_tags | 106 | -------------------------------------------------------------------------------- /visual-regression-testing/index.php: -------------------------------------------------------------------------------- 1 | Visual Regression Test Results'; 16 | print '

No tests found within ' . getcwd() . '

'; 17 | return; 18 | } 19 | 20 | // Sort the files by modification/creation date. 21 | usort($files, function($a, $b) { 22 | return filemtime($a) < filemtime($b); 23 | }); 24 | 25 | // Define environments and file count for each. 26 | $envs = array( 27 | 'local' => 0, 28 | 'dev' => 0, 29 | 'stage' => 0, 30 | 'prod' => 0 31 | ); 32 | 33 | // Organize the files by fail/pass/baseline. 34 | $files_fail = array(); 35 | $files_diff = array(); 36 | foreach ($files as $i => $file) { 37 | // Determine test env for file and increment env count. 38 | $urlarray = explode("/", $file); 39 | $env = $urlarray[count($urlarray)-3]; 40 | $envs[$env]++; 41 | 42 | if (strpos($file, '.fail') !== FALSE) { 43 | unset($files[$i]); 44 | $files_fail[] = $file; 45 | } 46 | elseif (strpos($file, '.diff') !== FALSE) { 47 | unset($files[$i]); 48 | $files_diff[] = $file; 49 | } 50 | } 51 | 52 | // Print output. 53 | print ""; 54 | print ""; 55 | print "
"; 56 | print '

Visual Regression Test Results

'; 57 | print ""; 58 | print "
"; 59 | print ""; 67 | print "
"; 68 | print ""; 69 | print "
"; 70 | 71 | /** 72 | * Recursive search for pattern. 73 | */ 74 | function glob_recursive($pattern, $flags = 0) { 75 | $files = glob($pattern, $flags); 76 | foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) { 77 | $files = array_merge($files, glob_recursive($dir.'/'.basename($pattern), $flags)); 78 | } 79 | return $files; 80 | } 81 | 82 | /** 83 | * Clean up URL's. 84 | */ 85 | function friendlyURL($inputString){ 86 | $url = strtolower($inputString); 87 | $url = substr($url, 0, strrpos($url, ".")); // Remove file extension. 88 | $patterns = $replacements = array(); 89 | $patterns[0] = '/(&|&)/i'; // Replace ampersands. 90 | $replacements[0] = '-and-'; 91 | $patterns[1] = '/[^a-zA-Z01-9\/\.\-\_]/i'; // Replace non-alphanumeric except slashes and periods. 92 | $replacements[1] = '-'; 93 | $patterns[2] = '/^./i'; // Remove dot from beginning of URL if it exists. 94 | $replacements[2] = ''; 95 | $url = preg_replace($patterns, $replacements, $url); 96 | return $url; 97 | } 98 | 99 | /** 100 | * Format links and print. 101 | */ 102 | function printLinks($files, $heading, $id = NULL) { 103 | if (empty($files)) { 104 | return; 105 | } 106 | 107 | print "All " . $heading .""; 110 | print (!is_null($id) ? "" : "
"); 111 | foreach ($files as $file) { 112 | $fileData = stat($file); 113 | $fileComponents = explode("/", ltrim($file, "./")); // Trim leading dots and slashes. Split file path into components. 114 | $fileComponentsCount = count($fileComponents); 115 | $fileName = $fileComponents[$fileComponentsCount - 1]; 116 | print ''; 117 | print ''; 118 | print ''; 119 | print ''; 127 | print ''; 128 | } 129 | print '
' . $fileName . '' . date('Y-m-d H:i:s', $fileData['mtime']) . ''; 120 | foreach($fileComponents as $i => $fileComponent) { 121 | // Don't print last three components: file env, results/screenshots, file name; 122 | if ($i < ($fileComponentsCount - 3)) { 123 | print '' . $fileComponent . ''; 124 | } 125 | } 126 | print '
'; 130 | } 131 | 132 | /** 133 | * Format filters and print. 134 | */ 135 | function printFilters($envs) { 136 | $filters = array(); 137 | 138 | // Only add filter to list if sceenshot exists for it. 139 | foreach ($envs as $env => $env_count) { 140 | if ($env_count > 0) { 141 | $filters[] = "
  • " . ucfirst($env) . "
  • "; 142 | } 143 | } 144 | 145 | // Only display list of filters if more than one filter is available. 146 | if (count($filters) > 1) { 147 | print "
      "; 148 | print "
    • All
    • "; 149 | foreach ($filters as $filter) { 150 | print $filter; 151 | } 152 | print "
    "; 153 | } 154 | } 155 | --------------------------------------------------------------------------------