diff --git a/gulp/browserSyncManager.js b/gulp/browserSyncManager.js index e84f535f2..f3be925bc 100644 --- a/gulp/browserSyncManager.js +++ b/gulp/browserSyncManager.js @@ -1,13 +1,23 @@ var browserSync = require('browser-sync'); +var bsyncHandle = null; module.exports = { closeServer: closeServer, reloadServer: reloadServer, streamToServer: streamToServer, startServer: startServer, + getBSyncHandle: getBSyncHandle }; +function getBSyncHandle() { + return bsyncHandle; +} + +function initBSyncHandle(label) { + return bsyncHandle = ((label ? browserSync.create(label) : browserSync.create())); +} + function closeServer(label) { browserSync.get(label).exit(); } @@ -25,9 +35,9 @@ function startServer(args) { var port = args.port; var baseDir = args.baseDir; var middleware = args.middleware; - var open = args.open; + var open = args.open || false; // Callee, '08-servers.js' does not set this to a value so expect it to be undefined - var server = browserSync.create(label); + initBSyncHandle(label); var conf = { port: port, server: { @@ -38,6 +48,6 @@ function startServer(args) { if(middleware) { conf.middleware = args.middleware; } - server.init(conf); + bsyncHandle.init(conf); } \ No newline at end of file diff --git a/gulp/config.js b/gulp/config.js index feb51f2ad..87c96755f 100644 --- a/gulp/config.js +++ b/gulp/config.js @@ -92,6 +92,10 @@ function viewJsDir() { return `primo-explore/custom/${view}/js`; } +function viewImgDir() { + return `primo-explore/custom/${view}/img`; +} + function mainPath() { return viewJsDir()+'/*.js'; } @@ -100,6 +104,10 @@ function mainJsPath() { return viewJsDir()+'/main.js'; } +function mainImgPath() { + return viewImgDir()+'/*.png'; +} + function customCssMainPath() { return viewCssDir()+'/*.css'; } @@ -154,6 +162,9 @@ function customNpmHtmlPath() { return `primo-explore/custom/${view}/node_modules/primo-explore*/html/*.html`; } +function customNpmImgPath() { + return `primo-explore/custom/${view}/node_modules/primo-explore*/img/*.*`; +} var SERVERS = { local: 'http://localhost:8002' }; @@ -176,10 +187,12 @@ let buildParams = { customModulePath: customModulePath, mainPath: mainPath, mainJsPath: mainJsPath, + mainImgPath: mainImgPath, viewRootDir: viewRootDir, viewJsDir: viewJsDir, viewHtmlDir: viewHtmlDir, viewCssDir: viewCssDir, + viewImgDir: viewImgDir, customScssDir: customScssDir, customScssMainPath: customScssMainPath, customCssPath: customCssPath, @@ -189,6 +202,7 @@ let buildParams = { customNpmJsCustomPath: customNpmJsCustomPath, customNpmJsModulePath: customNpmJsModulePath, customNpmCssPath: customNpmCssPath, + customNpmImgPath: customNpmImgPath, customNpmHtmlPath: customNpmHtmlPath, customCssMainPath: customCssMainPath, customColorsPath: customColorsPath diff --git a/gulp/tasks/04-custom-css.js b/gulp/tasks/04-custom-css.js index 927dae71d..7457663f8 100644 --- a/gulp/tasks/04-custom-css.js +++ b/gulp/tasks/04-custom-css.js @@ -8,24 +8,32 @@ let concat = require("gulp-concat"); let debug = require('gulp-debug'); var wrap = require("gulp-wrap"); var glob = require('glob'); +let browserSyncManager = require('../browserSyncManager'); let buildParams = config.buildParams; gulp.task('watch-css', gulp.series('select-view', (cb) => { - gulp.watch([buildParams.customCssMainPath(),buildParams.customNpmCssPath(),'!'+buildParams.customCssPath()], {interval: 1000, usePolling: true}, gulp.series('custom-css')); + var filesWatchGlob = [buildParams.customCssMainPath(), buildParams.customNpmCssPath()]; + var excludesFilesGlob = ['!'+buildParams.customCssPath()] + gulp.watch(filesWatchGlob.concat(excludesFilesGlob), {interval: 3000, usePolling: true}, gulp.series('custom-css')); cb(); -})); +})); // gulp.task('custom-css', gulp.series('select-view', () => { - - return gulp.src([buildParams.customCssMainPath(),buildParams.customNpmCssPath(),'!'+buildParams.customCssPath()]) - .pipe(concat(buildParams.customCssFile)) - .pipe(gulp.dest(buildParams.viewCssDir())); - + if (browserSyncManager.getBSyncHandle() === null) { + return gulp.src([buildParams.customCssMainPath(),buildParams.customNpmCssPath(),'!'+buildParams.customCssPath()]) + .pipe(concat(buildParams.customCssFile)) + .pipe(gulp.dest(buildParams.viewCssDir())); + } else { + return gulp.src([buildParams.customCssMainPath(),buildParams.customNpmCssPath(),'!'+buildParams.customCssPath()]) + .pipe(concat(buildParams.customCssFile)) + .pipe(gulp.dest(buildParams.viewCssDir())) + .pipe(browserSyncManager.getBSyncHandle().stream()); + } })); diff --git a/gulp/tasks/09-images.js b/gulp/tasks/09-images.js index 33059521f..e876a724b 100644 --- a/gulp/tasks/09-images.js +++ b/gulp/tasks/09-images.js @@ -3,15 +3,27 @@ const gulp = require('gulp'); const flatten = require('gulp-flatten'); const config = require('../config.js'); +const imagemin = require('gulp-imagemin'); let buildParams = config.buildParams; gulp.task('watch-img', () => { - gulp.watch([buildParams.viewImgDir(), '!'+buildParams.customNpmImgPath()], {interval: 1000, usePolling: true}, gulp.series('custom-img')); + var filesWatchGlob = [buildParams.viewImgDir()]; + var excludesFilesGlob = ['!'+ buildParams.customNpmImgPath()]; + gulp.watch(filesWatchGlob.concat(excludesFilesGlob), {interval: 1000, usePolling: true}, gulp.series('custom-img')); }); gulp.task('custom-img', () => { return gulp.src(buildParams.customNpmImgPath()) .pipe(flatten()) + .pipe(imagemin([ + imagemin.optipng({optimizationLevel: 5}), + imagemin.svgo({ + plugins: [ + {removeViewBox: true}, + {cleanupIDs: false} + ] + }) + ])) .pipe(gulp.dest(buildParams.viewImgDir())); }); diff --git a/gulp/tasks/10-create-package.js b/gulp/tasks/10-create-package.js index 0771f44f5..8af4bd36f 100644 --- a/gulp/tasks/10-create-package.js +++ b/gulp/tasks/10-create-package.js @@ -5,7 +5,7 @@ const prompt = require('prompt'); const zip = require('gulp-zip'); const config = require('../config.js'); -gulp.task('create-package', gulp.series('select-view', 'custom-js','custom-scss','custom-css', function (cb) { +gulp.task('create-package', gulp.series('select-view', 'custom-js', 'custom-scss', 'custom-css', 'custom-img', function (cb) { const code = config.view(); console.log('Creating package for : ('+code+'.zip)'); console.log(code); diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 000000000..48f53e55b --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,74 @@ +// Karma configuration +// Generated on Fri Oct 13 2023 22:07:08 GMT+0100 (British Summer Time) + +module.exports = function(config) { + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['jasmine', 'requirejs'], + + + // list of files / patterns to load in the browser + files: [ + 'tests/angular.min.js', + 'tests/angular-mocks.js', + // {pattern: 'primo-explore/custom/IAMS_VU2/js/*.js', included: false}, + {pattern: 'gulp/config.js', included: true}, + {pattern: 'tests/automated/**/*Spec.js', included: false}, + 'tests/test-main.js' + ], + + + // list of files / patterns to exclude + exclude: [ + ], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_ERROR, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Chrome'], + + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: Infinity + }) +} diff --git a/package.json b/package.json index 9c2810b54..158b331ee 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,23 @@ { "name": "primo-explore-devenv", - "version": "1.1.0", + "version": "1.1.3", "description": "The Exlibris Primo Open Discovery Framework", "author": "noamamit92", + "engines": { + "node": ">=13.14.0 < 15" + }, + "browserslist": [ + "last 2 version", + "> 2%" + ], + "repository": "https://github.com/ExLibrisGroup/primo-explore-devenv", + "dependencies": { + "requirejs": "2.1.0" + }, + "scripts": { + "testrunnerstart": "node ./node_modules/karma/bin/karma start karma.conf.js", + "testf-e": "node ./node_modules/karma/bin/karma run" + }, "devDependencies": { "babel-core": "6.26.3", "babel-plugin-angularjs-annotate": "0.8.2", @@ -13,7 +28,7 @@ "babel-preset-stage-2": "^6.24.1", "babelify": "8.0.0", "bluebird": "3.5.4", - "browser-sync": "2.9.9", + "browser-sync": "^2.23.7", "browserify": "16.2.3", "camel-case": "^3.0.0", "colors": "^1.3.3", @@ -35,6 +50,7 @@ "gulp-cssnano": "^2.1.3", "gulp-debug": "4.0.0", "gulp-flatten": "^0.4.0", + "gulp-imagemin": "^7.1.0", "gulp-plumber": "1.2.0", "gulp-rename": "1.4.0", "gulp-sass": "^5.1.0", @@ -42,11 +58,18 @@ "gulp-streamify": "1.0.2", "gulp-template": "5.0.0", "gulp-uglify": "3.0.2", + "gulp-using": "^0.1.1", "gulp-util": "3.0.8", "gulp-wrap": "^0.15.0", "gulp-zip": "4.2.0", "gulp4-run-sequence": "^1.0.1", "http-response-object": "3.0.2", + "jasmine-core": "3.5.0", + "karma": "2.0.0", + "karma-chrome-launcher": "2.2.0", + "karma-firefox-launcher": "1.1.0", + "karma-jasmine": "1.1.1", + "karma-requirejs": "1.1.0", "listdirs": "3.1.1", "lodash": "4.17.19", "merge-stream": "1.0.1", @@ -63,13 +86,5 @@ "tar-fs": "2.0.0", "vinyl-buffer": "1.0.1", "vinyl-source-stream": "2.0.0" - }, - "engines": { - "node": ">=16.17.0" - }, - "browserslist": [ - "last 2 version", - "> 2%" - ], - "repository": "https://github.com/ExLibrisGroup/primo-explore-devenv" -} + } +} \ No newline at end of file diff --git a/primo-explore/custom/.gitignore b/primo-explore/custom/.gitignore index 742d46278..fd5b47b0e 100644 --- a/primo-explore/custom/.gitignore +++ b/primo-explore/custom/.gitignore @@ -1,8 +1,16 @@ *.* # Add entries to prefixed with '!' to unhide your view files: # Eg: -# !*/css/** -# !*/html/** -# !*/img/** -# !*/js/** +#!*/css/** +#!*/js/** +#!*/html/** +#!*/img/** +!*/css/** +# The custom reskining files are now Git-visible from the above....BUT... +# the end-product of the build itself css/custom1.css does not need to be +# tracked hence the below +**/css/custom1.css +**/js/custom.js +**/img/* + diff --git a/primo-explore/custom/IAMS_VU2/css/README.md b/primo-explore/custom/IAMS_VU2/css/README.md new file mode 100644 index 000000000..71d94417c --- /dev/null +++ b/primo-explore/custom/IAMS_VU2/css/README.md @@ -0,0 +1,129 @@ +# The Primo New UI Customization Workflow Development Environment + + +##css documentation + +- Primo uses Angular Directives massively in this project + +- To learn more about directives see: +> https://docs.angularjs.org/guide/directive + +- Primo uses external directives from the Angular-material framework : +> https://material.angularjs.org/latest/ + +- Those directives are tagged by a prefix : "md-" + +- Primo also creates its own directives which are tagged by the "prm-" prefix. + + +Example: +``` +
+ + + + + + + + + +
+``` + + +- You can see in the example how we use : + +1. An HTML5 tag - header +2. A Primo directive : prm-topbar , prm-search-bar. +3. An external material design directive : md-progress-bar : +> https://material.angularjs.org/latest/api/directive/mdProgressLinear + + + +- When defining css rules it is important to understand the css cascading/specifity logic: + +> http://www.w3.org/TR/css3-cascade/ + +> https://specificity.keegan.st/ + + + + +- When you start working on customizing your css be aware of the ability to define css selectors based on the directive name, which is actually equivalent +to an html tag - this will enable you changing the design of a component cross-system without relying on id's/classes + +- For the example above we can define selectors: + +``` +prm-topbar input {....} +prm-topbar.md-primoExplore-theme input {....} +``` +- Primo is using a theme inside angular-material to define a palette of colors see: +> https://material.angularjs.org/latest/Theming/01_introduction + + +- This means that you will often encounter a class "md-primoExplore-theme" attached to elements. + + + +##Recipes/Examples: + + +# css Recipe 1 - Color Scheme + +- Open a new command line window + +- cd to the project base directory (C:\**\**\primo-explore-devenv) +- Run `gulp css-colors` to save the OTB css file +- Run `css-color-extractor primo-explore/tmp/app.css --format=css > primo-explore/tmp/colors.css` to extract the color definitions from the OTB css file and copy the css rules to primo-explore/custom/css/custom1.css + + +Run the following steps repeatedly until you are satisfied with the result + + +- Choose a color from the interface (using your browsers' dev tools or extensions such as colorzilla) + + +- Choose the new color from your library color scheme +- Replace all values in the custom1.css file +- Save and refresh your browser + + + +# css Recipe 2 - Moving the Facets to the Left + + +- Select the parent container containing the search result and the facets +- Copy the selector definition using your browsers' dev tools +- Define the container as +``` +display:flex; +flex-flow:row-reverse; +``` + + +- complete css definition: +``` +prm-search > md-content.md-primoExplore-theme .main { + display: -webkit-flex; !* Safari *! + -webkit-flex-flow: row-reverse wrap; !* Safari 6.1+ *! + display: flex; + flex-flow: row-reverse wrap; + +} +.screen-gt-sm .sidebar{ + webkit-flex: 0 0 15%; + flex: 0 0 15%; +} +``` +- Save and refresh your browser + + + + + + + + + diff --git a/primo-explore/custom/IAMS_VU2/css/bl_main_sitesearch_skin.css b/primo-explore/custom/IAMS_VU2/css/bl_main_sitesearch_skin.css new file mode 100644 index 000000000..195cee4e4 --- /dev/null +++ b/primo-explore/custom/IAMS_VU2/css/bl_main_sitesearch_skin.css @@ -0,0 +1,1065 @@ +@import "./header.css"; +@import "./footer.css"; +@import "./modal.css"; + + +.sidebar-inner-wrapper { + background: #e9e9e9; +} + + +md-content, +md-content.md-primoExplore-theme { + background: #e9e9e9 !important; +} + + +/*****************************************************************************/ +/* Global settings */ +/*****************************************************************************/ +/* Set fonts */ +body { + font-family: Arial, Helvetica, sans-serif; +} + +body { + background: #e9e9e9 url("../img/socam-hero-image-09.jpg") no-repeat center top; +} + +/* Colour of loading animation */ +.diamond { + background-color: #046b99 !important; +} + +/* Modify case for all main buttons (doesn't apply to buttons when logged in for requesting etc.) */ +.md-button { + text-transform: none; +} + + + +/* lots of text and hover boxes to change from aqua to blue*/ +.bar prm-authentication .md-button.link-alt-color, +.bar prm-authentication .section-title prm-icon.md-button, +.is-gallery-view prm-gallery-item .collection-element .item-actions .md-button, +.is-gallery-view prm-gallery-item .collection-element .item-actions button, +.is-grid-view prm-gallery-item .collection-element .item-actions .md-button, +.is-grid-view prm-gallery-item .collection-element .item-actions button, +.md-button.button-as-link.link-alt-color, +.section-title .bar prm-authentication prm-icon.md-button, +.section-title prm-icon.md-button.button-as-link, +prm-gallery-collection .collection-folder .item-actions .md-button, +prm-gallery-collection .collection-folder .item-actions button { + color: #046b99; +} + +.bar prm-authentication .md-button.link-alt-color._md-focused:not([disabled]), +.bar prm-authentication .md-button.link-alt-color.hovered:not([disabled]), +.bar prm-authentication .md-button.link-alt-color.md-focused:not([disabled]), +.bar prm-authentication .md-button.link-alt-color:focus:not([disabled]), +.bar prm-authentication .md-button.link-alt-color:hover:not([disabled]), +.bar prm-authentication .section-title prm-icon.md-button._md-focused:not([disabled]), +.bar prm-authentication .section-title prm-icon.md-button.hovered:not([disabled]), +.bar prm-authentication .section-title prm-icon.md-button.md-focused:not([disabled]), +.bar prm-authentication .section-title prm-icon.md-button:focus:not([disabled]), +.bar prm-authentication .section-title prm-icon.md-button:hover:not([disabled]), +.is-gallery-view prm-gallery-item .collection-element .item-actions ._md-focused.md-button:not([disabled]), +.is-gallery-view prm-gallery-item .collection-element .item-actions .hovered.md-button:not([disabled]), +.is-gallery-view prm-gallery-item .collection-element .item-actions .md-button:focus:not([disabled]), +.is-gallery-view prm-gallery-item .collection-element .item-actions .md-button:hover:not([disabled]), +.is-gallery-view prm-gallery-item .collection-element .item-actions .md-focused.md-button:not([disabled]), +.is-gallery-view prm-gallery-item .collection-element .item-actions button._md-focused:not([disabled]), +.is-gallery-view prm-gallery-item .collection-element .item-actions button.hovered:not([disabled]), +.is-gallery-view prm-gallery-item .collection-element .item-actions button.md-focused:not([disabled]), +.is-gallery-view prm-gallery-item .collection-element .item-actions button:focus:not([disabled]), +.is-gallery-view prm-gallery-item .collection-element .item-actions button:hover:not([disabled]), +.is-grid-view prm-gallery-item .collection-element .item-actions ._md-focused.md-button:not([disabled]), +.is-grid-view prm-gallery-item .collection-element .item-actions .hovered.md-button:not([disabled]), +.is-grid-view prm-gallery-item .collection-element .item-actions .md-button:focus:not([disabled]), +.is-grid-view prm-gallery-item .collection-element .item-actions .md-button:hover:not([disabled]), +.is-grid-view prm-gallery-item .collection-element .item-actions .md-focused.md-button:not([disabled]), +.is-grid-view prm-gallery-item .collection-element .item-actions button._md-focused:not([disabled]), +.is-grid-view prm-gallery-item .collection-element .item-actions button.hovered:not([disabled]), +.is-grid-view prm-gallery-item .collection-element .item-actions button.md-focused:not([disabled]), +.is-grid-view prm-gallery-item .collection-element .item-actions button:focus:not([disabled]), +.is-grid-view prm-gallery-item .collection-element .item-actions button:hover:not([disabled]), +.md-button.button-as-link.link-alt-color._md-focused:not([disabled]), +.md-button.button-as-link.link-alt-color.hovered:not([disabled]), +.md-button.button-as-link.link-alt-color.md-focused:not([disabled]), +.md-button.button-as-link.link-alt-color:focus:not([disabled]), +.md-button.button-as-link.link-alt-color:hover:not([disabled]), +.section-title .bar prm-authentication prm-icon.md-button._md-focused:not([disabled]), +.section-title .bar prm-authentication prm-icon.md-button.hovered:not([disabled]), +.section-title .bar prm-authentication prm-icon.md-button.md-focused:not([disabled]), +.section-title .bar prm-authentication prm-icon.md-button:focus:not([disabled]), +.section-title .bar prm-authentication prm-icon.md-button:hover:not([disabled]), +.section-title prm-icon.md-button.button-as-link._md-focused:not([disabled]), +.section-title prm-icon.md-button.button-as-link.hovered:not([disabled]), +.section-title prm-icon.md-button.button-as-link.md-focused:not([disabled]), +.section-title prm-icon.md-button.button-as-link:focus:not([disabled]), +.section-title prm-icon.md-button.button-as-link:hover:not([disabled]), +prm-gallery-collection .collection-folder .item-actions ._md-focused.md-button:not([disabled]), +prm-gallery-collection .collection-folder .item-actions .hovered.md-button:not([disabled]), +prm-gallery-collection .collection-folder .item-actions .md-button:focus:not([disabled]), +prm-gallery-collection .collection-folder .item-actions .md-button:hover:not([disabled]), +prm-gallery-collection .collection-folder .item-actions .md-focused.md-button:not([disabled]), +prm-gallery-collection .collection-folder .item-actions button._md-focused:not([disabled]), +prm-gallery-collection .collection-folder .item-actions button.hovered:not([disabled]), +prm-gallery-collection .collection-folder .item-actions button.md-focused:not([disabled]), +prm-gallery-collection .collection-folder .item-actions button:focus:not([disabled]), +prm-gallery-collection .collection-folder .item-actions button:hover:not([disabled]) { + color: #fff; + background-color: #046b99; +} + +/*****************************************************************************/ +/* Top nav bar */ +/*****************************************************************************/ +/* The very top of the page is tha BL standard header, loaded via AngularJS on BL_prmTopbarBefore*/ +/* There is a BL logo on that top banner so we hide the logo provided through the Primo back office */ + +/* hide the usual logo */ +prm-topbar #banner { + display: none; +} + + +/*Overriding some styles on the BL standard header */ + +#header-scrape .header__util { + height: 61px; +} + +#header-scrape .beta-logo { + background: url("../img/beta-beige-71x40.jpg") no-repeat; + width: 71px !important; + height: 40px !important; + background-size: 71px 40px; + image-rendering: -webkit-optimize-contrast; + margin-left: 15px; +} + + +/* We've added a logo saying 'beta service' to the standard BL header +/* the background is beige. text is red */ + +.beta-service-link a { + background: #f0e6da; + color: #e24b54 !important; + display: block; + font-size: 1.2em; + width: 65px; + height: 40px; + text-align: center; + padding: 6px 0; + margin-left: 14px; + border-block: none; + cursor: pointer; + font-weight: 600; +} + +.beta-service-link a:hover { + box-shadow: none; +} + +.beta-right { + margin-left: 40px; +} + + +/*Hide some parts of the BL standard header*/ +.search-container { + display: none +} + +.nav-secondary { + display: none +} + +/*hide login mobile*/ +#header-scrape .login-mobile { + display: none; +} + + + +/*****************/ +/* Top nav bar */ +/*****************/ + +/*This makes the nav bar sit in the middle of the page, aligned with under the header*/ +.top-nav-bar { + max-width: 976px; + margin: 0 auto; +} + +/*This spacer on the left of the bar doesn't have a style so we have to select it using its place as first flex-50 inside the topbar*/ +/*Make it smaller to move the 'Home' link to the left*/ +.top-nav-bar :nth-child(1 of .flex-50) { + max-width: 45px +} + + +.prm-secondary-color, +prm-search-bookmark-filter .md-button, +prm-topbar .top-nav-bar { + background: #1c304a; +} + + +/*reduce the height of the space so that it fits in the secondary nav of the standard header */ +prm-user-area-expandable { + height: 40px; +} + +/*Login button */ +/* Change it to white with blue text & hover */ +prm-topbar .md-button:not(.disable-hover) { + color: #ffffff !important; + background-color: #1c304a; + font-weight: 400 !important; + line-height: 1.5; + font-family: Arial, Helvetica, sans-serif; + font-size: 1rem !important; + white-space: nowrap; + height: 40px; + max-width: 14em; + /* border: thin solid #046b99;*/ +} + +prm-topbar .md-button:hover { + background-color: #046b99 !important; + color: #fff !important; + height: 40px; + /* box-shadow: 0 0 3px #046b99, 0 0 6px #fff, 0 0 9px #046b99;*/ +} + + +top-nav-bar-links .md-button:not(.disable-hover) { + color: #ffffff !important; + background-color: #1c304a; + font-weight: 400 !important; + line-height: 1.5; + font-family: Arial, Helvetica, sans-serif; + font-size: 1rem !important; + white-space: nowrap; + height: 40px; + /* border: thin solid #046b99;*/ +} + +top-nav-bar-links .md-button:hover { + background-color: #046b99 !important; + color: #fff !important; + white-space: nowrap; + height: 40px; + /* box-shadow: 0 0 3px #046b99, 0 0 6px #fff, 0 0 9px #046b99;*/ +} + +top-nav-bar-links .md-button:focus { + background-color: #046b99 !important; + color: #fff !important; + white-space: nowrap; + height: 40px; + /* box-shadow: 0 0 3px #046b99, 0 0 6px #fff, 0 0 9px #046b99;*/ +} + +/*style for the menu buttons?*/ +top-nav-bar-links .hoverable-over-dark:not(.disable-hover) { + color: #ffffff !important; + background-color: #1c304a; + font-weight: 400 !important; + line-height: 1.5; + font-family: Arial, Helvetica, sans-serif; + font-size: 1rem !important; + white-space: nowrap; + height: 40px; +} + + +/*working on this - removed .layout-full-height, from the style below*/ +prm-main-menu[menu-type=menu], +prm-main-menu[menu-type=menu] .top-nav-bar-links, +prm-topbar .md-button:not(.md-icon-button), +prm-topbar .top-nav-bar, +prm-user-area, +prm-user-area ._md-fab-toolbar-content, +prm-user-area ._md-fab-toolbar-wrapper, +prm-user-area .md-fab-action-item, +prm-user-area .md-fab-toolbar-content, +prm-user-area .md-fab-toolbar-wrapper, +prm-user-area .md-toolbar-tools, +prm-user-area md-fab-toolbar, +prm-user-area md-fab-trigger, +prm-user-area md-toolbar, +prm-user-area prm-authentication { + height: 40px; + min-height: 20px; +} + + +/*menu in mobile mode*/ +.layout-full-height { + height: 100% +} + +prm-main-menu[menu-type=full] .md-button, +prm-main-menu[menu-type=full] .md-button .md-headline, +prm-main-menu[menu-type=full] .overlay-menu-item .md-headline { + font-weight: 400; + font-size: 1em; + color: #000; +} + +/* Hides the Languages button in mobile view menu */ +prm-main-menu[menu-type=full] .overlay-menu-item { + display: none !important; +} + + +/* Pin next to login */ +prm-search-bookmark-filter { + height: 40px; +} + +prm-topbar prm-search-bookmark-filter a { + color: #ffffff !important; + background-color: #1c304a; +} + +/* Moves the Settings header down to show all topbar */ +body>primo-explore>div>prm-account>div { + padding-bottom: 142px; +} + +/* Resizes Personal Details box to show all content */ +#personalDetails { + min-width: 500px; + /* set a minimum width */ + min-height: 275px +} + +@media (max-width: 768px) { + #personalDetails { + min-width: unset; + /* remove the minimum width on smaller screens */ + } +} + +/* Hides Language selection in Personal Details section */ +body>primo-explore>div>prm-account>md-content>div.main.layout-row.flex>prm-personal-info>div.layout-row.layout-align-start-start>div.width-100.flex-xl-25.flex-md-30, +body>primo-explore>div>prm-account>md-content>div.main.layout-row.flex>prm-personal-info>div.layout-align-start-start.layout-column>div.width-100.flex-xl-25.flex-md-30 { + display: none; +} + +/* Hides the Library Card button in user menu */ +.my-library-card-ctm { + display: none; +} + +/* Hides the Languages button in user menu */ +.my-languages-ctm { + display: none; +} + +/*Hides the RefWorks button in user menu */ +.my-refworks-ctm { + display: none; +} + +.my-refworks-separator-ctm { + display: none; +} + +prm-library-card-menu { + display: none; +} + + +/* Remove the QR code button from topbar */ +#qrCodeScanner { + display: none; +} + +/* Selector for sub-headings in topbar 'show more' section */ +.md-subhead { + display: none; +} + + +/*****************************************************************************/ +/* Search boxes */ +/*****************************************************************************/ + +.prm-primary-bg.prm-hue1, +prm-atoz-search-bar.prm-hue1, +prm-browse-search-bar.prm-hue1, +prm-collection-gallery-header .prm-hue1.collection-header-inner, +prm-newspapers-search-bar.prm-hue1, +prm-search-bar.prm-hue1, +prm-spinner.prm-hue1.overlay-cover.light-on-dark:after, +prm-tags-search-bar.prm-hue1, +prm-tree-nav prm-spinner .prm-hue1.diamond { + background-color: #0D5257; +} + +/* Increase search box banner from 1.5 general padding, to 3em padding under and above main search box +SAM - reduced top padding from prm-search-bar as it interfered with the login button. Using margin instead. + +prm-atoz-search-bar, prm-browse-search-bar, prm-newspapers-search-bar, prm-search-bar, prm-tags-search-bar { + padding-bottom: 3em; padding-top: 3em; +} + +*/ + +prm-atoz-search-bar, +prm-browse-search-bar, +prm-newspapers-search-bar, +prm-search-bar, +prm-tags-search-bar { + padding-bottom: 3em; + padding-top: 3em; +} + +prm-search-bar { + /*width: 976px;*/ + padding-bottom: 3em; + padding-top: 1em; + /*margin: 200px auto 60px;*/ + margin: 200px 0 60px; + background: transparent !important; + /*height: 250px;*/ +} + +@media (max-width: 992px) { + prm-search-bar { + margin: 120px 0 0; + } +} + + +/* Change the search icon's colour +prm-search-bar .simple-search-wrapper .search-actions .md-button { + color:#fff; + background:#CF0303; +} +*/ +/* Change the background colour of the search icon on hover, currently black with 0.5 transparency +prm-search-bar .simple-search-wrapper .search-actions .md-button:hover { + background-color:rgba(0, 0, 0, 0.5); +} +*/ + +/*search buttons on advanced search +.md-button.button-confirm:hover:not([disabled]) { + color:#fff; + background:#CF0303 !important; +} +*/ +.md-button.button-confirm { + color: #fff; + background: #CF0303 !important; +} + +.md-button.button-confirm:hover { + color: #CF0303 !important; + background: #e9e9e9 !important; + /*background-color:rgba(0, 0, 0, 0.5);*/ +} + + +/* Enlarge Advanced search button, add border and glow on hover */ +.md-button.switch-to-advanced { + font-size: 120%; + border: thin solid #046b99; +} + +.md-button.switch-to-advanced:hover { + background-color: #046b99 !important; + color: #fff !important; + /*box-shadow: 0 0 3px #046b99, 0 0 6px #fff, 0 0 9px #046b99;*/ +} + +/* Enlarge Simple search button, add border and glow on hover */ +.md-button.switch-to-simple { + font-size: 120%; + border: thin solid #046b99; +} + +.md-button.switch-to-simple:hover { + color: #fff !important; + background-color: #046b99 !important; + /* box-shadow: 0 0 3px #046b99, 0 0 6px #fff, 0 0 9px #046b99;*/ +} + +/* Background colour to Advanced Search button white background with black text */ +prm-search-bar .search-switch-buttons .md-button.switch-to-advanced { + color: #046b99; + background-color: #ffffff; + border: thin solid #046b99; +} + +/* Colour and background of Simple search when on Advanced; white background with black text */ +prm-search-bar .search-switch-buttons .md-button.switch-to-simple { + color: #046b99; + background-color: #ffffff; + border: thin solid #046b99; +} + +/*Selector for Advanced search input boxes, setting background */ +.advanced-search-wrapper .inputs-row>md-input-container:last-child, +prm-search-bar .advanced-search-wrapper .inputs-row>md-input-container:last-child, +prm-tags-search-bar .advanced-search-wrapper .inputs-row>md-input-container:last-child { + background: #1c304a) none repeat scroll 0 0 !important; +} + +@media (max-width: 992px) { + primo-user-area { + display: none; + } +} + +/* Background of main search bar */ +/* +.prm-primary-bg, prm-atoz-search-bar, prm-browse-search-bar, prm-collection-gallery-header .collection-header-inner, prm-newspapers-search-bar, prm-search-bar, prm-spinner.overlay-cover.light-on-dark:after, prm-tags-search-bar { + background: #e9e9e9 url("../img/socam-hero-image-09-lower.jpg") no-repeat center top; +} +*/ +/* Background to Advanced Search */ +/*prm-search-bar .advanced-search-backdrop { + background: #e9e9e9 url("../img/socam-hero-image-09-lower.jpg") no-repeat center top; +} +*/ + +/*****************************************************************************/ +/* Home page */ +/*****************************************************************************/ + +/* QuickChat icon */ +.quick-chat-icon { + margin: auto; + width: 238px; + height: 86px; +} + +.column1 { + flex: 65; + layout: column; +} + +.column2 { + flex: 35; + layout: column; +} + + +.bl-catrow { + width: 100%; + display: inline-flex; +} + +.bl-catimagebox { + width: 100px; + padding-right: 16px; +} + +.bl-catimage { + width: 96px; + height: 54px; + background-size: 96px 54px; + image-rendering: -webkit-optimize-contrast; + display: block; +} + +.bl-cattxtbox { + width: 100%; +} + +.bl-cattitle { + color: #000; +} + +.bl-catdesc { + color: #000; +} + + +.bl-explore-advert { + content: url("../img/gates-at-the-british-library-st-pancras-building.jpg"); +} + +.bl-digiman-advert { + content: url("../img/illustration-from-the-ten-birth-tales-or-the-last-ten-jatakas.jpg"); +} + +.bl-illman-advert { + content: url("../img/detail-of-a-miniature-of-the-author-writing-his-book-roman-de-la-rose-f133.jpg"); +} + + + + + + + +/*********************************************************************/ +/* BRIEF RESULTS PAGE */ +/*********************************************************************/ + +/* record title */ +prm-brief-result .item-title span { + color: #1c304a; +} + +/*****************************************************************************/ +/* Facets */ +/*****************************************************************************/ + +/* Give background colour to facet titles */ +.section-title.md-button.md-primoExplore-theme.md-ink-ripple.layout-fill { + /* background: #1c304a - the dark one */ + /* background: #046b99 - the lighter one */ + background: #046b99 +} + +prm-facet-exact .section-title:focus, +prm-facet-exact .section-title:hover, +prm-facet-exact .section-title:active { + color: #046b99 !important; +} + + +/* Change the colour of the chevrons on the facet headers */ +prm-facet-exact .section-title prm-icon { + color: #fff; +} + +/* Change section header titles to white text */ +.section-title-header, +.sidebar-header { + color: #ffffff; +} + +.section-title-header:hover, +.sidebar-header:hover, +.sidebar-header:active { + color: #046b99 !important; +} + +/* Sort by text changed to white with section title, changing back to black as it has no background */ +h3.section-title-header { + color: #000; +} + +/*Give date input boxes a background colour */ +.ng-valid-min, +.ng-valid-max { + background: rgb(28, 48, 74, 0.1) none repeat scroll 0 0 !important; +} + +/* Add to facets to make text larger */ +.section-content .md-chips .md-chip.wrapping-chip { + font-size: 120%; +} + + +/*****************************************************************************/ +/* Full record */ +/*****************************************************************************/ + +/* Hide Tags section from full record – pane and left menu */ +div#tags { + display: none; +} + +[aria-label="Tags"] { + display: none; +} + +/* Remove BibTex and E-Mail from Send To options */ +#BibTeXPushTo { + display: none; +} + +#E-mail { + display: none; +} + +/*RE-order the Center Sections */ +#full-view-container>*:first-child { + display: flex; + flex-direction: column; +} + +#details { + order: 1; + margin-top: -2em; +} + +#links { + order: 2; +} + +#getit_link1_0 { + order: 3; +} + +#getit_link1_1 { + order: 4; + margin-bottom: -2em; +} + +#getit_link2 { + order: 5; +} + +#action_list { + order: 6; +} + +/*re-order the Left Navigation on the Detailed Page */ +[aria-label="Details"] { + order: 1 !important; +} + +[aria-label="Links"] { + order: 2 !important; +} + +[aria-label="Online access"] { + order: 3 !important; +} + +[aria-label="Physical item"] { + order: 4 !important; +} + +[aria-label="No orderable item"] { + order: 4 !important; +} + +[aria-label="I want this"] { + order: 5 !important; +} + +[aria-label="Send to"] { + order: 6 !important; +} + +/*prm-opac { display: none}*/ +prm-requests-services-ovl { + display: none +} + +/*prm-view-online { display: none}*/ + +.bl-tab1-content { + font-size: 15px; +} + +/*****************************************************************************/ +/* Settings page */ +/*****************************************************************************/ + + + +/*****************************************************************************/ +/* Footer */ +/*****************************************************************************/ + +/* Footer box and link colour */ +.grid_61 { + width: 100% +} + +.new-footer #page-footer { + background-color: #131313; + float: left; + width: 100% +} + +.new-footer #page-footer, +.new-footer #page-footer a { + color: #a09c9d +} + +.new-footer #page-footer .footer-block-inner { + display: block; + min-height: 100px +} + +.new-footer #page-footer .footer-block-inner .menu-block ul li a { + border-left: 1px solid #4e4e4e; + display: inline-block; + font-size: 12px; + font-size: .8rem; + margin-top: 0; + padding: 0 25px +} + +.new-footer #page-footer .footer-block-inner .menu-block ul li.desktop a, +.new-footer #page-footer .footer-block-inner .menu-block ul li.mobile a, +.new-footer #page-footer .footer-block-inner .menu-block ul li.noleftborder a { + border-left: 0 none; + padding-left: 0 +} + +.h-menu { + overflow: hidden +} + +.h-menu li { + float: left +} + +.footer-block .text-block p:last-of-type { + padding-left: 16px; + text-align: center; +} + +.footer-block-inner, +.main-content-block-inner { + position: relative; + float: none; + display: block; + margin: 0 auto +} + +/* This takes away the bullet points in bottom list */ +footer .menu-block ul.h-menu { + padding-top: 1rem; + display: grid; + justify-content: center; + grid-auto-flow: column; + list-style: none; +} + +/*****************************************************************************/ +/* Availability section */ +/*****************************************************************************/ + + +/* change the colour of the Online Access link to black */ +/*.not_restricted { + color: #000; +} +*/ + +/* message specific to records without an orderable item*/ +/* These links shouldn't actually exist any more */ +/* giving it a display none to fix a problem in the normalisation */ +.bl-no-item { + color: #000000 !important; + position: relative; + z-index: 100; + display: none +} + + +/* Browse this collection link*/ +.bl-browse-hierarchy { + color: #1c304a !important; +} + +.bl-browse-hierarchy a:hover { + border-bottom-width: 0px !important; + color: #1c304a !important +} + +/* hierarchy icon */ +.avail-img { + max-width: 17px; + vertical-align: bottom; + position: relative; + margin: 0 0px; + z-index: 100; +} + +.not_restricted, +[class*=check_] { + color: #1c304a !important; +} + +/*****************************************************************************/ +/* Title bar - appears below the logo+menus bar. Used in prm-topbar-after */ +/*****************************************************************************/ +/* !!! The title bar element is currently completely hidden, so most of this section is moot */ + + +/* Explore Archives and Manuscripts text*/ +.horizontal-bg-container-identity h2 { + font-size: 1.625rem; + font-weight: 400; + line-height: 1.5; + color: #fff; +} + +h2.identity-text-main { + font-size: 1.625rem; + font-weight: 400; + line-height: 1.5; + color: #fff; +} + +@media (max-width: 450px) { + h2.identity-text-main { + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #fff; + } +} + +.bl-top-row { + position: relative; + margin: 0 auto; +} + +.bl-title.container { + position: relative; + margin: 0 auto; +} + +.bl-title.container.container-header { + padding: 0; +} + +.bl-title.container.container-identity { + padding: 0; +} + +.bl-title.container { + position: relative; + margin: 0 auto; +} + +.bl-title.container.container-header, +.bl-title.container.container-identity { + padding: 0; + border-top: 2px solid white; +} + +.bl-title.container.main-nav-container-inner { + overflow-y: visible; +} + +.bl-title.identity { + padding: 2px 0px 1px; + margin-left: 1em; +} + +/*This is the main style for the bar with the title*/ +.horizontal-bg-container-identity { + background-color: #046b99; + height: 50px; +} + +@media (max-width: 450px) { + .horizontal-bg-container-identity { + background-color: #046b99; + height: 30px; + } +} + +/* We don't have a link at the moment, but if we do...*/ +h2.bl-title.identity-text-main a { + color: #333; + font-weight: lighter; +} + +h2.bl-title.identity-text-main a:hover { + background: transparent; + box-shadow: none; +} + +h2.identity-text-main a { + font-family: Arial, sans-serif; + font-size: 18px; +} + +/*****************************************************************************/ +/* Footer */ +/*****************************************************************************/ + +.footer-column { + flex: 100; + layout: column; + color: #a09c9d; + background-color: #131313; + float: left; + width: 100% +} + +.footer-column a { + color: #a09c9d +} + + +/* Footer box and link colour */ + + +.grid_61 { + width: 100% +} + +.new-footer #page-footer { + background-color: #131313; + float: left; + width: 100% +} + +.new-footer #page-footer, +.new-footer #page-footer a { + color: #a09c9d +} + +.new-footer #page-footer .footer-block-inner { + display: block; + min-height: 100px +} + +.new-footer #page-footer .footer-block-inner .menu-block ul li a { + border-left: 1px solid #4e4e4e; + display: inline-block; + font-size: 12px; + font-size: .8rem; + margin-top: 0; + padding: 0 25px +} + +.new-footer #page-footer .footer-block-inner .menu-block ul li.desktop a, +.new-footer #page-footer .footer-block-inner .menu-block ul li.mobile a, +.new-footer #page-footer .footer-block-inner .menu-block ul li.noleftborder a { + border-left: 0 none; + padding-left: 0 +} + +.h-menu { + overflow: hidden +} + +.h-menu li { + float: left +} + +.footer-block .text-block p:last-of-type { + padding-left: 16px; + text-align: center; +} + +.footer-block-inner, +.main-content-block-inner { + position: relative; + float: none; + display: block; + margin: 0 auto +} + +/* This takes away the bullet points in bottom list */ + +footer .menu-block ul.h-menu { + padding-top: 1rem; + display: grid; + justify-content: center; + grid-auto-flow: column; + list-style: none; +} \ No newline at end of file diff --git a/primo-explore/custom/IAMS_VU2/css/footer.css b/primo-explore/custom/IAMS_VU2/css/footer.css new file mode 100644 index 000000000..6ddee762e --- /dev/null +++ b/primo-explore/custom/IAMS_VU2/css/footer.css @@ -0,0 +1,141 @@ +#footer-static { + background-color: #000; + line-height: 1.375rem; + font-size: .875rem; + font-weight: 400; + color: #757575; + font-family: Arial,Helvetica,sans-serif +} + +#footer-static *,#footer-static:before,#footer-static:after { + box-sizing: border-box; + box-shadow: none; +} + +#footer-static .footer .container { + padding: 40px 45px 20px; + max-width: 880px; + margin: 0 auto +} + +@media (min-width: 992px) { + #footer-static .footer .container { + padding:40px 15px 20px + } +} + +#footer-static .footer ul { + list-style: none; + padding: 0 +} + +#footer-static .footer h5 { + font-size: 1.125rem; + color: #fff; + margin: 0 0 8px +} + +#footer-static .footer__navigation { + display: flex; + flex-direction: column +} + +@media (min-width: 768px) { + #footer-static .footer__navigation { + flex-direction:row; + flex-wrap: wrap + } +} + +@media (min-width: 992px) { + #footer-static .footer__navigation { + flex-wrap:nowrap + } +} + +#footer-static .footer__navigation a { + display: block; + padding: 5px 0; + font-size: .875rem; + line-height: 1.375rem +} + +#footer-static .footer__col { + width: 100%; + max-width: 100% +} + +@media (min-width: 768px) { + #footer-static .footer__col { + max-width:50% + } +} + +@media (min-width: 992px) { + #footer-static .footer__col { + max-width:25% + } +} + +#footer-static .footer__legal { + display: flex; + flex-direction: column +} + +@media (min-width: 768px) { + #footer-static .footer__legal { + flex-direction:row; + justify-content: center + } +} + +#footer-static .footer__legal a { + font-size: .875rem; + line-height: 1.313rem; + padding: 5px 0; + display: block; + text-align: left +} + +@media (min-width: 768px) { + #footer-static .footer__legal a { + border-left:1px solid #495057; + text-align: center; + padding: 0 12px + } +} + +@media (min-width: 992px) { + #footer-static .footer__legal a { + padding:0 18px + } +} + +@media (min-width: 768px) { + #footer-static .footer__legal li:first-of-type a { + border:none + } +} + +#footer-static .footer__copyright { + font-size: .875rem; + line-height: 1.313rem; + margin-top: 15px; + text-align: left +} + +@media (min-width: 768px) { + #footer-static .footer__copyright { + text-align:center + } +} + +#footer-static a { + text-decoration: none; + color: #999 +} + +#footer-static a:hover { + text-decoration: underline; +} + diff --git a/primo-explore/custom/IAMS_VU2/css/header.css b/primo-explore/custom/IAMS_VU2/css/header.css new file mode 100644 index 000000000..0c88babd0 --- /dev/null +++ b/primo-explore/custom/IAMS_VU2/css/header.css @@ -0,0 +1,627 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ +html { + line-height: 1.15; + -webkit-text-size-adjust: 100% +} + +body { + margin: 0 +} + +main { + display: block +} + +h1 { + font-size: 2em; + margin: .67em 0 +} + +hr { + box-sizing: content-box; + height: 0; + overflow: visible +} + +pre { + font-family: monospace,monospace; + font-size: 1em +} + +a { + background-color: transparent +} + +abbr[title] { + border-bottom: none; + text-decoration: underline; + text-decoration: underline dotted +} + +b,strong { + font-weight: bolder +} + +code,kbd,samp { + font-family: monospace,monospace; + font-size: 1em +} + +small { + font-size: 80% +} + +sub,sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline +} + +sub { + bottom: -.25em +} + +sup { + top: -.5em +} + +img { + border-style: none +} + +button,input,optgroup,select,textarea { + font-family: inherit; + font-size: 100%; + line-height: 1.15; + margin: 0 +} + +button,input { + overflow: visible +} + +button,select { + text-transform: none +} + +button,[type=button],[type=reset],[type=submit] { + -webkit-appearance: button +} + +button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner { + border-style: none; + padding: 0 +} + +button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring { + outline: 1px dotted ButtonText +} + +fieldset { + padding: .35em .75em .625em +} + +legend { + box-sizing: border-box; + color: inherit; + display: table; + max-width: 100%; + padding: 0; + white-space: normal +} + +progress { + vertical-align: baseline +} + +textarea { + overflow: auto +} + +[type=checkbox],[type=radio] { + box-sizing: border-box; + padding: 0 +} + +[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button { + height: auto +} + +[type=search] { + -webkit-appearance: textfield; + outline-offset: -2px +} + +[type=search]::-webkit-search-decoration { + -webkit-appearance: none +} + +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit +} + +details { + display: block +} + +summary { + display: list-item +} + +template { + display: none +} + +[hidden] { + display: none +} + +#header-scrape { + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + font-family: Arial,Helvetica,sans-serif; + color: #fff +} + +#header-scrape *,#header-scrape :after,#header-scrape :before { + box-sizing: border-box +} + +#header-scrape .header { + background: #1c304a; + border-bottom: 1px solid #fff; + display: none +} + +@media (min-width: 992px) { + #header-scrape .header { + display:flex + } +} + +#header-scrape .header .nav { + border-top: 1px solid #fff; + justify-content: space-between +} + +#header-scrape .header .nav a { + transition: background-color .15s ease-in-out; + border-right: 1px solid #fff +} + +#header-scrape .header .nav a:hover { + background-color: #000 +} + +#header-scrape .header__top { + width: 100%; + flex-direction: column; + display: flex +} + +#header-scrape .header__util { + display: flex; + justify-content: space-between; + align-items: center; + width: 100% +} + +#header-scrape .login { + display: flex; + align-items: center +} + +#header-scrape .login-mobile { + background: #000; + display: flex; + justify-content: flex-end +} + +@media (min-width: 992px) { + #header-scrape .login-mobile { + background:none; + display: none + } +} + +#header-scrape .login-link { + color: #fff; + display: block; + padding: 4px 12px +} + +#header-scrape .login-link:hover { + text-decoration: underline +} + +@media (min-width: 992px) { + #header-scrape .login-link { + padding:17px 12px + } +} + +#header-scrape .site-logo__link { + display: block; + z-index: 1; + position: relative +} + +#header-scrape .site-logo__link img { + display: block +} + +#header-scrape .nav { + display: flex; + padding-left: 0; + margin: 0; + list-style: none; + overflow: hidden +} + +#header-scrape .nav-more { + padding-left: 0; + margin: 0; + list-style: none; + position: relative +} + +#header-scrape .nav-secondary { + background: #1c304a; + max-width: 100% +} + +#header-scrape .nav-secondary-nav { + display: flex +} + +#header-scrape .nav-secondary a { + white-space: nowrap; + padding: 8px 15px; + transition: background-color .15s ease-in-out +} + +#header-scrape .nav-secondary a:hover { + background-color: #046b99 +} + +#header-scrape .nav-secondary a.nav-item--more_link { + padding-right: 35px; + position: relative +} + +#header-scrape .nav-secondary a.nav-item--more_link.open { + background: #046b99 +} + +#header-scrape .nav-secondary a.nav-item--more_link.open:after { + transform: translateY(3px) rotate(225deg) +} + +#header-scrape .nav-secondary a.nav-item--more_link:after { + content: ""; + height: 10px; + width: 10px; + display: inline-block; + vertical-align: middle; + border-right: 2px solid #fff; + border-bottom: 2px solid #fff; + position: absolute; + top: 0; + bottom: 0; + right: 15px; + margin: auto 0; + transform: translateY(-3px) rotate(45deg) +} + +#header-scrape .nav-item a { + color: #fff; + padding: 8px 15px; + display: block; + font-size: 1rem; + text-align: center +} + +#header-scrape .nav-item:last-child a { + border-right: none +} + +#header-scrape .nav-toggle { + border: none; + outline: none; + background: none +} + +#header-scrape .nav-toggle__line { + width: 30px; + height: 4px; + position: relative; + background: #fff +} + +#header-scrape .nav-toggle__line:before,#header-scrape .nav-toggle__line:after { + content: ""; + display: block; + width: 30px; + height: 4px; + background: #fff +} + +#header-scrape .nav-toggle__line:before { + position: absolute; + top: -11px +} + +#header-scrape .nav-toggle__line:after { + position: absolute; + top: 11px +} + +@media (min-width: 992px) { + #header-scrape .nav-toggle { + display:none + } +} + +#header-scrape .nav-expander { + position: relative; + z-index: 99999 +} + +#header-scrape .nav-mobile { + overflow-y: scroll; + position: fixed; + width: 100%; + height: 100%; + background: #000; + top: 0; + left: 0; + z-index: 99999 +} + +#header-scrape .nav-mobile__top { + display: flex; + justify-content: space-between; + padding: 0 30px 0 0 +} + +#header-scrape .nav-mobile .search-container { + padding: 15px +} + +#header-scrape .nav-mobile .search-container .form-row { + flex-direction: column +} + +@media (min-width: 992px) { + #header-scrape .nav-mobile { + display:none + } +} + +#header-scrape .link-expander { + color: #000; + background: #fff; + position: absolute; + z-index: -100; + top: -1000px +} + +#header-scrape .more-menu { + display: block; + position: absolute; + right: 0; + background: #046b99; + z-index: 99999; + padding-left: 0; + margin: 0; + list-style: none +} + +#header-scrape .more-menu .nav-item a { + text-align: right; + padding: 8px 25px +} + +#header-scrape .more-menu.hidden { + display: none +} + +#header-scrape .search-container { + margin-left: auto; + padding-right: 28px +} + +#header-scrape .search-button { + background-color: #046b99; + border-color: #046b99; + color: #fff; + padding: .375rem 1rem; + border: none; + outline: none; + display: flex; + align-items: center; + min-height: 100% +} + +#header-scrape .search-button:hover { + background-color: #1c304a; + border-color: #1c304a +} + +#header-scrape .search-button svg { + width: 18px; + height: 18px +} + +#header-scrape .input-group { + display: flex +} + +#header-scrape .form-group { + display: flex; + align-items: center +} + +#header-scrape .form-group label { + padding-right: 15px +} + +@media (min-width: 992px) { + #header-scrape .form-group label { + padding-right:0; + padding-left: 15px + } +} + +#header-scrape .form-inputs { + display: flex; + margin-top: 15px +} + +@media (min-width: 992px) { + #header-scrape .form-inputs { + margin-top:0 + } +} + +#header-scrape .form-control { + min-height: 38px; + font-size: 16px; + color: #757575; + border: none; + font-weight: 400; + line-height: 1.5; + padding: .375rem .75rem; + transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out +} + +#header-scrape .form-row { + display: flex +} + +#header-scrape .masthead { + background: #046b99; + display: flex; + align-items: center +} + +#header-scrape .masthead .masthead__content { + padding: 10px 30px; + display: flex; + width: 100%; + justify-content: space-between +} + +@media (min-width: 992px) { + #header-scrape .masthead .site-logo { + display:none + } +} + +#header-scrape .masthead h1 { + font-size: 18px; + font-weight: 400; + line-height: 1.5; + margin: 0 +} + +@media (min-width: 992px) { + #header-scrape .masthead h1 { + font-size:26px + } +} + +#header-scrape .accordion .title { + border-top: 1px solid #757575; + border-bottom: 1px solid #757575; + padding: 10px 20px; + position: relative +} + +#header-scrape .accordion .title a { + color: #fff +} + +#header-scrape .accordion__button .title:after { + content: ""; + height: 10px; + width: 10px; + display: inline-block; + vertical-align: middle; + border-right: 2px solid #fff; + border-bottom: 2px solid #fff; + position: absolute; + right: 20px; + top: 0; + bottom: 0; + margin: auto 0 +} + +#header-scrape .accordion__button[aria-expanded=true] { + background: #1c304a; + position: relative +} + +#header-scrape .accordion__button[aria-expanded=true] .title { + border-bottom: none +} + +#header-scrape .accordion__button[aria-expanded=true]:before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 5px; + background: #fff +} + +#header-scrape .accordion__button[aria-expanded=true] .title:after { + transform: rotate(225deg) +} + +#header-scrape .accordion__button[aria-expanded=false] .title:after { + transform: rotate(45deg) +} + +#header-scrape .accordion__panel { + background: #1c304a +} + +#header-scrape .accordion__panel .accordion__panel { + background: #046b99 +} + +#header-scrape a { + text-decoration: none +} + +#header-scrape .separator { + color: #fff; + margin: 0 .25rem +} + +#header-scrape .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + white-space: nowrap; + border: 0 +} + +#header-scrape .container { + width: 100%; + margin: 0 auto; + max-width: 976px +} + +#header-scrape .box-shadow { + box-shadow: 0 2px 5px #0003 +} diff --git a/primo-explore/custom/IAMS_VU2/css/modal.css b/primo-explore/custom/IAMS_VU2/css/modal.css new file mode 100644 index 000000000..e2d15b889 --- /dev/null +++ b/primo-explore/custom/IAMS_VU2/css/modal.css @@ -0,0 +1,90 @@ +#new-version { + z-index: 75; + position: fixed; + left: 5rem; + right: auto; + top: auto; + /*bottom: 5rem;*/ + width: auto; + width: calc(100% - 2rem); + max-width: 35rem; + height: auto; + /*max-height: 80vh;*/ + overflow-y: auto; + border: 0.05rem solid #000; + box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.5); + box-sizing: border-box; + margin: 0; + padding: 1rem; + background-color: #fff; + color: #252320; + line-height: 24px; +} + + +#new-version .advice-body { + position: relative; + margin: 0; + padding: 0; + background-color: inherit; + color: inherit; + border: none; + overflow-y: auto; +} + +#new-version .advice-body .heading { + margin: 0; + padding: 0.5rem 0 0.5rem 0; + color: #3c1053; +} +.dialog-button-row { + padding: 0 0 1rem 0; +} + + +#new-version .advice-body .teaser p { + margin: 0; + padding: 0; +} + +#new-version .advice-body .additional p { + margin: 0; + padding: 1rem 0 0 0; +} + +#new-version button.dialog-button { + background-color: #046b99; + top: 0; + right: 0; + border: none; + color: #fff; + cursor: pointer; + margin: 0; +} + +#new-version button.dialog-button:hover { + background-color: red; +} + +#new-version button.more { + background-color: #fff; + position: absolute; + bottom: 0; + right: 0; + border: none; + color: inherit; + cursor: pointer; + padding: 0; + margin: 0; +} + +#new-version button.more::before { +/* spacer which fades the last words of teaser (if necessary) to make the More... button clearly visible */ + content: ''; + background: linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(255,255,255,0.8) 66%, rgba(255,255,255,1) 100%); + display: inline-block; + height: 1rem; + left: -10rem; + position: absolute; + width: 10rem; +} \ No newline at end of file diff --git a/tests/angular-mocks.js b/tests/angular-mocks.js new file mode 100644 index 000000000..47d501804 --- /dev/null +++ b/tests/angular-mocks.js @@ -0,0 +1,3757 @@ +/** + * @license AngularJS v1.8.3 + * (c) 2010-2020 Google LLC. http://angularjs.org + * License: MIT + */ +(function(window, angular) { + + 'use strict'; + + /* global routeToRegExp: true */ + + /** + * @param {string} path - The path to parse. (It is assumed to have query and hash stripped off.) + * @param {Object} opts - Options. + * @return {Object} - An object containing an array of path parameter names (`keys`) and a regular + * expression (`regexp`) that can be used to identify a matching URL and extract the path + * parameter values. + * + * @description + * Parses the given path, extracting path parameter names and a regular expression to match URLs. + * + * Originally inspired by `pathRexp` in `visionmedia/express/lib/utils.js`. + */ + function routeToRegExp(path, opts) { + var keys = []; + + var pattern = path + .replace(/([().])/g, '\\$1') + .replace(/(\/)?:(\w+)(\*\?|[?*])?/g, function(_, slash, key, option) { + var optional = option === '?' || option === '*?'; + var star = option === '*' || option === '*?'; + keys.push({name: key, optional: optional}); + slash = slash || ''; + return ( + (optional ? '(?:' + slash : slash + '(?:') + + (star ? '(.+?)' : '([^/]+)') + + (optional ? '?)?' : ')') + ); + }) + .replace(/([/$*])/g, '\\$1'); + + if (opts.ignoreTrailingSlashes) { + pattern = pattern.replace(/\/+$/, '') + '/*'; + } + + return { + keys: keys, + regexp: new RegExp( + '^' + pattern + '(?:[?#]|$)', + opts.caseInsensitiveMatch ? 'i' : '' + ) + }; + } + + 'use strict'; + + /* global routeToRegExp: false */ + + /** + * @ngdoc object + * @name angular.mock + * @description + * + * Namespace from 'angular-mocks.js' which contains testing related code. + * + */ + angular.mock = {}; + + /** + * ! This is a private undocumented service ! + * + * @name $browser + * + * @description + * This service is a mock implementation of {@link ng.$browser}. It provides fake + * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, + * cookies, etc. + * + * The api of this service is the same as that of the real {@link ng.$browser $browser}, except + * that there are several helper methods available which can be used in tests. + */ + angular.mock.$BrowserProvider = function() { + this.$get = [ + '$log', '$$taskTrackerFactory', + function($log, $$taskTrackerFactory) { + return new angular.mock.$Browser($log, $$taskTrackerFactory); + } + ]; + }; + + angular.mock.$Browser = function($log, $$taskTrackerFactory) { + var self = this; + var taskTracker = $$taskTrackerFactory($log); + + this.isMock = true; + self.$$url = 'http://server/'; + self.$$lastUrl = self.$$url; // used by url polling fn + self.pollFns = []; + + // Task-tracking API + self.$$completeOutstandingRequest = taskTracker.completeTask; + self.$$incOutstandingRequestCount = taskTracker.incTaskCount; + self.notifyWhenNoOutstandingRequests = taskTracker.notifyWhenNoPendingTasks; + + // register url polling fn + + self.onUrlChange = function(listener) { + self.pollFns.push( + function() { + if (self.$$lastUrl !== self.$$url || self.$$state !== self.$$lastState) { + self.$$lastUrl = self.$$url; + self.$$lastState = self.$$state; + listener(self.$$url, self.$$state); + } + } + ); + + return listener; + }; + + self.$$applicationDestroyed = angular.noop; + self.$$checkUrlChange = angular.noop; + + self.deferredFns = []; + self.deferredNextId = 0; + + self.defer = function(fn, delay, taskType) { + var timeoutId = self.deferredNextId++; + + delay = delay || 0; + taskType = taskType || taskTracker.DEFAULT_TASK_TYPE; + + taskTracker.incTaskCount(taskType); + self.deferredFns.push({ + id: timeoutId, + type: taskType, + time: (self.defer.now + delay), + fn: fn + }); + self.deferredFns.sort(function(a, b) { return a.time - b.time; }); + + return timeoutId; + }; + + + /** + * @name $browser#defer.now + * + * @description + * Current milliseconds mock time. + */ + self.defer.now = 0; + + + self.defer.cancel = function(deferId) { + var taskIndex; + + angular.forEach(self.deferredFns, function(task, index) { + if (task.id === deferId) taskIndex = index; + }); + + if (angular.isDefined(taskIndex)) { + var task = self.deferredFns.splice(taskIndex, 1)[0]; + taskTracker.completeTask(angular.noop, task.type); + return true; + } + + return false; + }; + + + /** + * @name $browser#defer.flush + * + * @description + * Flushes all pending requests and executes the defer callbacks. + * + * See {@link ngMock.$flushPendingsTasks} for more info. + * + * @param {number=} number of milliseconds to flush. See {@link #defer.now} + */ + self.defer.flush = function(delay) { + var nextTime; + + if (angular.isDefined(delay)) { + // A delay was passed so compute the next time + nextTime = self.defer.now + delay; + } else if (self.deferredFns.length) { + // No delay was passed so set the next time so that it clears the deferred queue + nextTime = self.deferredFns[self.deferredFns.length - 1].time; + } else { + // No delay passed, but there are no deferred tasks so flush - indicates an error! + throw new Error('No deferred tasks to be flushed'); + } + + while (self.deferredFns.length && self.deferredFns[0].time <= nextTime) { + // Increment the time and call the next deferred function + self.defer.now = self.deferredFns[0].time; + var task = self.deferredFns.shift(); + taskTracker.completeTask(task.fn, task.type); + } + + // Ensure that the current time is correct + self.defer.now = nextTime; + }; + + /** + * @name $browser#defer.getPendingTasks + * + * @description + * Returns the currently pending tasks that need to be flushed. + * You can request a specific type of tasks only, by specifying a `taskType`. + * + * @param {string=} taskType - The type tasks to return. + */ + self.defer.getPendingTasks = function(taskType) { + return !taskType + ? self.deferredFns + : self.deferredFns.filter(function(task) { return task.type === taskType; }); + }; + + /** + * @name $browser#defer.formatPendingTasks + * + * @description + * Formats each task in a list of pending tasks as a string, suitable for use in error messages. + * + * @param {Array} pendingTasks - A list of task objects. + * @return {Array} A list of stringified tasks. + */ + self.defer.formatPendingTasks = function(pendingTasks) { + return pendingTasks.map(function(task) { + return '{id: ' + task.id + ', type: ' + task.type + ', time: ' + task.time + '}'; + }); + }; + + /** + * @name $browser#defer.verifyNoPendingTasks + * + * @description + * Verifies that there are no pending tasks that need to be flushed. + * You can check for a specific type of tasks only, by specifying a `taskType`. + * + * See {@link $verifyNoPendingTasks} for more info. + * + * @param {string=} taskType - The type tasks to check for. + */ + self.defer.verifyNoPendingTasks = function(taskType) { + var pendingTasks = self.defer.getPendingTasks(taskType); + + if (pendingTasks.length) { + var formattedTasks = self.defer.formatPendingTasks(pendingTasks).join('\n '); + throw new Error('Deferred tasks to flush (' + pendingTasks.length + '):\n ' + + formattedTasks); + } + }; + + self.$$baseHref = '/'; + self.baseHref = function() { + return this.$$baseHref; + }; + }; + angular.mock.$Browser.prototype = { + + /** + * @name $browser#poll + * + * @description + * run all fns in pollFns + */ + poll: function poll() { + angular.forEach(this.pollFns, function(pollFn) { + pollFn(); + }); + }, + + url: function(url, replace, state) { + if (angular.isUndefined(state)) { + state = null; + } + if (url) { + // The `$browser` service trims empty hashes; simulate it. + this.$$url = url.replace(/#$/, ''); + // Native pushState serializes & copies the object; simulate it. + this.$$state = angular.copy(state); + return this; + } + + return this.$$url; + }, + + state: function() { + return this.$$state; + } + }; + + /** + * @ngdoc service + * @name $flushPendingTasks + * + * @description + * Flushes all currently pending tasks and executes the corresponding callbacks. + * + * Optionally, you can also pass a `delay` argument to only flush tasks that are scheduled to be + * executed within `delay` milliseconds. Currently, `delay` only applies to timeouts, since all + * other tasks have a delay of 0 (i.e. they are scheduled to be executed as soon as possible, but + * still asynchronously). + * + * If no delay is specified, it uses a delay such that all currently pending tasks are flushed. + * + * The types of tasks that are flushed include: + * + * - Pending timeouts (via {@link $timeout}). + * - Pending tasks scheduled via {@link ng.$rootScope.Scope#$applyAsync $applyAsync}. + * - Pending tasks scheduled via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}. + * These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises). + * + *
+ * Periodic tasks scheduled via {@link $interval} use a different queue and are not flushed by + * `$flushPendingTasks()`. Use {@link ngMock.$interval#flush $interval.flush(millis)} instead. + *
+ * + * @param {number=} delay - The number of milliseconds to flush. + */ + angular.mock.$FlushPendingTasksProvider = function() { + this.$get = [ + '$browser', + function($browser) { + return function $flushPendingTasks(delay) { + return $browser.defer.flush(delay); + }; + } + ]; + }; + + /** + * @ngdoc service + * @name $verifyNoPendingTasks + * + * @description + * Verifies that there are no pending tasks that need to be flushed. It throws an error if there are + * still pending tasks. + * + * You can check for a specific type of tasks only, by specifying a `taskType`. + * + * Available task types: + * + * - `$timeout`: Pending timeouts (via {@link $timeout}). + * - `$http`: Pending HTTP requests (via {@link $http}). + * - `$route`: In-progress route transitions (via {@link $route}). + * - `$applyAsync`: Pending tasks scheduled via {@link ng.$rootScope.Scope#$applyAsync $applyAsync}. + * - `$evalAsync`: Pending tasks scheduled via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}. + * These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises). + * + *
+ * Periodic tasks scheduled via {@link $interval} use a different queue and are not taken into + * account by `$verifyNoPendingTasks()`. There is currently no way to verify that there are no + * pending {@link $interval} tasks. + *
+ * + * @param {string=} taskType - The type of tasks to check for. + */ + angular.mock.$VerifyNoPendingTasksProvider = function() { + this.$get = [ + '$browser', + function($browser) { + return function $verifyNoPendingTasks(taskType) { + return $browser.defer.verifyNoPendingTasks(taskType); + }; + } + ]; + }; + + /** + * @ngdoc provider + * @name $exceptionHandlerProvider + * + * @description + * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors + * passed to the `$exceptionHandler`. + */ + + /** + * @ngdoc service + * @name $exceptionHandler + * + * @description + * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed + * to it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration + * information. + * + * + * ```js + * describe('$exceptionHandlerProvider', function() { + * + * it('should capture log messages and exceptions', function() { + * + * module(function($exceptionHandlerProvider) { + * $exceptionHandlerProvider.mode('log'); + * }); + * + * inject(function($log, $exceptionHandler, $timeout) { + * $timeout(function() { $log.log(1); }); + * $timeout(function() { $log.log(2); throw 'banana peel'; }); + * $timeout(function() { $log.log(3); }); + * expect($exceptionHandler.errors).toEqual([]); + * expect($log.assertEmpty()); + * $timeout.flush(); + * expect($exceptionHandler.errors).toEqual(['banana peel']); + * expect($log.log.logs).toEqual([[1], [2], [3]]); + * }); + * }); + * }); + * ``` + */ + + angular.mock.$ExceptionHandlerProvider = function() { + var handler; + + /** + * @ngdoc method + * @name $exceptionHandlerProvider#mode + * + * @description + * Sets the logging mode. + * + * @param {string} mode Mode of operation, defaults to `rethrow`. + * + * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` + * mode stores an array of errors in `$exceptionHandler.errors`, to allow later assertion of + * them. See {@link ngMock.$log#assertEmpty assertEmpty()} and + * {@link ngMock.$log#reset reset()}. + * - `rethrow`: If any errors are passed to the handler in tests, it typically means that there + * is a bug in the application or test, so this mock will make these tests fail. For any + * implementations that expect exceptions to be thrown, the `rethrow` mode will also maintain + * a log of thrown errors in `$exceptionHandler.errors`. + */ + this.mode = function(mode) { + + switch (mode) { + case 'log': + case 'rethrow': + var errors = []; + handler = function(e) { + if (arguments.length === 1) { + errors.push(e); + } else { + errors.push([].slice.call(arguments, 0)); + } + if (mode === 'rethrow') { + throw e; + } + }; + handler.errors = errors; + break; + default: + throw new Error('Unknown mode \'' + mode + '\', only \'log\'/\'rethrow\' modes are allowed!'); + } + }; + + this.$get = function() { + return handler; + }; + + this.mode('rethrow'); + }; + + + /** + * @ngdoc service + * @name $log + * + * @description + * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays + * (one array per logging level). These arrays are exposed as `logs` property of each of the + * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. + * + */ + angular.mock.$LogProvider = function() { + var debug = true; + + function concat(array1, array2, index) { + return array1.concat(Array.prototype.slice.call(array2, index)); + } + + this.debugEnabled = function(flag) { + if (angular.isDefined(flag)) { + debug = flag; + return this; + } else { + return debug; + } + }; + + this.$get = function() { + var $log = { + log: function() { $log.log.logs.push(concat([], arguments, 0)); }, + warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, + info: function() { $log.info.logs.push(concat([], arguments, 0)); }, + error: function() { $log.error.logs.push(concat([], arguments, 0)); }, + debug: function() { + if (debug) { + $log.debug.logs.push(concat([], arguments, 0)); + } + } + }; + + /** + * @ngdoc method + * @name $log#reset + * + * @description + * Reset all of the logging arrays to empty. + */ + $log.reset = function() { + /** + * @ngdoc property + * @name $log#log.logs + * + * @description + * Array of messages logged using {@link ng.$log#log `log()`}. + * + * @example + * ```js + * $log.log('Some Log'); + * var first = $log.log.logs.unshift(); + * ``` + */ + $log.log.logs = []; + /** + * @ngdoc property + * @name $log#info.logs + * + * @description + * Array of messages logged using {@link ng.$log#info `info()`}. + * + * @example + * ```js + * $log.info('Some Info'); + * var first = $log.info.logs.unshift(); + * ``` + */ + $log.info.logs = []; + /** + * @ngdoc property + * @name $log#warn.logs + * + * @description + * Array of messages logged using {@link ng.$log#warn `warn()`}. + * + * @example + * ```js + * $log.warn('Some Warning'); + * var first = $log.warn.logs.unshift(); + * ``` + */ + $log.warn.logs = []; + /** + * @ngdoc property + * @name $log#error.logs + * + * @description + * Array of messages logged using {@link ng.$log#error `error()`}. + * + * @example + * ```js + * $log.error('Some Error'); + * var first = $log.error.logs.unshift(); + * ``` + */ + $log.error.logs = []; + /** + * @ngdoc property + * @name $log#debug.logs + * + * @description + * Array of messages logged using {@link ng.$log#debug `debug()`}. + * + * @example + * ```js + * $log.debug('Some Error'); + * var first = $log.debug.logs.unshift(); + * ``` + */ + $log.debug.logs = []; + }; + + /** + * @ngdoc method + * @name $log#assertEmpty + * + * @description + * Assert that all of the logging methods have no logged messages. If any messages are present, + * an exception is thrown. + */ + $log.assertEmpty = function() { + var errors = []; + angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) { + angular.forEach($log[logLevel].logs, function(log) { + angular.forEach(log, function(logItem) { + errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + + (logItem.stack || '')); + }); + }); + }); + if (errors.length) { + errors.unshift('Expected $log to be empty! Either a message was logged unexpectedly, or ' + + 'an expected log message was not checked and removed:'); + errors.push(''); + throw new Error(errors.join('\n---------\n')); + } + }; + + $log.reset(); + return $log; + }; + }; + + + /** + * @ngdoc service + * @name $interval + * + * @description + * Mock implementation of the $interval service. + * + * Use {@link ngMock.$interval#flush `$interval.flush(millis)`} to + * move forward by `millis` milliseconds and trigger any functions scheduled to run in that + * time. + * + * @param {function()} fn A function that should be called repeatedly. + * @param {number} delay Number of milliseconds between each function call. + * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat + * indefinitely. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @param {...*=} Pass additional parameters to the executed function. + * @returns {promise} A promise which will be notified on each iteration. + */ + angular.mock.$IntervalProvider = function() { + this.$get = ['$browser', '$$intervalFactory', + function($browser, $$intervalFactory) { + var repeatFns = [], + nextRepeatId = 0, + now = 0, + setIntervalFn = function(tick, delay, deferred, skipApply) { + var id = nextRepeatId++; + var fn = !skipApply ? tick : function() { + tick(); + $browser.defer.flush(); + }; + + repeatFns.push({ + nextTime: (now + (delay || 0)), + delay: delay || 1, + fn: fn, + id: id, + deferred: deferred + }); + repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime; }); + + return id; + }, + clearIntervalFn = function(id) { + for (var fnIndex = repeatFns.length - 1; fnIndex >= 0; fnIndex--) { + if (repeatFns[fnIndex].id === id) { + repeatFns.splice(fnIndex, 1); + break; + } + } + }; + + var $interval = $$intervalFactory(setIntervalFn, clearIntervalFn); + + /** + * @ngdoc method + * @name $interval#cancel + * + * @description + * Cancels a task associated with the `promise`. + * + * @param {promise} promise A promise from calling the `$interval` function. + * @returns {boolean} Returns `true` if the task was successfully cancelled. + */ + $interval.cancel = function(promise) { + if (!promise) return false; + + for (var fnIndex = repeatFns.length - 1; fnIndex >= 0; fnIndex--) { + if (repeatFns[fnIndex].id === promise.$$intervalId) { + var deferred = repeatFns[fnIndex].deferred; + deferred.promise.then(undefined, function() {}); + deferred.reject('canceled'); + repeatFns.splice(fnIndex, 1); + return true; + } + } + + return false; + }; + + /** + * @ngdoc method + * @name $interval#flush + * @description + * + * Runs interval tasks scheduled to be run in the next `millis` milliseconds. + * + * @param {number} millis maximum timeout amount to flush up until. + * + * @return {number} The amount of time moved forward. + */ + $interval.flush = function(millis) { + var before = now; + now += millis; + while (repeatFns.length && repeatFns[0].nextTime <= now) { + var task = repeatFns[0]; + task.fn(); + if (task.nextTime === before) { + // this can only happen the first time + // a zero-delay interval gets triggered + task.nextTime++; + } + task.nextTime += task.delay; + repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;}); + } + return millis; + }; + + return $interval; + }]; + }; + + + function jsonStringToDate(string) { + // The R_ISO8061_STR regex is never going to fit into the 100 char limit! + // eslit-disable-next-line max-len + var R_ISO8061_STR = /^(-?\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; + + var match; + if ((match = string.match(R_ISO8061_STR))) { + var date = new Date(0), + tzHour = 0, + tzMin = 0; + if (match[9]) { + tzHour = toInt(match[9] + match[10]); + tzMin = toInt(match[9] + match[11]); + } + date.setUTCFullYear(toInt(match[1]), toInt(match[2]) - 1, toInt(match[3])); + date.setUTCHours(toInt(match[4] || 0) - tzHour, + toInt(match[5] || 0) - tzMin, + toInt(match[6] || 0), + toInt(match[7] || 0)); + return date; + } + return string; + } + + function toInt(str) { + return parseInt(str, 10); + } + + function padNumberInMock(num, digits, trim) { + var neg = ''; + if (num < 0) { + neg = '-'; + num = -num; + } + num = '' + num; + while (num.length < digits) num = '0' + num; + if (trim) { + num = num.substr(num.length - digits); + } + return neg + num; + } + + + /** + * @ngdoc type + * @name angular.mock.TzDate + * @description + * + * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. + * + * Mock of the Date type which has its timezone specified via constructor arg. + * + * The main purpose is to create Date-like instances with timezone fixed to the specified timezone + * offset, so that we can test code that depends on local timezone settings without dependency on + * the time zone settings of the machine where the code is running. + * + * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) + * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* + * + * @example + * !!!! WARNING !!!!! + * This is not a complete Date object so only methods that were implemented can be called safely. + * To make matters worse, TzDate instances inherit stuff from Date via a prototype. + * + * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is + * incomplete we might be missing some non-standard methods. This can result in errors like: + * "Date.prototype.foo called on incompatible Object". + * + * ```js + * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z'); + * newYearInBratislava.getTimezoneOffset() => -60; + * newYearInBratislava.getFullYear() => 2010; + * newYearInBratislava.getMonth() => 0; + * newYearInBratislava.getDate() => 1; + * newYearInBratislava.getHours() => 0; + * newYearInBratislava.getMinutes() => 0; + * newYearInBratislava.getSeconds() => 0; + * ``` + * + */ + angular.mock.TzDate = function(offset, timestamp) { + var self = new Date(0); + if (angular.isString(timestamp)) { + var tsStr = timestamp; + + self.origDate = jsonStringToDate(timestamp); + + timestamp = self.origDate.getTime(); + if (isNaN(timestamp)) { + // eslint-disable-next-line no-throw-literal + throw { + name: 'Illegal Argument', + message: 'Arg \'' + tsStr + '\' passed into TzDate constructor is not a valid date string' + }; + } + } else { + self.origDate = new Date(timestamp); + } + + var localOffset = new Date(timestamp).getTimezoneOffset(); + self.offsetDiff = localOffset * 60 * 1000 - offset * 1000 * 60 * 60; + self.date = new Date(timestamp + self.offsetDiff); + + self.getTime = function() { + return self.date.getTime() - self.offsetDiff; + }; + + self.toLocaleDateString = function() { + return self.date.toLocaleDateString(); + }; + + self.getFullYear = function() { + return self.date.getFullYear(); + }; + + self.getMonth = function() { + return self.date.getMonth(); + }; + + self.getDate = function() { + return self.date.getDate(); + }; + + self.getHours = function() { + return self.date.getHours(); + }; + + self.getMinutes = function() { + return self.date.getMinutes(); + }; + + self.getSeconds = function() { + return self.date.getSeconds(); + }; + + self.getMilliseconds = function() { + return self.date.getMilliseconds(); + }; + + self.getTimezoneOffset = function() { + return offset * 60; + }; + + self.getUTCFullYear = function() { + return self.origDate.getUTCFullYear(); + }; + + self.getUTCMonth = function() { + return self.origDate.getUTCMonth(); + }; + + self.getUTCDate = function() { + return self.origDate.getUTCDate(); + }; + + self.getUTCHours = function() { + return self.origDate.getUTCHours(); + }; + + self.getUTCMinutes = function() { + return self.origDate.getUTCMinutes(); + }; + + self.getUTCSeconds = function() { + return self.origDate.getUTCSeconds(); + }; + + self.getUTCMilliseconds = function() { + return self.origDate.getUTCMilliseconds(); + }; + + self.getDay = function() { + return self.date.getDay(); + }; + + // provide this method only on browsers that already have it + if (self.toISOString) { + self.toISOString = function() { + return padNumberInMock(self.origDate.getUTCFullYear(), 4) + '-' + + padNumberInMock(self.origDate.getUTCMonth() + 1, 2) + '-' + + padNumberInMock(self.origDate.getUTCDate(), 2) + 'T' + + padNumberInMock(self.origDate.getUTCHours(), 2) + ':' + + padNumberInMock(self.origDate.getUTCMinutes(), 2) + ':' + + padNumberInMock(self.origDate.getUTCSeconds(), 2) + '.' + + padNumberInMock(self.origDate.getUTCMilliseconds(), 3) + 'Z'; + }; + } + + //hide all methods not implemented in this mock that the Date prototype exposes + var unimplementedMethods = ['getUTCDay', + 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', + 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', + 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', + 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', + 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; + + angular.forEach(unimplementedMethods, function(methodName) { + self[methodName] = function() { + throw new Error('Method \'' + methodName + '\' is not implemented in the TzDate mock'); + }; + }); + + return self; + }; + + //make "tzDateInstance instanceof Date" return true + angular.mock.TzDate.prototype = Date.prototype; + + + /** + * @ngdoc service + * @name $animate + * + * @description + * Mock implementation of the {@link ng.$animate `$animate`} service. Exposes two additional methods + * for testing animations. + * + * You need to require the `ngAnimateMock` module in your test suite for instance `beforeEach(module('ngAnimateMock'))` + */ + angular.mock.animate = angular.module('ngAnimateMock', ['ng']) + .info({ angularVersion: '1.8.3' }) + + .config(['$provide', function($provide) { + + $provide.factory('$$forceReflow', function() { + function reflowFn() { + reflowFn.totalReflows++; + } + reflowFn.totalReflows = 0; + return reflowFn; + }); + + $provide.factory('$$animateAsyncRun', function() { + var queue = []; + var queueFn = function() { + return function(fn) { + queue.push(fn); + }; + }; + queueFn.flush = function() { + if (queue.length === 0) return false; + + for (var i = 0; i < queue.length; i++) { + queue[i](); + } + queue = []; + + return true; + }; + return queueFn; + }); + + $provide.decorator('$$animateJs', ['$delegate', function($delegate) { + var runners = []; + + var animateJsConstructor = function() { + var animator = $delegate.apply($delegate, arguments); + // If no javascript animation is found, animator is undefined + if (animator) { + runners.push(animator); + } + return animator; + }; + + animateJsConstructor.$closeAndFlush = function() { + runners.forEach(function(runner) { + runner.end(); + }); + runners = []; + }; + + return animateJsConstructor; + }]); + + $provide.decorator('$animateCss', ['$delegate', function($delegate) { + var runners = []; + + var animateCssConstructor = function(element, options) { + var animator = $delegate(element, options); + runners.push(animator); + return animator; + }; + + animateCssConstructor.$closeAndFlush = function() { + runners.forEach(function(runner) { + runner.end(); + }); + runners = []; + }; + + return animateCssConstructor; + }]); + + $provide.decorator('$animate', ['$delegate', '$timeout', '$browser', '$$rAF', '$animateCss', '$$animateJs', + '$$forceReflow', '$$animateAsyncRun', '$rootScope', + function($delegate, $timeout, $browser, $$rAF, $animateCss, $$animateJs, + $$forceReflow, $$animateAsyncRun, $rootScope) { + var animate = { + queue: [], + cancel: $delegate.cancel, + on: $delegate.on, + off: $delegate.off, + pin: $delegate.pin, + get reflows() { + return $$forceReflow.totalReflows; + }, + enabled: $delegate.enabled, + /** + * @ngdoc method + * @name $animate#closeAndFlush + * @description + * + * This method will close all pending animations (both {@link ngAnimate#javascript-based-animations Javascript} + * and {@link ngAnimate.$animateCss CSS}) and it will also flush any remaining animation frames and/or callbacks. + */ + closeAndFlush: function() { + // we allow the flush command to swallow the errors + // because depending on whether CSS or JS animations are + // used, there may not be a RAF flush. The primary flush + // at the end of this function must throw an exception + // because it will track if there were pending animations + this.flush(true); + $animateCss.$closeAndFlush(); + $$animateJs.$closeAndFlush(); + this.flush(); + }, + /** + * @ngdoc method + * @name $animate#flush + * @description + * + * This method is used to flush the pending callbacks and animation frames to either start + * an animation or conclude an animation. Note that this will not actually close an + * actively running animation (see {@link ngMock.$animate#closeAndFlush `closeAndFlush()`} for that). + */ + flush: function(hideErrors) { + $rootScope.$digest(); + + var doNextRun, somethingFlushed = false; + do { + doNextRun = false; + + if ($$rAF.queue.length) { + $$rAF.flush(); + doNextRun = somethingFlushed = true; + } + + if ($$animateAsyncRun.flush()) { + doNextRun = somethingFlushed = true; + } + } while (doNextRun); + + if (!somethingFlushed && !hideErrors) { + throw new Error('No pending animations ready to be closed or flushed'); + } + + $rootScope.$digest(); + } + }; + + angular.forEach( + ['animate','enter','leave','move','addClass','removeClass','setClass'], function(method) { + animate[method] = function() { + animate.queue.push({ + event: method, + element: arguments[0], + options: arguments[arguments.length - 1], + args: arguments + }); + return $delegate[method].apply($delegate, arguments); + }; + }); + + return animate; + }]); + + }]); + + + /** + * @ngdoc function + * @name angular.mock.dump + * @description + * + * *NOTE*: This is not an injectable instance, just a globally available function. + * + * Method for serializing common AngularJS objects (scope, elements, etc..) into strings. + * It is useful for logging objects to the console when debugging. + * + * @param {*} object - any object to turn into string. + * @return {string} a serialized string of the argument + */ + angular.mock.dump = function(object) { + return serialize(object); + + function serialize(object) { + var out; + + if (angular.isElement(object)) { + object = angular.element(object); + out = angular.element('
'); + angular.forEach(object, function(element) { + out.append(angular.element(element).clone()); + }); + out = out.html(); + } else if (angular.isArray(object)) { + out = []; + angular.forEach(object, function(o) { + out.push(serialize(o)); + }); + out = '[ ' + out.join(', ') + ' ]'; + } else if (angular.isObject(object)) { + if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { + out = serializeScope(object); + } else if (object instanceof Error) { + out = object.stack || ('' + object.name + ': ' + object.message); + } else { + // TODO(i): this prevents methods being logged, + // we should have a better way to serialize objects + out = angular.toJson(object, true); + } + } else { + out = String(object); + } + + return out; + } + + function serializeScope(scope, offset) { + offset = offset || ' '; + var log = [offset + 'Scope(' + scope.$id + '): {']; + for (var key in scope) { + if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) { + log.push(' ' + key + ': ' + angular.toJson(scope[key])); + } + } + var child = scope.$$childHead; + while (child) { + log.push(serializeScope(child, offset + ' ')); + child = child.$$nextSibling; + } + log.push('}'); + return log.join('\n' + offset); + } + }; + + /** + * @ngdoc service + * @name $httpBackend + * @description + * Fake HTTP backend implementation suitable for unit testing applications that use the + * {@link ng.$http $http service}. + * + *
+ * **Note**: For fake HTTP backend implementation suitable for end-to-end testing or backend-less + * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. + *
+ * + * During unit testing, we want our unit tests to run quickly and have no external dependencies so + * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or + * [JSONP](http://en.wikipedia.org/wiki/JSONP) requests to a real server. All we really need is + * to verify whether a certain request has been sent or not, or alternatively just let the + * application make requests, respond with pre-trained responses and assert that the end result is + * what we expect it to be. + * + * This mock implementation can be used to respond with static or dynamic responses via the + * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). + * + * When an AngularJS application needs some data from a server, it calls the $http service, which + * sends the request to a real server using $httpBackend service. With dependency injection, it is + * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify + * the requests and respond with some testing data without sending a request to a real server. + * + * There are two ways to specify what test data should be returned as http responses by the mock + * backend when the code under test makes http requests: + * + * - `$httpBackend.expect` - specifies a request expectation + * - `$httpBackend.when` - specifies a backend definition + * + * + * ## Request Expectations vs Backend Definitions + * + * Request expectations provide a way to make assertions about requests made by the application and + * to define responses for those requests. The test will fail if the expected requests are not made + * or they are made in the wrong order. + * + * Backend definitions allow you to define a fake backend for your application which doesn't assert + * if a particular request was made or not, it just returns a trained response if a request is made. + * The test will pass whether or not the request gets made during testing. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Request expectationsBackend definitions
Syntax.expect(...).respond(...).when(...).respond(...)
Typical usagestrict unit testsloose (black-box) unit testing
Fulfills multiple requestsNOYES
Order of requests mattersYESNO
Request requiredYESNO
Response requiredoptional (see below)YES
+ * + * In cases where both backend definitions and request expectations are specified during unit + * testing, the request expectations are evaluated first. + * + * If a request expectation has no response specified, the algorithm will search your backend + * definitions for an appropriate response. + * + * If a request didn't match any expectation or if the expectation doesn't have the response + * defined, the backend definitions are evaluated in sequential order to see if any of them match + * the request. The response from the first matched definition is returned. + * + * + * ## Flushing HTTP requests + * + * The $httpBackend used in production always responds to requests asynchronously. If we preserved + * this behavior in unit testing, we'd have to create async unit tests, which are hard to write, + * to follow and to maintain. But neither can the testing mock respond synchronously; that would + * change the execution of the code under test. For this reason, the mock $httpBackend has a + * `flush()` method, which allows the test to explicitly flush pending requests. This preserves + * the async api of the backend, while allowing the test to execute synchronously. + * + * + * ## Unit testing with mock $httpBackend + * The following code shows how to setup and use the mock backend when unit testing a controller. + * First we create the controller under test: + * + ```js + // The module code + angular + .module('MyApp', []) + .controller('MyController', MyController); + + // The controller code + function MyController($scope, $http) { + var authToken; + + $http.get('/auth.py').then(function(response) { + authToken = response.headers('A-Token'); + $scope.user = response.data; + }).catch(function() { + $scope.status = 'Failed...'; + }); + + $scope.saveMessage = function(message) { + var headers = { 'Authorization': authToken }; + $scope.status = 'Saving...'; + + $http.post('/add-msg.py', message, { headers: headers } ).then(function(response) { + $scope.status = ''; + }).catch(function() { + $scope.status = 'Failed...'; + }); + }; + } + ``` + * + * Now we setup the mock backend and create the test specs: + * + ```js + // testing controller + describe('MyController', function() { + var $httpBackend, $rootScope, createController, authRequestHandler; + + // Set up the module + beforeEach(module('MyApp')); + + beforeEach(inject(function($injector) { + // Set up the mock http service responses + $httpBackend = $injector.get('$httpBackend'); + // backend definition common for all tests + authRequestHandler = $httpBackend.when('GET', '/auth.py') + .respond({userId: 'userX'}, {'A-Token': 'xxx'}); + + // Get hold of a scope (i.e. the root scope) + $rootScope = $injector.get('$rootScope'); + // The $controller service is used to create instances of controllers + var $controller = $injector.get('$controller'); + + createController = function() { + return $controller('MyController', {'$scope' : $rootScope }); + }; + })); + + + afterEach(function() { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); + + + it('should fetch authentication token', function() { + $httpBackend.expectGET('/auth.py'); + var controller = createController(); + $httpBackend.flush(); + }); + + + it('should fail authentication', function() { + + // Notice how you can change the response even after it was set + authRequestHandler.respond(401, ''); + + $httpBackend.expectGET('/auth.py'); + var controller = createController(); + $httpBackend.flush(); + expect($rootScope.status).toBe('Failed...'); + }); + + + it('should send msg to server', function() { + var controller = createController(); + $httpBackend.flush(); + + // now you don’t care about the authentication, but + // the controller will still send the request and + // $httpBackend will respond without you having to + // specify the expectation and response for this request + + $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, ''); + $rootScope.saveMessage('message content'); + expect($rootScope.status).toBe('Saving...'); + $httpBackend.flush(); + expect($rootScope.status).toBe(''); + }); + + + it('should send auth header', function() { + var controller = createController(); + $httpBackend.flush(); + + $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) { + // check if the header was sent, if it wasn't the expectation won't + // match the request and the test will fail + return headers['Authorization'] === 'xxx'; + }).respond(201, ''); + + $rootScope.saveMessage('whatever'); + $httpBackend.flush(); + }); + }); + ``` + * + * ## Dynamic responses + * + * You define a response to a request by chaining a call to `respond()` onto a definition or expectation. + * If you provide a **callback** as the first parameter to `respond(callback)` then you can dynamically generate + * a response based on the properties of the request. + * + * The `callback` function should be of the form `function(method, url, data, headers, params)`. + * + * ### Query parameters + * + * By default, query parameters on request URLs are parsed into the `params` object. So a request URL + * of `/list?q=searchstr&orderby=-name` would set `params` to be `{q: 'searchstr', orderby: '-name'}`. + * + * ### Regex parameter matching + * + * If an expectation or definition uses a **regex** to match the URL, you can provide an array of **keys** via a + * `params` argument. The index of each **key** in the array will match the index of a **group** in the + * **regex**. + * + * The `params` object in the **callback** will now have properties with these keys, which hold the value of the + * corresponding **group** in the **regex**. + * + * This also applies to the `when` and `expect` shortcut methods. + * + * + * ```js + * $httpBackend.expect('GET', /\/user\/(.+)/, undefined, undefined, ['id']) + * .respond(function(method, url, data, headers, params) { + * // for requested url of '/user/1234' params is {id: '1234'} + * }); + * + * $httpBackend.whenPATCH(/\/user\/(.+)\/article\/(.+)/, undefined, undefined, ['user', 'article']) + * .respond(function(method, url, data, headers, params) { + * // for url of '/user/1234/article/567' params is {user: '1234', article: '567'} + * }); + * ``` + * + * ## Matching route requests + * + * For extra convenience, `whenRoute` and `expectRoute` shortcuts are available. These methods offer colon + * delimited matching of the url path, ignoring the query string and trailing slashes. This allows declarations + * similar to how application routes are configured with `$routeProvider`. Because these methods convert + * the definition url to regex, declaration order is important. Combined with query parameter parsing, + * the following is possible: + * + ```js + $httpBackend.whenRoute('GET', '/users/:id') + .respond(function(method, url, data, headers, params) { + return [200, MockUserList[Number(params.id)]]; + }); + + $httpBackend.whenRoute('GET', '/users') + .respond(function(method, url, data, headers, params) { + var userList = angular.copy(MockUserList), + defaultSort = 'lastName', + count, pages, isPrevious, isNext; + + // paged api response '/v1/users?page=2' + params.page = Number(params.page) || 1; + + // query for last names '/v1/users?q=Archer' + if (params.q) { + userList = $filter('filter')({lastName: params.q}); + } + + pages = Math.ceil(userList.length / pagingLength); + isPrevious = params.page > 1; + isNext = params.page < pages; + + return [200, { + count: userList.length, + previous: isPrevious, + next: isNext, + // sort field -> '/v1/users?sortBy=firstName' + results: $filter('orderBy')(userList, params.sortBy || defaultSort) + .splice((params.page - 1) * pagingLength, pagingLength) + }]; + }); + ``` + */ + angular.mock.$httpBackendDecorator = + ['$rootScope', '$timeout', '$delegate', createHttpBackendMock]; + + /** + * General factory function for $httpBackend mock. + * Returns instance for unit testing (when no arguments specified): + * - passing through is disabled + * - auto flushing is disabled + * + * Returns instance for e2e testing (when `$delegate` and `$browser` specified): + * - passing through (delegating request to real backend) is enabled + * - auto flushing is enabled + * + * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) + * @param {Object=} $browser Auto-flushing enabled if specified + * @return {Object} Instance of $httpBackend mock + */ + function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { + var definitions = [], + expectations = [], + matchLatestDefinition = false, + responses = [], + responsesPush = angular.bind(responses, responses.push), + copy = angular.copy, + // We cache the original backend so that if both ngMock and ngMockE2E override the + // service the ngMockE2E version can pass through to the real backend + originalHttpBackend = $delegate.$$originalHttpBackend || $delegate; + + function createResponse(status, data, headers, statusText) { + if (angular.isFunction(status)) return status; + + return function() { + return angular.isNumber(status) + ? [status, data, headers, statusText, 'complete'] + : [200, status, data, headers, 'complete']; + }; + } + + // TODO(vojta): change params to: method, url, data, headers, callback + function $httpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) { + + var xhr = new MockXhr(), + expectation = expectations[0], + wasExpected = false; + + xhr.$$events = eventHandlers; + xhr.upload.$$events = uploadEventHandlers; + + function prettyPrint(data) { + return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) + ? data + : angular.toJson(data); + } + + function wrapResponse(wrapped) { + if (!$browser && timeout) { + if (timeout.then) { + timeout.then(function() { + handlePrematureEnd(angular.isDefined(timeout.$$timeoutId) ? 'timeout' : 'abort'); + }); + } else { + $timeout(function() { + handlePrematureEnd('timeout'); + }, timeout); + } + } + + handleResponse.description = method + ' ' + url; + return handleResponse; + + function handleResponse() { + var response = wrapped.response(method, url, data, headers, wrapped.params(url)); + xhr.$$respHeaders = response[2]; + callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(), + copy(response[3] || ''), copy(response[4])); + } + + function handlePrematureEnd(reason) { + for (var i = 0, ii = responses.length; i < ii; i++) { + if (responses[i] === handleResponse) { + responses.splice(i, 1); + callback(-1, undefined, '', undefined, reason); + break; + } + } + } + } + + function createFatalError(message) { + var error = new Error(message); + // In addition to being converted to a rejection, these errors also need to be passed to + // the $exceptionHandler and be rethrown (so that the test fails). + error.$$passToExceptionHandler = true; + return error; + } + + if (expectation && expectation.match(method, url)) { + if (!expectation.matchData(data)) { + throw createFatalError('Expected ' + expectation + ' with different data\n' + + 'EXPECTED: ' + prettyPrint(expectation.data) + '\n' + + 'GOT: ' + data); + } + + if (!expectation.matchHeaders(headers)) { + throw createFatalError('Expected ' + expectation + ' with different headers\n' + + 'EXPECTED: ' + prettyPrint(expectation.headers) + '\n' + + 'GOT: ' + prettyPrint(headers)); + } + + expectations.shift(); + + if (expectation.response) { + responses.push(wrapResponse(expectation)); + return; + } + wasExpected = true; + } + + var i = matchLatestDefinition ? definitions.length : -1, definition; + + while ((definition = definitions[matchLatestDefinition ? --i : ++i])) { + if (definition.match(method, url, data, headers || {})) { + if (definition.response) { + // if $browser specified, we do auto flush all requests + ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); + } else if (definition.passThrough) { + originalHttpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers); + } else throw createFatalError('No response defined !'); + return; + } + } + + if (wasExpected) { + throw createFatalError('No response defined !'); + } + + throw createFatalError('Unexpected request: ' + method + ' ' + url + '\n' + + (expectation ? 'Expected ' + expectation : 'No more request expected')); + } + + /** + * @ngdoc method + * @name $httpBackend#when + * @description + * Creates a new backend definition. + * + * @param {string} method HTTP method. + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + * + * - respond – + * ```js + * {function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers, params)} + * ``` + * – The respond method takes a set of static data to be returned or a function that can + * return an array containing response status (number), response data (Array|Object|string), + * response headers (Object), HTTP status text (string), and XMLHttpRequest status (string: + * `complete`, `error`, `timeout` or `abort`). The respond method returns the `requestHandler` + * object for possible overrides. + */ + $httpBackend.when = function(method, url, data, headers, keys) { + + assertArgDefined(arguments, 1, 'url'); + + var definition = new MockHttpExpectation(method, url, data, headers, keys), + chain = { + respond: function(status, data, headers, statusText) { + definition.passThrough = undefined; + definition.response = createResponse(status, data, headers, statusText); + return chain; + } + }; + + if ($browser) { + chain.passThrough = function() { + definition.response = undefined; + definition.passThrough = true; + return chain; + }; + } + + definitions.push(definition); + return chain; + }; + + /** + * @ngdoc method + * @name $httpBackend#matchLatestDefinitionEnabled + * @description + * This method can be used to change which mocked responses `$httpBackend` returns, when defining + * them with {@link ngMock.$httpBackend#when $httpBackend.when()} (and shortcut methods). + * By default, `$httpBackend` returns the first definition that matches. When setting + * `$http.matchLatestDefinitionEnabled(true)`, it will use the last response that matches, i.e. the + * one that was added last. + * + * ```js + * hb.when('GET', '/url1').respond(200, 'content', {}); + * hb.when('GET', '/url1').respond(201, 'another', {}); + * hb('GET', '/url1'); // receives "content" + * + * $http.matchLatestDefinitionEnabled(true) + * hb('GET', '/url1'); // receives "another" + * + * hb.when('GET', '/url1').respond(201, 'onemore', {}); + * hb('GET', '/url1'); // receives "onemore" + * ``` + * + * This is useful if a you have a default response that is overriden inside specific tests. + * + * Note that different from config methods on providers, `matchLatestDefinitionEnabled()` can be changed + * even when the application is already running. + * + * @param {Boolean=} value value to set, either `true` or `false`. Default is `false`. + * If omitted, it will return the current value. + * @return {$httpBackend|Boolean} self when used as a setter, and the current value when used + * as a getter + */ + $httpBackend.matchLatestDefinitionEnabled = function(value) { + if (angular.isDefined(value)) { + matchLatestDefinition = value; + return this; + } else { + return matchLatestDefinition; + } + }; + + /** + * @ngdoc method + * @name $httpBackend#whenGET + * @description + * Creates a new backend definition for GET requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenHEAD + * @description + * Creates a new backend definition for HEAD requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenDELETE + * @description + * Creates a new backend definition for DELETE requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenPOST + * @description + * Creates a new backend definition for POST requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenPUT + * @description + * Creates a new backend definition for PUT requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenJSONP + * @description + * Creates a new backend definition for JSONP requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + createShortMethods('when'); + + /** + * @ngdoc method + * @name $httpBackend#whenRoute + * @description + * Creates a new backend definition that compares only with the requested route. + * + * @param {string} method HTTP method. + * @param {string} url HTTP url string that supports colon param matching. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + * See {@link ngMock.$httpBackend#when `when`} for more info. + */ + $httpBackend.whenRoute = function(method, url) { + var parsed = parseRouteUrl(url); + return $httpBackend.when(method, parsed.regexp, undefined, undefined, parsed.keys); + }; + + /** + * @ngdoc method + * @name $httpBackend#expect + * @description + * Creates a new request expectation. + * + * @param {string} method HTTP method. + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + * + * - respond – + * ```js + * {function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers, params)} + * ``` + * – The respond method takes a set of static data to be returned or a function that can + * return an array containing response status (number), response data (Array|Object|string), + * response headers (Object), HTTP status text (string), and XMLHttpRequest status (string: + * `complete`, `error`, `timeout` or `abort`). The respond method returns the `requestHandler` + * object for possible overrides. + */ + $httpBackend.expect = function(method, url, data, headers, keys) { + + assertArgDefined(arguments, 1, 'url'); + + var expectation = new MockHttpExpectation(method, url, data, headers, keys), + chain = { + respond: function(status, data, headers, statusText) { + expectation.response = createResponse(status, data, headers, statusText); + return chain; + } + }; + + expectations.push(expectation); + return chain; + }; + + /** + * @ngdoc method + * @name $httpBackend#expectGET + * @description + * Creates a new request expectation for GET requests. For more info see `expect()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current expectation. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. See #expect for more info. + */ + + /** + * @ngdoc method + * @name $httpBackend#expectHEAD + * @description + * Creates a new request expectation for HEAD requests. For more info see `expect()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current expectation. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#expectDELETE + * @description + * Creates a new request expectation for DELETE requests. For more info see `expect()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current expectation. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#expectPOST + * @description + * Creates a new request expectation for POST requests. For more info see `expect()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current expectation. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#expectPUT + * @description + * Creates a new request expectation for PUT requests. For more info see `expect()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current expectation. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#expectPATCH + * @description + * Creates a new request expectation for PATCH requests. For more info see `expect()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current expectation. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#expectJSONP + * @description + * Creates a new request expectation for JSONP requests. For more info see `expect()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives an url + * and returns true if the url matches the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + createShortMethods('expect'); + + /** + * @ngdoc method + * @name $httpBackend#expectRoute + * @description + * Creates a new request expectation that compares only with the requested route. + * + * @param {string} method HTTP method. + * @param {string} url HTTP url string that supports colon param matching. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + * See {@link ngMock.$httpBackend#expect `expect`} for more info. + */ + $httpBackend.expectRoute = function(method, url) { + var parsed = parseRouteUrl(url); + return $httpBackend.expect(method, parsed.regexp, undefined, undefined, parsed.keys); + }; + + + /** + * @ngdoc method + * @name $httpBackend#flush + * @description + * Flushes pending requests using the trained responses. Requests are flushed in the order they + * were made, but it is also possible to skip one or more requests (for example to have them + * flushed later). This is useful for simulating scenarios where responses arrive from the server + * in any order. + * + * If there are no pending requests to flush when the method is called, an exception is thrown (as + * this is typically a sign of programming error). + * + * @param {number=} count - Number of responses to flush. If undefined/null, all pending requests + * (starting after `skip`) will be flushed. + * @param {number=} [skip=0] - Number of pending requests to skip. For example, a value of `5` + * would skip the first 5 pending requests and start flushing from the 6th onwards. + */ + $httpBackend.flush = function(count, skip, digest) { + if (digest !== false) $rootScope.$digest(); + + skip = skip || 0; + if (skip >= responses.length) throw new Error('No pending request to flush !'); + + if (angular.isDefined(count) && count !== null) { + while (count--) { + var part = responses.splice(skip, 1); + if (!part.length) throw new Error('No more pending request to flush !'); + part[0](); + } + } else { + while (responses.length > skip) { + responses.splice(skip, 1)[0](); + } + } + $httpBackend.verifyNoOutstandingExpectation(digest); + }; + + + /** + * @ngdoc method + * @name $httpBackend#verifyNoOutstandingExpectation + * @description + * Verifies that all of the requests defined via the `expect` api were made. If any of the + * requests were not made, verifyNoOutstandingExpectation throws an exception. + * + * Typically, you would call this method following each test case that asserts requests using an + * "afterEach" clause. + * + * ```js + * afterEach($httpBackend.verifyNoOutstandingExpectation); + * ``` + */ + $httpBackend.verifyNoOutstandingExpectation = function(digest) { + if (digest !== false) $rootScope.$digest(); + if (expectations.length) { + throw new Error('Unsatisfied requests: ' + expectations.join(', ')); + } + }; + + + /** + * @ngdoc method + * @name $httpBackend#verifyNoOutstandingRequest + * @description + * Verifies that there are no outstanding requests that need to be flushed. + * + * Typically, you would call this method following each test case that asserts requests using an + * "afterEach" clause. + * + * ```js + * afterEach($httpBackend.verifyNoOutstandingRequest); + * ``` + */ + $httpBackend.verifyNoOutstandingRequest = function(digest) { + if (digest !== false) $rootScope.$digest(); + if (responses.length) { + var unflushedDescriptions = responses.map(function(res) { return res.description; }); + throw new Error('Unflushed requests: ' + responses.length + '\n ' + + unflushedDescriptions.join('\n ')); + } + }; + + + /** + * @ngdoc method + * @name $httpBackend#resetExpectations + * @description + * Resets all request expectations, but preserves all backend definitions. Typically, you would + * call resetExpectations during a multiple-phase test when you want to reuse the same instance of + * $httpBackend mock. + */ + $httpBackend.resetExpectations = function() { + expectations.length = 0; + responses.length = 0; + }; + + $httpBackend.$$originalHttpBackend = originalHttpBackend; + + return $httpBackend; + + + function createShortMethods(prefix) { + angular.forEach(['GET', 'DELETE', 'JSONP', 'HEAD'], function(method) { + $httpBackend[prefix + method] = function(url, headers, keys) { + assertArgDefined(arguments, 0, 'url'); + + // Change url to `null` if `undefined` to stop it throwing an exception further down + if (angular.isUndefined(url)) url = null; + + return $httpBackend[prefix](method, url, undefined, headers, keys); + }; + }); + + angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { + $httpBackend[prefix + method] = function(url, data, headers, keys) { + assertArgDefined(arguments, 0, 'url'); + + // Change url to `null` if `undefined` to stop it throwing an exception further down + if (angular.isUndefined(url)) url = null; + + return $httpBackend[prefix](method, url, data, headers, keys); + }; + }); + } + + function parseRouteUrl(url) { + var strippedUrl = stripQueryAndHash(url); + var parseOptions = {caseInsensitiveMatch: true, ignoreTrailingSlashes: true}; + return routeToRegExp(strippedUrl, parseOptions); + } + } + + function assertArgDefined(args, index, name) { + if (args.length > index && angular.isUndefined(args[index])) { + throw new Error('Undefined argument `' + name + '`; the argument is provided but not defined'); + } + } + + function stripQueryAndHash(url) { + return url.replace(/[?#].*$/, ''); + } + + function MockHttpExpectation(expectedMethod, expectedUrl, expectedData, expectedHeaders, + expectedKeys) { + + this.data = expectedData; + this.headers = expectedHeaders; + + this.match = function(method, url, data, headers) { + if (expectedMethod !== method) return false; + if (!this.matchUrl(url)) return false; + if (angular.isDefined(data) && !this.matchData(data)) return false; + if (angular.isDefined(headers) && !this.matchHeaders(headers)) return false; + return true; + }; + + this.matchUrl = function(url) { + if (!expectedUrl) return true; + if (angular.isFunction(expectedUrl.test)) return expectedUrl.test(url); + if (angular.isFunction(expectedUrl)) return expectedUrl(url); + return (expectedUrl === url || compareUrlWithQuery(url)); + }; + + this.matchHeaders = function(headers) { + if (angular.isUndefined(expectedHeaders)) return true; + if (angular.isFunction(expectedHeaders)) return expectedHeaders(headers); + return angular.equals(expectedHeaders, headers); + }; + + this.matchData = function(data) { + if (angular.isUndefined(expectedData)) return true; + if (expectedData && angular.isFunction(expectedData.test)) return expectedData.test(data); + if (expectedData && angular.isFunction(expectedData)) return expectedData(data); + if (expectedData && !angular.isString(expectedData)) { + return angular.equals(angular.fromJson(angular.toJson(expectedData)), angular.fromJson(data)); + } + // eslint-disable-next-line eqeqeq + return expectedData == data; + }; + + this.toString = function() { + return expectedMethod + ' ' + expectedUrl; + }; + + this.params = function(url) { + var queryStr = url.indexOf('?') === -1 ? '' : url.substring(url.indexOf('?') + 1); + var strippedUrl = stripQueryAndHash(url); + + return angular.extend(extractParamsFromQuery(queryStr), extractParamsFromPath(strippedUrl)); + }; + + function compareUrlWithQuery(url) { + var urlWithQueryRe = /^([^?]*)\?(.*)$/; + + var expectedMatch = urlWithQueryRe.exec(expectedUrl); + var actualMatch = urlWithQueryRe.exec(url); + + return !!(expectedMatch && actualMatch) && + (expectedMatch[1] === actualMatch[1]) && + (normalizeQuery(expectedMatch[2]) === normalizeQuery(actualMatch[2])); + } + + function normalizeQuery(queryStr) { + return queryStr.split('&').sort().join('&'); + } + + function extractParamsFromPath(strippedUrl) { + var keyObj = {}; + + if (!expectedUrl || !angular.isFunction(expectedUrl.test) || + !expectedKeys || !expectedKeys.length) return keyObj; + + var match = expectedUrl.exec(strippedUrl); + if (!match) return keyObj; + + for (var i = 1, len = match.length; i < len; ++i) { + var key = expectedKeys[i - 1]; + var val = match[i]; + if (key && val) { + keyObj[key.name || key] = val; + } + } + + return keyObj; + } + + function extractParamsFromQuery(queryStr) { + var obj = {}, + keyValuePairs = queryStr.split('&'). + filter(angular.identity). // Ignore empty segments. + map(function(keyValue) { return keyValue.replace(/\+/g, '%20').split('='); }); + + angular.forEach(keyValuePairs, function(pair) { + var key = tryDecodeURIComponent(pair[0]); + if (angular.isDefined(key)) { + var val = angular.isDefined(pair[1]) ? tryDecodeURIComponent(pair[1]) : true; + if (!hasOwnProperty.call(obj, key)) { + obj[key] = val; + } else if (angular.isArray(obj[key])) { + obj[key].push(val); + } else { + obj[key] = [obj[key], val]; + } + } + }); + + return obj; + } + + function tryDecodeURIComponent(value) { + try { + return decodeURIComponent(value); + } catch (e) { + // Ignore any invalid uri component + } + } + } + + function createMockXhr() { + return new MockXhr(); + } + + function MockXhr() { + + // hack for testing $http, $httpBackend + MockXhr.$$lastInstance = this; + + this.open = function(method, url, async) { + this.$$method = method; + this.$$url = url; + this.$$async = async; + this.$$reqHeaders = {}; + this.$$respHeaders = {}; + }; + + this.send = function(data) { + this.$$data = data; + }; + + this.setRequestHeader = function(key, value) { + this.$$reqHeaders[key] = value; + }; + + this.getResponseHeader = function(name) { + // the lookup must be case insensitive, + // that's why we try two quick lookups first and full scan last + var header = this.$$respHeaders[name]; + if (header) return header; + + name = angular.$$lowercase(name); + header = this.$$respHeaders[name]; + if (header) return header; + + header = undefined; + angular.forEach(this.$$respHeaders, function(headerVal, headerName) { + if (!header && angular.$$lowercase(headerName) === name) header = headerVal; + }); + return header; + }; + + this.getAllResponseHeaders = function() { + var lines = []; + + angular.forEach(this.$$respHeaders, function(value, key) { + lines.push(key + ': ' + value); + }); + return lines.join('\n'); + }; + + this.abort = function() { + if (isFunction(this.onabort)) { + this.onabort(); + } + }; + + // This section simulates the events on a real XHR object (and the upload object) + // When we are testing $httpBackend (inside the AngularJS project) we make partial use of this + // but store the events directly ourselves on `$$events`, instead of going through the `addEventListener` + this.$$events = {}; + this.addEventListener = function(name, listener) { + if (angular.isUndefined(this.$$events[name])) this.$$events[name] = []; + this.$$events[name].push(listener); + }; + + this.upload = { + $$events: {}, + addEventListener: this.addEventListener + }; + } + + + /** + * @ngdoc service + * @name $timeout + * @description + * + * This service is just a simple decorator for {@link ng.$timeout $timeout} service + * that adds a "flush" and "verifyNoPendingTasks" methods. + */ + + angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $browser) { + + /** + * @ngdoc method + * @name $timeout#flush + * + * @deprecated + * sinceVersion="1.7.3" + * + * This method flushes all types of tasks (not only timeouts), which is unintuitive. + * It is recommended to use {@link ngMock.$flushPendingTasks} instead. + * + * @description + * + * Flushes the queue of pending tasks. + * + * _This method is essentially an alias of {@link ngMock.$flushPendingTasks}._ + * + *
+ * For historical reasons, this method will also flush non-`$timeout` pending tasks, such as + * {@link $q} promises and tasks scheduled via + * {@link ng.$rootScope.Scope#$applyAsync $applyAsync} and + * {@link ng.$rootScope.Scope#$evalAsync $evalAsync}. + *
+ * + * @param {number=} delay maximum timeout amount to flush up until + */ + $delegate.flush = function(delay) { + // For historical reasons, `$timeout.flush()` flushes all types of pending tasks. + // Keep the same behavior for backwards compatibility (and because it doesn't make sense to + // selectively flush scheduled events out of order). + $browser.defer.flush(delay); + }; + + /** + * @ngdoc method + * @name $timeout#verifyNoPendingTasks + * + * @deprecated + * sinceVersion="1.7.3" + * + * This method takes all types of tasks (not only timeouts) into account, which is unintuitive. + * It is recommended to use {@link ngMock.$verifyNoPendingTasks} instead, which additionally + * allows checking for timeouts only (with `$verifyNoPendingTasks('$timeout')`). + * + * @description + * + * Verifies that there are no pending tasks that need to be flushed. It throws an error if there + * are still pending tasks. + * + * _This method is essentially an alias of {@link ngMock.$verifyNoPendingTasks} (called with no + * arguments)._ + * + *
+ *

+ * For historical reasons, this method will also verify non-`$timeout` pending tasks, such as + * pending {@link $http} requests, in-progress {@link $route} transitions, unresolved + * {@link $q} promises and tasks scheduled via + * {@link ng.$rootScope.Scope#$applyAsync $applyAsync} and + * {@link ng.$rootScope.Scope#$evalAsync $evalAsync}. + *

+ *

+ * It is recommended to use {@link ngMock.$verifyNoPendingTasks} instead, which additionally + * supports verifying a specific type of tasks. For example, you can verify there are no + * pending timeouts with `$verifyNoPendingTasks('$timeout')`. + *

+ *
+ */ + $delegate.verifyNoPendingTasks = function() { + // For historical reasons, `$timeout.verifyNoPendingTasks()` takes all types of pending tasks + // into account. Keep the same behavior for backwards compatibility. + var pendingTasks = $browser.defer.getPendingTasks(); + + if (pendingTasks.length) { + var formattedTasks = $browser.defer.formatPendingTasks(pendingTasks).join('\n '); + var hasPendingTimeout = pendingTasks.some(function(task) { return task.type === '$timeout'; }); + var extraMessage = hasPendingTimeout ? '' : '\n\nNone of the pending tasks are timeouts. ' + + 'If you only want to verify pending timeouts, use ' + + '`$verifyNoPendingTasks(\'$timeout\')` instead.'; + + throw new Error('Deferred tasks to flush (' + pendingTasks.length + '):\n ' + + formattedTasks + extraMessage); + } + }; + + return $delegate; + }]; + + angular.mock.$RAFDecorator = ['$delegate', function($delegate) { + var rafFn = function(fn) { + var index = rafFn.queue.length; + rafFn.queue.push(fn); + return function() { + rafFn.queue.splice(index, 1); + }; + }; + + rafFn.queue = []; + rafFn.supported = $delegate.supported; + + rafFn.flush = function() { + if (rafFn.queue.length === 0) { + throw new Error('No rAF callbacks present'); + } + + var length = rafFn.queue.length; + for (var i = 0; i < length; i++) { + rafFn.queue[i](); + } + + rafFn.queue = rafFn.queue.slice(i); + }; + + return rafFn; + }]; + + /** + * + */ + var originalRootElement; + angular.mock.$RootElementProvider = function() { + this.$get = ['$injector', function($injector) { + originalRootElement = angular.element('
').data('$injector', $injector); + return originalRootElement; + }]; + }; + + /** + * @ngdoc service + * @name $controller + * @description + * A decorator for {@link ng.$controller} with additional `bindings` parameter, useful when testing + * controllers of directives that use {@link $compile#-bindtocontroller- `bindToController`}. + * + * ## Example + * + * ```js + * + * // Directive definition ... + * + * myMod.directive('myDirective', { + * controller: 'MyDirectiveController', + * bindToController: { + * name: '@' + * } + * }); + * + * + * // Controller definition ... + * + * myMod.controller('MyDirectiveController', ['$log', function($log) { + * this.log = function() { + * $log.info(this.name); + * }; + * }]); + * + * + * // In a test ... + * + * describe('myDirectiveController', function() { + * describe('log()', function() { + * it('should write the bound name to the log', inject(function($controller, $log) { + * var ctrl = $controller('MyDirectiveController', { /* no locals */ }, { name: 'Clark Kent' }); + * ctrl.log(); + * + * expect(ctrl.name).toEqual('Clark Kent'); + * expect($log.info.logs).toEqual(['Clark Kent']); + * })); + * }); + * }); + * + * ``` + * + * @param {Function|string} constructor If called with a function then it's considered to be the + * controller constructor function. Otherwise it's considered to be a string which is used + * to retrieve the controller constructor using the following steps: + * + * * check if a controller with given name is registered via `$controllerProvider` + * * check if evaluating the string on the current scope returns a constructor + * + * The string can use the `controller as property` syntax, where the controller instance is published + * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this + * to work correctly. + * + * @param {Object} locals Injection locals for Controller. + * @param {Object=} bindings Properties to add to the controller instance. This is used to simulate + * the `bindToController` feature and simplify certain kinds of tests. + * @return {Object} Instance of given controller. + */ + function createControllerDecorator() { + angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { + return function(expression, locals, later, ident) { + if (later && typeof later === 'object') { + var instantiate = $delegate(expression, locals, true, ident); + var instance = instantiate(); + angular.extend(instance, later); + return instance; + } + return $delegate(expression, locals, later, ident); + }; + }]; + + return angular.mock.$ControllerDecorator; + } + + /** + * @ngdoc service + * @name $componentController + * @description + * A service that can be used to create instances of component controllers. Useful for unit-testing. + * + * Be aware that the controller will be instantiated and attached to the scope as specified in + * the component definition object. If you do not provide a `$scope` object in the `locals` param + * then the helper will create a new isolated scope as a child of `$rootScope`. + * + * If you are using `$element` or `$attrs` in the controller, make sure to provide them as `locals`. + * The `$element` must be a jqLite-wrapped DOM element, and `$attrs` should be an object that + * has all properties / functions that you are using in the controller. If this is getting too complex, + * you should compile the component instead and access the component's controller via the + * {@link angular.element#methods `controller`} function. + * + * See also the section on {@link guide/component#unit-testing-component-controllers unit-testing component controllers} + * in the guide. + * + * @param {string} componentName the name of the component whose controller we want to instantiate + * @param {Object} locals Injection locals for Controller. + * @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used + * to simulate the `bindToController` feature and simplify certain kinds of tests. + * @param {string=} ident Override the property name to use when attaching the controller to the scope. + * @return {Object} Instance of requested controller. + */ + angular.mock.$ComponentControllerProvider = ['$compileProvider', + function ComponentControllerProvider($compileProvider) { + this.$get = ['$controller','$injector', '$rootScope', function($controller, $injector, $rootScope) { + return function $componentController(componentName, locals, bindings, ident) { + // get all directives associated to the component name + var directives = $injector.get(componentName + 'Directive'); + // look for those directives that are components + var candidateDirectives = directives.filter(function(directiveInfo) { + // components have controller, controllerAs and restrict:'E' + return directiveInfo.controller && directiveInfo.controllerAs && directiveInfo.restrict === 'E'; + }); + // check if valid directives found + if (candidateDirectives.length === 0) { + throw new Error('No component found'); + } + if (candidateDirectives.length > 1) { + throw new Error('Too many components found'); + } + // get the info of the component + var directiveInfo = candidateDirectives[0]; + // create a scope if needed + locals = locals || {}; + locals.$scope = locals.$scope || $rootScope.$new(true); + return $controller(directiveInfo.controller, locals, bindings, ident || directiveInfo.controllerAs); + }; + }]; + }]; + + + /** + * @ngdoc module + * @name ngMock + * @packageName angular-mocks + * @description + * + * The `ngMock` module provides support to inject and mock AngularJS services into unit tests. + * In addition, ngMock also extends various core AngularJS services such that they can be + * inspected and controlled in a synchronous manner within test code. + * + * @installation + * + * First, download the file: + * * [Google CDN](https://developers.google.com/speed/libraries/devguide#angularjs) e.g. + * `"//ajax.googleapis.com/ajax/libs/angularjs/X.Y.Z/angular-mocks.js"` + * * [NPM](https://www.npmjs.com/) e.g. `npm install angular-mocks@X.Y.Z` + * * [Yarn](https://yarnpkg.com) e.g. `yarn add angular-mocks@X.Y.Z` + * * [Bower](http://bower.io) e.g. `bower install angular-mocks#X.Y.Z` + * * [code.angularjs.org](https://code.angularjs.org/) (discouraged for production use) e.g. + * `"//code.angularjs.org/X.Y.Z/angular-mocks.js"` + * + * where X.Y.Z is the AngularJS version you are running. + * + * Then, configure your test runner to load `angular-mocks.js` after `angular.js`. + * This example uses Karma: + * + * ``` + * config.set({ + * files: [ + * 'build/angular.js', // and other module files you need + * 'build/angular-mocks.js', + * '', + * '' + * ] + * }); + * ``` + * + * Including the `angular-mocks.js` file automatically adds the `ngMock` module, so your tests + * are ready to go! + */ + angular.module('ngMock', ['ng']).provider({ + $browser: angular.mock.$BrowserProvider, + $exceptionHandler: angular.mock.$ExceptionHandlerProvider, + $log: angular.mock.$LogProvider, + $interval: angular.mock.$IntervalProvider, + $rootElement: angular.mock.$RootElementProvider, + $componentController: angular.mock.$ComponentControllerProvider, + $flushPendingTasks: angular.mock.$FlushPendingTasksProvider, + $verifyNoPendingTasks: angular.mock.$VerifyNoPendingTasksProvider + }).config(['$provide', '$compileProvider', function($provide, $compileProvider) { + $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); + $provide.decorator('$$rAF', angular.mock.$RAFDecorator); + $provide.decorator('$rootScope', angular.mock.$RootScopeDecorator); + $provide.decorator('$controller', createControllerDecorator($compileProvider)); + $provide.decorator('$httpBackend', angular.mock.$httpBackendDecorator); + }]).info({ angularVersion: '1.8.3' }); + + /** + * @ngdoc module + * @name ngMockE2E + * @module ngMockE2E + * @packageName angular-mocks + * @description + * + * The `ngMockE2E` is an AngularJS module which contains mocks suitable for end-to-end testing. + * Currently there is only one mock present in this module - + * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. + */ + angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { + $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); + }]).info({ angularVersion: '1.8.3' }); + + /** + * @ngdoc service + * @name $httpBackend + * @module ngMockE2E + * @description + * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of + * applications that use the {@link ng.$http $http service}. + * + *
+ * **Note**: For fake http backend implementation suitable for unit testing please see + * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. + *
+ * + * This implementation can be used to respond with static or dynamic responses via the `when` api + * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the + * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch + * templates from a webserver). + * + * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application + * is being developed with the real backend api replaced with a mock, it is often desirable for + * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch + * templates or static files from the webserver). To configure the backend with this behavior + * use the `passThrough` request handler of `when` instead of `respond`. + * + * Additionally, we don't want to manually have to flush mocked out requests like we do during unit + * testing. For this reason the e2e $httpBackend flushes mocked out requests + * automatically, closely simulating the behavior of the XMLHttpRequest object. + * + * To setup the application to run with this http backend, you have to create a module that depends + * on the `ngMockE2E` and your application modules and defines the fake backend: + * + * ```js + * var myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']); + * myAppDev.run(function($httpBackend) { + * var phones = [{name: 'phone1'}, {name: 'phone2'}]; + * + * // returns the current list of phones + * $httpBackend.whenGET('/phones').respond(phones); + * + * // adds a new phone to the phones array + * $httpBackend.whenPOST('/phones').respond(function(method, url, data) { + * var phone = angular.fromJson(data); + * phones.push(phone); + * return [200, phone, {}]; + * }); + * $httpBackend.whenGET(/^\/templates\//).passThrough(); // Requests for templates are handled by the real server + * //... + * }); + * ``` + * + * Afterwards, bootstrap your app with this new module. + * + * @example + * + * + * var myApp = angular.module('myApp', []); + * + * myApp.controller('MainCtrl', function MainCtrl($http) { + * var ctrl = this; + * + * ctrl.phones = []; + * ctrl.newPhone = { + * name: '' + * }; + * + * ctrl.getPhones = function() { + * $http.get('/phones').then(function(response) { + * ctrl.phones = response.data; + * }); + * }; + * + * ctrl.addPhone = function(phone) { + * $http.post('/phones', phone).then(function() { + * ctrl.newPhone = {name: ''}; + * return ctrl.getPhones(); + * }); + * }; + * + * ctrl.getPhones(); + * }); + * + * + * var myAppDev = angular.module('myAppE2E', ['myApp', 'ngMockE2E']); + * + * myAppDev.run(function($httpBackend) { + * var phones = [{name: 'phone1'}, {name: 'phone2'}]; + * + * // returns the current list of phones + * $httpBackend.whenGET('/phones').respond(phones); + * + * // adds a new phone to the phones array + * $httpBackend.whenPOST('/phones').respond(function(method, url, data) { + * var phone = angular.fromJson(data); + * phones.push(phone); + * return [200, phone, {}]; + * }); + * }); + * + * + *
+ *
+ * + * + *
+ *

Phones

+ *
    + *
  • {{phone.name}}
  • + *
+ *
+ *
+ *
+ * + * + */ + + /** + * @ngdoc method + * @name $httpBackend#when + * @module ngMockE2E + * @description + * Creates a new backend definition. + * + * @param {string} method HTTP method. + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + * + * - respond – + * ``` + * { function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers, params)} + * ``` + * – The respond method takes a set of static data to be returned or a function that can return + * an array containing response status (number), response data (Array|Object|string), response + * headers (Object), and the text for the status (string). + * - passThrough – `{function()}` – Any request matching a backend definition with + * `passThrough` handler will be passed through to the real backend (an XHR request will be made + * to the server.) + * - Both methods return the `requestHandler` object for possible overrides. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenGET + * @module ngMockE2E + * @description + * Creates a new backend definition for GET requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenHEAD + * @module ngMockE2E + * @description + * Creates a new backend definition for HEAD requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenDELETE + * @module ngMockE2E + * @description + * Creates a new backend definition for DELETE requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenPOST + * @module ngMockE2E + * @description + * Creates a new backend definition for POST requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenPUT + * @module ngMockE2E + * @description + * Creates a new backend definition for PUT requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenPATCH + * @module ngMockE2E + * @description + * Creates a new backend definition for PATCH requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenJSONP + * @module ngMockE2E + * @description + * Creates a new backend definition for JSONP requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)=} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ + /** + * @ngdoc method + * @name $httpBackend#whenRoute + * @module ngMockE2E + * @description + * Creates a new backend definition that compares only with the requested route. + * + * @param {string} method HTTP method. + * @param {string} url HTTP url string that supports colon param matching. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ + /** + * @ngdoc method + * @name $httpBackend#matchLatestDefinitionEnabled + * @module ngMockE2E + * @description + * This method can be used to change which mocked responses `$httpBackend` returns, when defining + * them with {@link ngMock.$httpBackend#when $httpBackend.when()} (and shortcut methods). + * By default, `$httpBackend` returns the first definition that matches. When setting + * `$http.matchLatestDefinitionEnabled(true)`, it will use the last response that matches, i.e. the + * one that was added last. + * + * ```js + * hb.when('GET', '/url1').respond(200, 'content', {}); + * hb.when('GET', '/url1').respond(201, 'another', {}); + * hb('GET', '/url1'); // receives "content" + * + * $http.matchLatestDefinitionEnabled(true) + * hb('GET', '/url1'); // receives "another" + * + * hb.when('GET', '/url1').respond(201, 'onemore', {}); + * hb('GET', '/url1'); // receives "onemore" + * ``` + * + * This is useful if a you have a default response that is overriden inside specific tests. + * + * Note that different from config methods on providers, `matchLatestDefinitionEnabled()` can be changed + * even when the application is already running. + * + * @param {Boolean=} value value to set, either `true` or `false`. Default is `false`. + * If omitted, it will return the current value. + * @return {$httpBackend|Boolean} self when used as a setter, and the current value when used + * as a getter + */ + angular.mock.e2e = {}; + angular.mock.e2e.$httpBackendDecorator = + ['$rootScope', '$timeout', '$delegate', '$browser', createHttpBackendMock]; + + + /** + * @ngdoc type + * @name $rootScope.Scope + * @module ngMock + * @description + * {@link ng.$rootScope.Scope Scope} type decorated with helper methods useful for testing. These + * methods are automatically available on any {@link ng.$rootScope.Scope Scope} instance when + * `ngMock` module is loaded. + * + * In addition to all the regular `Scope` methods, the following helper methods are available: + */ + angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { + + var $rootScopePrototype = Object.getPrototypeOf($delegate); + + $rootScopePrototype.$countChildScopes = countChildScopes; + $rootScopePrototype.$countWatchers = countWatchers; + + return $delegate; + + // ------------------------------------------------------------------------------------------ // + + /** + * @ngdoc method + * @name $rootScope.Scope#$countChildScopes + * @module ngMock + * @this $rootScope.Scope + * @description + * Counts all the direct and indirect child scopes of the current scope. + * + * The current scope is excluded from the count. The count includes all isolate child scopes. + * + * @returns {number} Total number of child scopes. + */ + function countChildScopes() { + var count = 0; // exclude the current scope + var pendingChildHeads = [this.$$childHead]; + var currentScope; + + while (pendingChildHeads.length) { + currentScope = pendingChildHeads.shift(); + + while (currentScope) { + count += 1; + pendingChildHeads.push(currentScope.$$childHead); + currentScope = currentScope.$$nextSibling; + } + } + + return count; + } + + + /** + * @ngdoc method + * @name $rootScope.Scope#$countWatchers + * @this $rootScope.Scope + * @module ngMock + * @description + * Counts all the watchers of direct and indirect child scopes of the current scope. + * + * The watchers of the current scope are included in the count and so are all the watchers of + * isolate child scopes. + * + * @returns {number} Total number of watchers. + */ + function countWatchers() { + var count = this.$$watchers ? this.$$watchers.length : 0; // include the current scope + var pendingChildHeads = [this.$$childHead]; + var currentScope; + + while (pendingChildHeads.length) { + currentScope = pendingChildHeads.shift(); + + while (currentScope) { + count += currentScope.$$watchers ? currentScope.$$watchers.length : 0; + pendingChildHeads.push(currentScope.$$childHead); + currentScope = currentScope.$$nextSibling; + } + } + + return count; + } + }]; + + + (function(jasmineOrMocha) { + + if (!jasmineOrMocha) { + return; + } + + var currentSpec = null, + injectorState = new InjectorState(), + annotatedFunctions = [], + wasInjectorCreated = function() { + return !!currentSpec; + }; + + angular.mock.$$annotate = angular.injector.$$annotate; + angular.injector.$$annotate = function(fn) { + if (typeof fn === 'function' && !fn.$inject) { + annotatedFunctions.push(fn); + } + return angular.mock.$$annotate.apply(this, arguments); + }; + + /** + * @ngdoc function + * @name angular.mock.module + * @description + * + * *NOTE*: This function is also published on window for easy access.
+ * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha + * + * This function registers a module configuration code. It collects the configuration information + * which will be used when the injector is created by {@link angular.mock.inject inject}. + * + * See {@link angular.mock.inject inject} for usage example + * + * @param {...(string|Function|Object)} fns any number of modules which are represented as string + * aliases or as anonymous module initialization functions. The modules are used to + * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an + * object literal is passed each key-value pair will be registered on the module via + * {@link auto.$provide $provide}.value, the key being the string name (or token) to associate + * with the value on the injector. + */ + var module = window.module = angular.mock.module = function() { + var moduleFns = Array.prototype.slice.call(arguments, 0); + return wasInjectorCreated() ? workFn() : workFn; + ///////////////////// + function workFn() { + if (currentSpec.$injector) { + throw new Error('Injector already created, can not register a module!'); + } else { + var fn, modules = currentSpec.$modules || (currentSpec.$modules = []); + angular.forEach(moduleFns, function(module) { + if (angular.isObject(module) && !angular.isArray(module)) { + fn = ['$provide', function($provide) { + angular.forEach(module, function(value, key) { + $provide.value(key, value); + }); + }]; + } else { + fn = module; + } + if (currentSpec.$providerInjector) { + currentSpec.$providerInjector.invoke(fn); + } else { + modules.push(fn); + } + }); + } + } + }; + + module.$$beforeAllHook = (window.before || window.beforeAll); + module.$$afterAllHook = (window.after || window.afterAll); + + // purely for testing ngMock itself + module.$$currentSpec = function(to) { + if (arguments.length === 0) return to; + currentSpec = to; + }; + + /** + * @ngdoc function + * @name angular.mock.module.sharedInjector + * @description + * + * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha + * + * This function ensures a single injector will be used for all tests in a given describe context. + * This contrasts with the default behaviour where a new injector is created per test case. + * + * Use sharedInjector when you want to take advantage of Jasmine's `beforeAll()`, or mocha's + * `before()` methods. Call `module.sharedInjector()` before you setup any other hooks that + * will create (i.e call `module()`) or use (i.e call `inject()`) the injector. + * + * You cannot call `sharedInjector()` from within a context already using `sharedInjector()`. + * + * ## Example + * + * Typically beforeAll is used to make many assertions about a single operation. This can + * cut down test run-time as the test setup doesn't need to be re-run, and enabling focussed + * tests each with a single assertion. + * + * ```js + * describe("Deep Thought", function() { + * + * module.sharedInjector(); + * + * beforeAll(module("UltimateQuestion")); + * + * beforeAll(inject(function(DeepThought) { + * expect(DeepThought.answer).toBeUndefined(); + * DeepThought.generateAnswer(); + * })); + * + * it("has calculated the answer correctly", inject(function(DeepThought) { + * // Because of sharedInjector, we have access to the instance of the DeepThought service + * // that was provided to the beforeAll() hook. Therefore we can test the generated answer + * expect(DeepThought.answer).toBe(42); + * })); + * + * it("has calculated the answer within the expected time", inject(function(DeepThought) { + * expect(DeepThought.runTimeMillennia).toBeLessThan(8000); + * })); + * + * it("has double checked the answer", inject(function(DeepThought) { + * expect(DeepThought.absolutelySureItIsTheRightAnswer).toBe(true); + * })); + * + * }); + * + * ``` + */ + module.sharedInjector = function() { + if (!(module.$$beforeAllHook && module.$$afterAllHook)) { + throw Error('sharedInjector() cannot be used unless your test runner defines beforeAll/afterAll'); + } + + var initialized = false; + + module.$$beforeAllHook(/** @this */ function() { + if (injectorState.shared) { + injectorState.sharedError = Error('sharedInjector() cannot be called inside a context that has already called sharedInjector()'); + throw injectorState.sharedError; + } + initialized = true; + currentSpec = this; + injectorState.shared = true; + }); + + module.$$afterAllHook(function() { + if (initialized) { + injectorState = new InjectorState(); + module.$$cleanup(); + } else { + injectorState.sharedError = null; + } + }); + }; + + module.$$beforeEach = function() { + if (injectorState.shared && currentSpec && currentSpec !== this) { + var state = currentSpec; + currentSpec = this; + angular.forEach(['$injector','$modules','$providerInjector', '$injectorStrict'], function(k) { + currentSpec[k] = state[k]; + state[k] = null; + }); + } else { + currentSpec = this; + originalRootElement = null; + annotatedFunctions = []; + } + }; + + module.$$afterEach = function() { + if (injectorState.cleanupAfterEach()) { + module.$$cleanup(); + } + }; + + module.$$cleanup = function() { + var injector = currentSpec.$injector; + + annotatedFunctions.forEach(function(fn) { + delete fn.$inject; + }); + + currentSpec.$injector = null; + currentSpec.$modules = null; + currentSpec.$providerInjector = null; + currentSpec = null; + + if (injector) { + // Ensure `$rootElement` is instantiated, before checking `originalRootElement` + var $rootElement = injector.get('$rootElement'); + var rootNode = $rootElement && $rootElement[0]; + var cleanUpNodes = !originalRootElement ? [] : [originalRootElement[0]]; + if (rootNode && (!originalRootElement || rootNode !== originalRootElement[0])) { + cleanUpNodes.push(rootNode); + } + angular.element.cleanData(cleanUpNodes); + + // Ensure `$destroy()` is available, before calling it + // (a mocked `$rootScope` might not implement it (or not even be an object at all)) + var $rootScope = injector.get('$rootScope'); + if ($rootScope && $rootScope.$destroy) $rootScope.$destroy(); + } + + // clean up jquery's fragment cache + angular.forEach(angular.element.fragments, function(val, key) { + delete angular.element.fragments[key]; + }); + + MockXhr.$$lastInstance = null; + + angular.forEach(angular.callbacks, function(val, key) { + delete angular.callbacks[key]; + }); + angular.callbacks.$$counter = 0; + }; + + (window.beforeEach || window.setup)(module.$$beforeEach); + (window.afterEach || window.teardown)(module.$$afterEach); + + /** + * @ngdoc function + * @name angular.mock.inject + * @description + * + * *NOTE*: This function is also published on window for easy access.
+ * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha + * + * The inject function wraps a function into an injectable function. The inject() creates new + * instance of {@link auto.$injector $injector} per test, which is then used for + * resolving references. + * + * + * ## Resolving References (Underscore Wrapping) + * Often, we would like to inject a reference once, in a `beforeEach()` block and reuse this + * in multiple `it()` clauses. To be able to do this we must assign the reference to a variable + * that is declared in the scope of the `describe()` block. Since we would, most likely, want + * the variable to have the same name of the reference we have a problem, since the parameter + * to the `inject()` function would hide the outer variable. + * + * To help with this, the injected parameters can, optionally, be enclosed with underscores. + * These are ignored by the injector when the reference name is resolved. + * + * For example, the parameter `_myService_` would be resolved as the reference `myService`. + * Since it is available in the function body as `_myService_`, we can then assign it to a variable + * defined in an outer scope. + * + * ``` + * // Defined out reference variable outside + * var myService; + * + * // Wrap the parameter in underscores + * beforeEach( inject( function(_myService_){ + * myService = _myService_; + * })); + * + * // Use myService in a series of tests. + * it('makes use of myService', function() { + * myService.doStuff(); + * }); + * + * ``` + * + * See also {@link angular.mock.module angular.mock.module} + * + * ## Example + * Example of what a typical jasmine tests looks like with the inject method. + * ```js + * + * angular.module('myApplicationModule', []) + * .value('mode', 'app') + * .value('version', 'v1.0.1'); + * + * + * describe('MyApp', function() { + * + * // You need to load modules that you want to test, + * // it loads only the "ng" module by default. + * beforeEach(module('myApplicationModule')); + * + * + * // inject() is used to inject arguments of all given functions + * it('should provide a version', inject(function(mode, version) { + * expect(version).toEqual('v1.0.1'); + * expect(mode).toEqual('app'); + * })); + * + * + * // The inject and module method can also be used inside of the it or beforeEach + * it('should override a version and test the new version is injected', function() { + * // module() takes functions or strings (module aliases) + * module(function($provide) { + * $provide.value('version', 'overridden'); // override version here + * }); + * + * inject(function(version) { + * expect(version).toEqual('overridden'); + * }); + * }); + * }); + * + * ``` + * + * @param {...Function} fns any number of functions which will be injected using the injector. + */ + + + + var ErrorAddingDeclarationLocationStack = function ErrorAddingDeclarationLocationStack(e, errorForStack) { + this.message = e.message; + this.name = e.name; + if (e.line) this.line = e.line; + if (e.sourceId) this.sourceId = e.sourceId; + if (e.stack && errorForStack) + this.stack = e.stack + '\n' + errorForStack.stack; + if (e.stackArray) this.stackArray = e.stackArray; + }; + ErrorAddingDeclarationLocationStack.prototype = Error.prototype; + + window.inject = angular.mock.inject = function() { + var blockFns = Array.prototype.slice.call(arguments, 0); + var errorForStack = new Error('Declaration Location'); + // IE10+ and PhanthomJS do not set stack trace information, until the error is thrown + if (!errorForStack.stack) { + try { + throw errorForStack; + } catch (e) { /* empty */ } + } + return wasInjectorCreated() ? WorkFn.call(currentSpec) : WorkFn; + ///////////////////// + function WorkFn() { + var modules = currentSpec.$modules || []; + var strictDi = !!currentSpec.$injectorStrict; + modules.unshift(['$injector', function($injector) { + currentSpec.$providerInjector = $injector; + }]); + modules.unshift('ngMock'); + modules.unshift('ng'); + var injector = currentSpec.$injector; + if (!injector) { + if (strictDi) { + // If strictDi is enabled, annotate the providerInjector blocks + angular.forEach(modules, function(moduleFn) { + if (typeof moduleFn === 'function') { + angular.injector.$$annotate(moduleFn); + } + }); + } + injector = currentSpec.$injector = angular.injector(modules, strictDi); + currentSpec.$injectorStrict = strictDi; + } + for (var i = 0, ii = blockFns.length; i < ii; i++) { + if (currentSpec.$injectorStrict) { + // If the injector is strict / strictDi, and the spec wants to inject using automatic + // annotation, then annotate the function here. + injector.annotate(blockFns[i]); + } + try { + injector.invoke(blockFns[i] || angular.noop, this); + } catch (e) { + if (e.stack && errorForStack) { + throw new ErrorAddingDeclarationLocationStack(e, errorForStack); + } + throw e; + } finally { + errorForStack = null; + } + } + } + }; + + + angular.mock.inject.strictDi = function(value) { + value = arguments.length ? !!value : true; + return wasInjectorCreated() ? workFn() : workFn; + + function workFn() { + if (value !== currentSpec.$injectorStrict) { + if (currentSpec.$injector) { + throw new Error('Injector already created, can not modify strict annotations'); + } else { + currentSpec.$injectorStrict = value; + } + } + } + }; + + function InjectorState() { + this.shared = false; + this.sharedError = null; + + this.cleanupAfterEach = function() { + return !this.shared || this.sharedError; + }; + } + })(window.jasmine || window.mocha); + + 'use strict'; + + (function() { + /** + * @ngdoc function + * @name browserTrigger + * @description + * + * This is a global (window) function that is only available when the {@link ngMock} module is + * included. + * + * It can be used to trigger a native browser event on an element, which is useful for unit testing. + * + * + * @param {Object} element Either a wrapped jQuery/jqLite node or a DOMElement + * @param {string=} eventType Optional event type. If none is specified, the function tries + * to determine the right event type for the element, e.g. `change` for + * `input[text]`. + * @param {Object=} eventData An optional object which contains additional event data that is used + * when creating the event: + * + * - `bubbles`: [Event.bubbles](https://developer.mozilla.org/docs/Web/API/Event/bubbles). + * Not applicable to all events. + * + * - `cancelable`: [Event.cancelable](https://developer.mozilla.org/docs/Web/API/Event/cancelable). + * Not applicable to all events. + * + * - `charcode`: [charCode](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/charcode) + * for keyboard events (keydown, keypress, and keyup). + * + * - `data`: [data](https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent/data) for + * [CompositionEvents](https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent). + * + * - `elapsedTime`: the elapsedTime for + * [TransitionEvent](https://developer.mozilla.org/docs/Web/API/TransitionEvent) + * and [AnimationEvent](https://developer.mozilla.org/docs/Web/API/AnimationEvent). + * + * - `keycode`: [keyCode](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/keycode) + * for keyboard events (keydown, keypress, and keyup). + * + * - `keys`: an array of possible modifier keys (ctrl, alt, shift, meta) for + * [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent) and + * keyboard events (keydown, keypress, and keyup). + * + * - `relatedTarget`: the + * [relatedTarget](https://developer.mozilla.org/docs/Web/API/MouseEvent/relatedTarget) + * for [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent). + * + * - `which`: [which](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/which) + * for keyboard events (keydown, keypress, and keyup). + * + * - `x`: x-coordinates for [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent) + * and [TouchEvent](https://developer.mozilla.org/docs/Web/API/TouchEvent). + * + * - `y`: y-coordinates for [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent) + * and [TouchEvent](https://developer.mozilla.org/docs/Web/API/TouchEvent). + * + */ + window.browserTrigger = function browserTrigger(element, eventType, eventData) { + if (element && !element.nodeName) element = element[0]; + if (!element) return; + + eventData = eventData || {}; + var relatedTarget = eventData.relatedTarget || element; + var keys = eventData.keys; + var x = eventData.x; + var y = eventData.y; + + var inputType = (element.type) ? element.type.toLowerCase() : null, + nodeName = element.nodeName.toLowerCase(); + if (!eventType) { + eventType = { + 'text': 'change', + 'textarea': 'change', + 'hidden': 'change', + 'password': 'change', + 'button': 'click', + 'submit': 'click', + 'reset': 'click', + 'image': 'click', + 'checkbox': 'click', + 'radio': 'click', + 'select-one': 'change', + 'select-multiple': 'change', + '_default_': 'click' + }[inputType || '_default_']; + } + + if (nodeName === 'option') { + element.parentNode.value = element.value; + element = element.parentNode; + eventType = 'change'; + } + + keys = keys || []; + function pressed(key) { + return keys.indexOf(key) !== -1; + } + + var evnt; + if (/transitionend/.test(eventType)) { + if (window.WebKitTransitionEvent) { + evnt = new window.WebKitTransitionEvent(eventType, eventData); + evnt.initEvent(eventType, eventData.bubbles, true); + } else { + try { + evnt = new window.TransitionEvent(eventType, eventData); + } catch (e) { + evnt = window.document.createEvent('TransitionEvent'); + evnt.initTransitionEvent(eventType, eventData.bubbles, null, null, eventData.elapsedTime || 0); + } + } + } else if (/animationend/.test(eventType)) { + if (window.WebKitAnimationEvent) { + evnt = new window.WebKitAnimationEvent(eventType, eventData); + evnt.initEvent(eventType, eventData.bubbles, true); + } else { + try { + evnt = new window.AnimationEvent(eventType, eventData); + } catch (e) { + evnt = window.document.createEvent('AnimationEvent'); + evnt.initAnimationEvent(eventType, eventData.bubbles, null, null, eventData.elapsedTime || 0); + } + } + } else if (/touch/.test(eventType) && supportsTouchEvents()) { + evnt = createTouchEvent(element, eventType, x, y); + } else if (/key/.test(eventType)) { + evnt = window.document.createEvent('Events'); + evnt.initEvent(eventType, eventData.bubbles, eventData.cancelable); + evnt.view = window; + evnt.ctrlKey = pressed('ctrl'); + evnt.altKey = pressed('alt'); + evnt.shiftKey = pressed('shift'); + evnt.metaKey = pressed('meta'); + evnt.keyCode = eventData.keyCode; + evnt.charCode = eventData.charCode; + evnt.which = eventData.which; + } else if (/composition/.test(eventType)) { + try { + evnt = new window.CompositionEvent(eventType, { + data: eventData.data + }); + } catch (e) { + // Support: IE9+ + evnt = window.document.createEvent('CompositionEvent', {}); + evnt.initCompositionEvent( + eventType, + eventData.bubbles, + eventData.cancelable, + window, + eventData.data, + null + ); + } + + } else { + evnt = window.document.createEvent('MouseEvents'); + x = x || 0; + y = y || 0; + evnt.initMouseEvent(eventType, true, true, window, 0, x, y, x, y, pressed('ctrl'), + pressed('alt'), pressed('shift'), pressed('meta'), 0, relatedTarget); + } + + /* we're unable to change the timeStamp value directly so this + * is only here to allow for testing where the timeStamp value is + * read */ + evnt.$manualTimeStamp = eventData.timeStamp; + + if (!evnt) return; + + if (!eventData.bubbles || supportsEventBubblingInDetachedTree() || isAttachedToDocument(element)) { + return element.dispatchEvent(evnt); + } else { + triggerForPath(element, evnt); + } + }; + + function supportsTouchEvents() { + if ('_cached' in supportsTouchEvents) { + return supportsTouchEvents._cached; + } + if (!window.document.createTouch || !window.document.createTouchList) { + supportsTouchEvents._cached = false; + return false; + } + try { + window.document.createEvent('TouchEvent'); + } catch (e) { + supportsTouchEvents._cached = false; + return false; + } + supportsTouchEvents._cached = true; + return true; + } + + function createTouchEvent(element, eventType, x, y) { + var evnt = new window.Event(eventType); + x = x || 0; + y = y || 0; + + var touch = window.document.createTouch(window, element, Date.now(), x, y, x, y); + var touches = window.document.createTouchList(touch); + + evnt.touches = touches; + + return evnt; + } + + function supportsEventBubblingInDetachedTree() { + if ('_cached' in supportsEventBubblingInDetachedTree) { + return supportsEventBubblingInDetachedTree._cached; + } + supportsEventBubblingInDetachedTree._cached = false; + var doc = window.document; + if (doc) { + var parent = doc.createElement('div'), + child = parent.cloneNode(); + parent.appendChild(child); + parent.addEventListener('e', function() { + supportsEventBubblingInDetachedTree._cached = true; + }); + var evnt = window.document.createEvent('Events'); + evnt.initEvent('e', true, true); + child.dispatchEvent(evnt); + } + return supportsEventBubblingInDetachedTree._cached; + } + + function triggerForPath(element, evnt) { + var stop = false; + + var _stopPropagation = evnt.stopPropagation; + evnt.stopPropagation = function() { + stop = true; + _stopPropagation.apply(evnt, arguments); + }; + patchEventTargetForBubbling(evnt, element); + do { + element.dispatchEvent(evnt); + // eslint-disable-next-line no-unmodified-loop-condition + } while (!stop && (element = element.parentNode)); + } + + function patchEventTargetForBubbling(event, target) { + event._target = target; + Object.defineProperty(event, 'target', {get: function() { return this._target;}}); + } + + function isAttachedToDocument(element) { + while ((element = element.parentNode)) { + if (element === window) { + return true; + } + } + return false; + } + })(); + + + })(window, window.angular); \ No newline at end of file diff --git a/tests/angular.min.js b/tests/angular.min.js new file mode 100644 index 000000000..011af0778 --- /dev/null +++ b/tests/angular.min.js @@ -0,0 +1,10778 @@ +!function(t) { + function e(r) { + if (n[r]) + return n[r].exports; + var i = n[r] = { + i: r, + l: !1, + exports: {} + }; + return t[r].call(i.exports, i, i.exports, e), + i.l = !0, + i.exports + } + var n = {}; + return e.m = t, + e.c = n, + e.d = function(t, n, r) { + e.o(t, n) || Object.defineProperty(t, n, { + configurable: !1, + enumerable: !0, + get: r + }) + } + , + e.n = function(t) { + var n = t && t.__esModule ? function() { + return t.default + } + : function() { + return t + } + ; + return e.d(n, "a", n), + n + } + , + e.o = function(t, e) { + return Object.prototype.hasOwnProperty.call(t, e) + } + , + e.p = "", + e(e.s = 0) +}([function(t, e, n) { + t.exports = n(1) +} +, function(t, e) { + !function(t) { + "use strict"; + function e(t) { + return w(t) ? (b(t.objectMaxDepth) && (yi.objectMaxDepth = n(t.objectMaxDepth) ? t.objectMaxDepth : NaN), + void (b(t.urlErrorParamsEnabled) && R(t.urlErrorParamsEnabled) && (yi.urlErrorParamsEnabled = t.urlErrorParamsEnabled))) : yi + } + function n(t) { + return C(t) && t > 0 + } + function r(t, e) { + e = e || Error; + var n = "https://errors.angularjs.org/1.8.2/" + , r = n.replace(".", "\\.") + "[\\s\\S]*" + , i = new RegExp(r,"g"); + return function() { + var r, o, a = arguments[0], s = arguments[1], u = "[" + (t ? t + ":" : "") + a + "] ", c = K(arguments, 2).map(function(t) { + return At(t, yi.objectMaxDepth) + }); + if (u += s.replace(/\{\d+\}/g, function(t) { + var e = +t.slice(1, -1); + return e < c.length ? c[e].replace(i, "") : t + }), + u += "\n" + n + (t ? t + "/" : "") + a, + yi.urlErrorParamsEnabled) + for (o = 0, + r = "?"; o < c.length; o++, + r = "&") + u += r + "p" + o + "=" + encodeURIComponent(c[o]); + return new e(u) + } + } + function i(t) { + if (null == t || T(t)) + return !1; + if (k(t) || E(t) || vi && t instanceof vi) + return !0; + var e = "length"in Object(t) && t.length; + return C(e) && (e >= 0 && e - 1 in t || "function" == typeof t.item) + } + function o(t, e, n) { + var r, a; + if (t) + if (O(t)) + for (r in t) + "prototype" !== r && "length" !== r && "name" !== r && t.hasOwnProperty(r) && e.call(n, t[r], r, t); + else if (k(t) || i(t)) { + var s = "object" != typeof t; + for (r = 0, + a = t.length; r < a; r++) + (s || r in t) && e.call(n, t[r], r, t) + } else if (t.forEach && t.forEach !== o) + t.forEach(e, n, t); + else if (x(t)) + for (r in t) + e.call(n, t[r], r, t); + else if ("function" == typeof t.hasOwnProperty) + for (r in t) + t.hasOwnProperty(r) && e.call(n, t[r], r, t); + else + for (r in t) + xi.call(t, r) && e.call(n, t[r], r, t); + return t + } + function a(t, e, n) { + for (var r = Object.keys(t).sort(), i = 0; i < r.length; i++) + e.call(n, t[r[i]], r[i]); + return r + } + function s(t) { + return function(e, n) { + t(n, e) + } + } + function u() { + return ++Ni + } + function c(t, e) { + e ? t.$$hashKey = e : delete t.$$hashKey + } + function l(t, e, n) { + for (var r = t.$$hashKey, i = 0, o = e.length; i < o; ++i) { + var a = e[i]; + if (w(a) || O(a)) + for (var s = Object.keys(a), u = 0, f = s.length; u < f; u++) { + var h = s[u] + , p = a[h]; + n && w(p) ? S(p) ? t[h] = new Date(p.valueOf()) : M(p) ? t[h] = new RegExp(p) : p.nodeName ? t[h] = p.cloneNode(!0) : D(p) ? t[h] = p.clone() : "__proto__" !== h && (w(t[h]) || (t[h] = k(p) ? [] : {}), + l(t[h], [p], !0)) : t[h] = p + } + } + return c(t, r), + t + } + function f(t) { + return l(t, Si.call(arguments, 1), !1) + } + function h(t) { + return l(t, Si.call(arguments, 1), !0) + } + function p(t) { + return parseInt(t, 10) + } + function d(t, e) { + return f(Object.create(t), e) + } + function $() {} + function v(t) { + return t + } + function m(t) { + return function() { + return t + } + } + function g(t) { + return O(t.toString) && t.toString !== Oi + } + function y(t) { + return "undefined" == typeof t + } + function b(t) { + return "undefined" != typeof t + } + function w(t) { + return null !== t && "object" == typeof t + } + function x(t) { + return null !== t && "object" == typeof t && !Mi(t) + } + function E(t) { + return "string" == typeof t + } + function C(t) { + return "number" == typeof t + } + function S(t) { + return "[object Date]" === Oi.call(t) + } + function k(t) { + return Array.isArray(t) || t instanceof Array + } + function A(t) { + var e = Oi.call(t); + switch (e) { + case "[object Error]": + return !0; + case "[object Exception]": + return !0; + case "[object DOMException]": + return !0; + default: + return t instanceof Error + } + } + function O(t) { + return "function" == typeof t + } + function M(t) { + return "[object RegExp]" === Oi.call(t) + } + function T(t) { + return t && t.window === t + } + function V(t) { + return t && t.$evalAsync && t.$watch + } + function N(t) { + return "[object File]" === Oi.call(t) + } + function j(t) { + return "[object FormData]" === Oi.call(t) + } + function I(t) { + return "[object Blob]" === Oi.call(t) + } + function R(t) { + return "boolean" == typeof t + } + function P(t) { + return t && O(t.then) + } + function U(t) { + return t && C(t.length) && Ii.test(Oi.call(t)) + } + function _(t) { + return "[object ArrayBuffer]" === Oi.call(t) + } + function D(t) { + return !(!t || !(t.nodeName || t.prop && t.attr && t.find)) + } + function L(t) { + var e, n = {}, r = t.split(","); + for (e = 0; e < r.length; e++) + n[r[e]] = !0; + return n + } + function q(t) { + return Ei(t.nodeName || t[0] && t[0].nodeName) + } + function F(t, e) { + return Array.prototype.indexOf.call(t, e) !== -1 + } + function H(t, e) { + var n = t.indexOf(e); + return n >= 0 && t.splice(n, 1), + n + } + function B(t, e, r) { + function i(t, e, n) { + if (n--, + n < 0) + return "..."; + var r, i = e.$$hashKey; + if (k(t)) + for (var o = 0, s = t.length; o < s; o++) + e.push(a(t[o], n)); + else if (x(t)) + for (r in t) + e[r] = a(t[r], n); + else if (t && "function" == typeof t.hasOwnProperty) + for (r in t) + t.hasOwnProperty(r) && (e[r] = a(t[r], n)); + else + for (r in t) + xi.call(t, r) && (e[r] = a(t[r], n)); + return c(e, i), + e + } + function a(t, e) { + if (!w(t)) + return t; + var n = u.indexOf(t); + if (n !== -1) + return l[n]; + if (T(t) || V(t)) + throw Ti("cpws", "Can't copy! Making copies of Window or Scope instances is not supported."); + var r = !1 + , o = s(t); + return void 0 === o && (o = k(t) ? [] : Object.create(Mi(t)), + r = !0), + u.push(t), + l.push(o), + r ? i(t, o, e) : o + } + function s(t) { + switch (Oi.call(t)) { + case "[object Int8Array]": + case "[object Int16Array]": + case "[object Int32Array]": + case "[object Float32Array]": + case "[object Float64Array]": + case "[object Uint8Array]": + case "[object Uint8ClampedArray]": + case "[object Uint16Array]": + case "[object Uint32Array]": + return new t.constructor(a(t.buffer),t.byteOffset,t.length); + case "[object ArrayBuffer]": + if (!t.slice) { + var e = new ArrayBuffer(t.byteLength); + return new Uint8Array(e).set(new Uint8Array(t)), + e + } + return t.slice(0); + case "[object Boolean]": + case "[object Number]": + case "[object String]": + case "[object Date]": + return new t.constructor(t.valueOf()); + case "[object RegExp]": + var n = new RegExp(t.source,t.toString().match(/[^\/]*$/)[0]); + return n.lastIndex = t.lastIndex, + n; + case "[object Blob]": + return new t.constructor([t],{ + type: t.type + }) + } + if (O(t.cloneNode)) + return t.cloneNode(!0) + } + var u = [] + , l = []; + if (r = n(r) ? r : NaN, + e) { + if (U(e) || _(e)) + throw Ti("cpta", "Can't copy! TypedArray destination cannot be mutated."); + if (t === e) + throw Ti("cpi", "Can't copy! Source and destination are identical."); + return k(e) ? e.length = 0 : o(e, function(t, n) { + "$$hashKey" !== n && delete e[n] + }), + u.push(t), + l.push(e), + i(t, e, r) + } + return a(t, r) + } + function z(t, e) { + return t === e || t !== t && e !== e + } + function W(t, e) { + if (t === e) + return !0; + if (null === t || null === e) + return !1; + if (t !== t && e !== e) + return !0; + var n, r, i, o = typeof t, a = typeof e; + if (o === a && "object" === o) { + if (!k(t)) { + if (S(t)) + return !!S(e) && z(t.getTime(), e.getTime()); + if (M(t)) + return !!M(e) && t.toString() === e.toString(); + if (V(t) || V(e) || T(t) || T(e) || k(e) || S(e) || M(e)) + return !1; + i = xt(); + for (r in t) + if ("$" !== r.charAt(0) && !O(t[r])) { + if (!W(t[r], e[r])) + return !1; + i[r] = !0 + } + for (r in e) + if (!(r in i) && "$" !== r.charAt(0) && b(e[r]) && !O(e[r])) + return !1; + return !0 + } + if (!k(e)) + return !1; + if ((n = t.length) === e.length) { + for (r = 0; r < n; r++) + if (!W(t[r], e[r])) + return !1; + return !0 + } + } + return !1 + } + function G(t, e, n) { + return t.concat(Si.call(e, n)) + } + function K(t, e) { + return Si.call(t, e || 0) + } + function J(t, e) { + var n = arguments.length > 2 ? K(arguments, 2) : []; + return !O(e) || e instanceof RegExp ? e : n.length ? function() { + return arguments.length ? e.apply(t, G(n, arguments, 0)) : e.apply(t, n) + } + : function() { + return arguments.length ? e.apply(t, arguments) : e.call(t) + } + } + function Z(e, n) { + var r = n; + return "string" == typeof e && "$" === e.charAt(0) && "$" === e.charAt(1) ? r = void 0 : T(n) ? r = "$WINDOW" : n && t.document === n ? r = "$DOCUMENT" : V(n) && (r = "$SCOPE"), + r + } + function Y(t, e) { + if (!y(t)) + return C(e) || (e = e ? 2 : null), + JSON.stringify(t, Z, e) + } + function X(t) { + return E(t) ? JSON.parse(t) : t + } + function Q(t, e) { + t = t.replace(Di, ""); + var n = Date.parse("Jan 01, 1970 00:00:00 " + t) / 6e4; + return ji(n) ? e : n + } + function tt(t, e) { + return t = new Date(t.getTime()), + t.setMinutes(t.getMinutes() + e), + t + } + function et(t, e, n) { + n = n ? -1 : 1; + var r = t.getTimezoneOffset() + , i = Q(e, r); + return tt(t, n * (i - r)) + } + function nt(t) { + t = vi(t).clone().empty(); + var e = vi("
").append(t).html(); + try { + return t[0].nodeType === Wi ? Ei(e) : e.match(/^(<[^>]+>)/)[1].replace(/^<([\w-]+)/, function(t, e) { + return "<" + Ei(e) + }) + } catch (t) { + return Ei(e) + } + } + function rt(t) { + try { + return decodeURIComponent(t) + } catch (t) {} + } + function it(t) { + var e = {}; + return o((t || "").split("&"), function(t) { + var n, r, i; + t && (r = t = t.replace(/\+/g, "%20"), + n = t.indexOf("="), + n !== -1 && (r = t.substring(0, n), + i = t.substring(n + 1)), + r = rt(r), + b(r) && (i = !b(i) || rt(i), + xi.call(e, r) ? k(e[r]) ? e[r].push(i) : e[r] = [e[r], i] : e[r] = i)) + }), + e + } + function ot(t) { + var e = []; + return o(t, function(t, n) { + k(t) ? o(t, function(t) { + e.push(st(n, !0) + (t === !0 ? "" : "=" + st(t, !0))) + }) : e.push(st(n, !0) + (t === !0 ? "" : "=" + st(t, !0))) + }), + e.length ? e.join("&") : "" + } + function at(t) { + return st(t, !0).replace(/%26/gi, "&").replace(/%3D/gi, "=").replace(/%2B/gi, "+") + } + function st(t, e) { + return encodeURIComponent(t).replace(/%40/gi, "@").replace(/%3A/gi, ":").replace(/%24/g, "$").replace(/%2C/gi, ",").replace(/%3B/gi, ";").replace(/%20/g, e ? "%20" : "+") + } + function ut(t, e) { + var n, r, i = Li.length; + for (r = 0; r < i; ++r) + if (n = Li[r] + e, + E(n = t.getAttribute(n))) + return n; + return null + } + function ct(e) { + var n = e.currentScript; + if (!n) + return !0; + if (!(n instanceof t.HTMLScriptElement || n instanceof t.SVGScriptElement)) + return !1; + var r = n.attributes + , i = [r.getNamedItem("src"), r.getNamedItem("href"), r.getNamedItem("xlink:href")]; + return i.every(function(t) { + if (!t) + return !0; + if (!t.value) + return !1; + var n = e.createElement("a"); + if (n.href = t.value, + e.location.origin === n.origin) + return !0; + switch (n.protocol) { + case "http:": + case "https:": + case "ftp:": + case "blob:": + case "file:": + case "data:": + return !0; + default: + return !1 + } + }) + } + function lt(e, n) { + var r, i, a = {}; + if (o(Li, function(t) { + var n = t + "app"; + !r && e.hasAttribute && e.hasAttribute(n) && (r = e, + i = e.getAttribute(n)) + }), + o(Li, function(t) { + var n, o = t + "app"; + !r && (n = e.querySelector("[" + o.replace(":", "\\:") + "]")) && (r = n, + i = n.getAttribute(o)) + }), + r) { + if (!qi) + return void t.console.error("AngularJS: disabling automatic bootstrap.