├── .gitignore
├── .jshintrc
├── .npmignore
├── .travis.yml
├── CONTRIBUTING.md
├── Gruntfile.js
├── LICENSE
├── README.md
├── bower.json
├── package-lock.json
├── package.json
├── server.js
├── src
├── loadCSS.js
└── onloadCSS.js
└── test
├── .htaccess
├── attributes.html
├── body.html
├── control.html
├── dom-append.html
├── import-head.html
├── import.html
├── index.html
├── mediatoggle.html
├── new-high.html
├── new-low.html
├── qunit
├── files
│ ├── preloadtest.css
│ └── test.css
├── index.html
├── libs
│ └── qunit
│ │ ├── qunit.css
│ │ └── qunit.js
└── tests.js
├── recommended.html
├── slow.css
├── test-onload.html
└── test.html
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "boss": true,
3 | "curly": true,
4 | "eqeqeq": true,
5 | "eqnull": true,
6 | "expr": true,
7 | "immed": true,
8 | "noarg": true,
9 | "smarttabs": true,
10 | "trailing": true,
11 | "undef": true,
12 | "unused": true,
13 | "node": true,
14 | "loopfunc": true,
15 |
16 | "browser": true,
17 | "qunit": true,
18 |
19 | "globals": {
20 | "loadCSS": false,
21 | "onloadCSS": false
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 12.6.0
4 | before_script:
5 | - npm install
6 | script: grunt -v
7 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to loadCSS
2 |
3 | Contributions are appreciated. In order for us to consider including a contribution, it does have to meet a few criteria:
4 |
5 | * Code is specific to one issue (eg. feature, extension or bug)
6 | * Code is formatted according to JavaScript Style Guide.
7 | * Code has full test coverage and all tests pass.
8 |
9 | ## Code to an Issue
10 |
11 | Use a separate git branch for each contribution. Give the branch a meaningful name.
12 | When you are contributing a new extensions use the name of this extension, like `dom-toggleclass`.
13 | Otherwise give it a descriptive name like `doc-generator` or reference a specific issue like `issues-12`.
14 | When the issue is resolved create a pull request to allow us to review and accept your contribution.
15 |
16 | ## JavaScript Style Guide
17 |
18 | Code should be formatted according to the [jQuery JavaScript Style Guide](http://contribute.jquery.org/style-guide/).
19 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | /* global module:false */
2 | module.exports = function(grunt) {
3 |
4 | require( 'matchdep' ).filterDev( ['grunt-*', '!grunt-cli'] ).forEach( grunt.loadNpmTasks );
5 |
6 | // Project configuration.
7 | grunt.initConfig({
8 | jshint: {
9 | all: {
10 | options: {
11 | jshintrc: ".jshintrc"
12 | },
13 |
14 | src: [
15 | '*.js',
16 | 'test/**/*.js',
17 | 'src/**/*.js',
18 | ]
19 | }
20 | },
21 | concat: {
22 | dist: {
23 | files: {
24 | 'dist/loadCSS.js': ['src/loadCSS.js'],
25 | 'dist/onloadCSS.js': ['src/onloadCSS.js']
26 | }
27 | }
28 | },
29 | uglify: {
30 | options: {
31 | preserveComments: /^\!/
32 | },
33 | dist: {
34 | files: {
35 | 'dist/loadCSS.min.js': ['src/loadCSS.js'],
36 | 'dist/onloadCSS.min.js': ['src/onloadCSS.js']
37 | }
38 | }
39 | },
40 | qunit: {
41 | files: ['test/qunit/**/*.html']
42 | }
43 | });
44 |
45 | grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']);
46 | grunt.registerTask('stage', ['default']);
47 | };
48 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) @scottjehl, 2016 Filament Group
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | :warning: This project is archived and the repository is no longer maintained.
2 |
3 | # loadCSS
4 |
5 | A pattern for loading CSS asynchronously
6 | [c]2020 @scottjehl, @zachleat [Filament Group, Inc.](https://www.filamentgroup.com/)
7 | Licensed MIT
8 |
9 | ## Why an ansychronous CSS loader?
10 |
11 | Referencing CSS stylesheets with `link[rel=stylesheet]` or `@import` causes browsers to delay page rendering while a stylesheet loads. When loading stylesheets that are not critical to the initial rendering of a page, this blocking behavior is undesirable. The pattern below allows us to fetch and apply CSS asynchronously. If necessary, this repo also offers a separate (and optional) JavaScript function for loading stylesheets dynamically.
12 |
13 |
14 | ## How to use
15 |
16 | As a primary pattern, we recommend loading asynchronous CSS like this from HTML:
17 |
18 | ``
19 |
20 | This article explains why this approach is best: https://www.filamentgroup.com/lab/load-css-simpler/
21 |
22 | That is probably all you need! But if you want to load a CSS file from a JavaScript function, read on...
23 |
24 | ## Dynamic CSS loading with the loadCSS function
25 |
26 | The [loadCSS.js](https://github.com/filamentgroup/loadCSS/blob/master/src/loadCSS.js) file exposes a global `loadCSS` function that you can call to load CSS files programmatically, if needed. This is handy for cases where you need to dynamically load CSS from script.
27 |
28 | ``` javascript
29 | loadCSS( "path/to/mystylesheet.css" );
30 | ```
31 |
32 | The code above will insert a new CSS stylesheet `link` *after* the last stylesheet or script that it finds in the page, and the function will return a reference to that `link` element, should you want to reference it later in your script. Multiple calls to loadCSS will reference CSS files in the order they are called, but keep in mind that they may finish loading in a different order than they were called.
33 |
34 | ## Function API
35 |
36 | The loadCSS function has 3 optional arguments.
37 |
38 | - `before`: By default, loadCSS attempts to inject the stylesheet link *after* all CSS and JS in the page. However, if you desire a more specific location in your document, such as before a particular stylesheet link, you can use the `before` argument to specify a particular element to use as an insertion point. Your stylesheet will be inserted *before* the element you specify. For example, here's how that can be done by simply applying an `id` attribute to your `script`.
39 | ```html
40 |
41 | ...
42 |
46 | ...
47 |
48 | ```
49 |
50 | - `media`: You can optionally pass a string to the media argument to set the `media=""` of the stylesheet - the default value is `all`.
51 | - `attributes`: You can also optionally pass an Object of attribute name/attribute value pairs to set on the stylesheet. This can be used to specify Subresource Integrity attributes:
52 | ```javascript
53 | loadCSS(
54 | "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css",
55 | null,
56 | null,
57 | {
58 | "crossorigin": "anonymous",
59 | "integrity": "sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
60 | }
61 | );
62 | ```
63 |
64 | #### Using with `onload`
65 |
66 | Onload event support for `link` elements is spotty in some browsers, so if you need to add an onload callback, include [`onloadCSS` function](https://github.com/filamentgroup/loadCSS/blob/master/src/onloadCSS.js) on your page and use the `onloadCSS` function:
67 |
68 | ```javascript
69 | var stylesheet = loadCSS( "path/to/mystylesheet.css" );
70 | onloadCSS( stylesheet, function() {
71 | console.log( "Stylesheet has loaded." );
72 | });
73 | ```
74 |
75 | ### Browser Support
76 |
77 | The loadCSS patterns attempt to load a css file asynchronously in any JavaScript-capable browser. However, some older browsers such as Internet Explorer 8 and older will block rendering while the stylesheet is loading. This merely means that the stylesheet will load as if you referenced it with an ordinary link element.
78 |
79 |
80 | # Changes in version 3.0 (no more preload polyfill)
81 |
82 | As of version 3.0, we no longer support or include a polyfill for a `rel=preload` markup pattern. This is because we have since determined that the markup pattern described at the top of this readme is simpler and better for performance, while the former preload pattern could sometimes conflict with resource priorities in ways that aren't helpful for loading CSS in a non-blocking way.
83 |
84 | To update, you can change your preload markup to [this HTML pattern](https://github.com/filamentgroup/loadCSS/blob/master/README.md#how-to-use) and delete the JS from your build.
85 |
86 | Since this change breaks the API from prior versions, we made it a major version bump. That way, if you are still needing to use the now-deprecated preload pattern, you can keep your code pointing at prior versions that are still on NPM, such as version 2.1.0 https://github.com/filamentgroup/loadCSS/releases/tag/v2.1.0
87 |
88 |
89 | #### Contributions and bug fixes
90 |
91 | Both are very much appreciated - especially bug fixes. As for contributions, the goals of this project are to keep things very simple and utilitarian, so if we don't accept a feature addition, it's not necessarily because it's a bad idea. It just may not meet the goals of the project. Thanks!
92 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "loadcss",
3 | "main": [
4 | "src/loadCSS.js"
5 | ],
6 | "ignore": [
7 | "**/.*"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fg-loadcss",
3 | "version": "3.1.0",
4 | "description": "A function for loading CSS asynchronously",
5 | "main": "src/loadCSS.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/filamentgroup/loadCSS.git"
9 | },
10 | "author": "Filament Group ",
11 | "license": "MIT",
12 | "bugs": {
13 | "url": "https://github.com/filamentgroup/loadCSS/issues"
14 | },
15 | "homepage": "https://github.com/filamentgroup/loadCSS",
16 | "engines": {
17 | "node": ">= 11.9.0"
18 | },
19 | "devDependencies": {
20 | "grunt": "^1.1.0",
21 | "grunt-cli": "~1.3.2",
22 | "grunt-contrib-concat": "^1.0.1",
23 | "grunt-contrib-jshint": "~2.0.0",
24 | "grunt-contrib-qunit": "^3.1.0",
25 | "grunt-contrib-uglify": "^4.0.0",
26 | "husky": "^1.3.1",
27 | "matchdep": "^2.0.0"
28 | },
29 | "scripts": {
30 | "start": "npx ./server.js",
31 | "test": "npx grunt"
32 | },
33 | "husky": {
34 | "hooks": {
35 | "pre-commit": "npm test"
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | /* jshint esversion: 6 */
2 |
3 | const fs = require('fs');
4 | const http = require('http');
5 | const path = require('path');
6 | const port = 3000;
7 |
8 | const server = http.createServer(requestHandler);
9 |
10 | server.listen(port, (err) => {
11 | if (err) {
12 | return console.error('could not run server', err);
13 | }
14 |
15 | console.log(`server is listening on ${port}`);
16 | });
17 |
18 |
19 | const contentTypes = {
20 | '.css': 'text/css',
21 | '.html': 'text/html',
22 | '.js': 'application/javascript',
23 | };
24 |
25 | function requestHandler (request, response) {
26 | console.log(JSON.stringify(request.url));
27 | try {
28 | response.setHeader('charset', 'UTF-8');
29 | response.setHeader('Cache-Control', 'max-age=500');
30 |
31 | if (!path.extname(request.url)) {
32 | request.url += '/index.html';
33 | }
34 |
35 | response.setHeader('Content-type', contentTypes[path.extname(request.url)]);
36 |
37 | const content = fs.readFileSync(
38 | path.join('.', request.url)
39 | ).toString().replace(//g, (match, filepath) => fs.readFileSync(
40 | path.resolve(path.dirname(path.join('.', request.url)), filepath)
41 | ));
42 |
43 | if (request.url.endsWith('slow.css')) {
44 | setTimeout(() => {
45 | response.end( content );
46 | }, 5000);
47 | } else {
48 | response.end( content );
49 | }
50 | } catch (error) {
51 | const errorMessage = (error.message && (error.message + '\n' + error.stack)) || error;
52 |
53 | if (errorMessage.includes('ENOENT')) {
54 | response.statusCode = 404;
55 | } else {
56 | response.statusCode = 500;
57 | }
58 |
59 | response.end('
' + errorMessage);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/loadCSS.js:
--------------------------------------------------------------------------------
1 | /*! loadCSS. [c]2020 Filament Group, Inc. MIT License */
2 | (function(w){
3 | "use strict";
4 | /* exported loadCSS */
5 | var loadCSS = function( href, before, media, attributes ){
6 | // Arguments explained:
7 | // `href` [REQUIRED] is the URL for your CSS file.
8 | // `before` [OPTIONAL] is the element the script should use as a reference for injecting our stylesheet before
9 | // By default, loadCSS attempts to inject the link after the last stylesheet or script in the DOM. However, you might desire a more specific location in your document.
10 | // `media` [OPTIONAL] is the media type or query of the stylesheet. By default it will be 'all'
11 | // `attributes` [OPTIONAL] is the Object of attribute name/attribute value pairs to set on the stylesheet's DOM Element.
12 | var doc = w.document;
13 | var ss = doc.createElement( "link" );
14 | var ref;
15 | if( before ){
16 | ref = before;
17 | }
18 | else {
19 | var refs = ( doc.body || doc.getElementsByTagName( "head" )[ 0 ] ).childNodes;
20 | ref = refs[ refs.length - 1];
21 | }
22 |
23 | var sheets = doc.styleSheets;
24 | // Set any of the provided attributes to the stylesheet DOM Element.
25 | if( attributes ){
26 | for( var attributeName in attributes ){
27 | if( attributes.hasOwnProperty( attributeName ) ){
28 | ss.setAttribute( attributeName, attributes[attributeName] );
29 | }
30 | }
31 | }
32 | ss.rel = "stylesheet";
33 | ss.href = href;
34 | // temporarily set media to something inapplicable to ensure it'll fetch without blocking render
35 | ss.media = "only x";
36 |
37 | // wait until body is defined before injecting link. This ensures a non-blocking load in IE11.
38 | function ready( cb ){
39 | if( doc.body ){
40 | return cb();
41 | }
42 | setTimeout(function(){
43 | ready( cb );
44 | });
45 | }
46 | // Inject link
47 | // Note: the ternary preserves the existing behavior of "before" argument, but we could choose to change the argument to "after" in a later release and standardize on ref.nextSibling for all refs
48 | // Note: `insertBefore` is used instead of `appendChild`, for safety re: http://www.paulirish.com/2011/surefire-dom-element-insertion/
49 | ready( function(){
50 | ref.parentNode.insertBefore( ss, ( before ? ref : ref.nextSibling ) );
51 | });
52 | // A method (exposed on return object for external use) that mimics onload by polling document.styleSheets until it includes the new sheet.
53 | var onloadcssdefined = function( cb ){
54 | var resolvedHref = ss.href;
55 | var i = sheets.length;
56 | while( i-- ){
57 | if( sheets[ i ].href === resolvedHref ){
58 | return cb();
59 | }
60 | }
61 | setTimeout(function() {
62 | onloadcssdefined( cb );
63 | });
64 | };
65 |
66 | function loadCB(){
67 | if( ss.addEventListener ){
68 | ss.removeEventListener( "load", loadCB );
69 | }
70 | ss.media = media || "all";
71 | }
72 |
73 | // once loaded, set link's media back to `all` so that the stylesheet applies once it loads
74 | if( ss.addEventListener ){
75 | ss.addEventListener( "load", loadCB);
76 | }
77 | ss.onloadcssdefined = onloadcssdefined;
78 | onloadcssdefined( loadCB );
79 | return ss;
80 | };
81 | // commonjs
82 | if( typeof exports !== "undefined" ){
83 | exports.loadCSS = loadCSS;
84 | }
85 | else {
86 | w.loadCSS = loadCSS;
87 | }
88 | }( typeof global !== "undefined" ? global : this ));
89 |
--------------------------------------------------------------------------------
/src/onloadCSS.js:
--------------------------------------------------------------------------------
1 | /*! onloadCSS. (onload callback for loadCSS) [c]2017 Filament Group, Inc. MIT License */
2 | /* global navigator */
3 | /* exported onloadCSS */
4 | function onloadCSS( ss, callback ) {
5 | var called;
6 | function newcb(){
7 | if( !called && callback ){
8 | called = true;
9 | callback.call( ss );
10 | }
11 | }
12 | if( ss.addEventListener ){
13 | ss.addEventListener( "load", newcb );
14 | }
15 | if( ss.attachEvent ){
16 | ss.attachEvent( "onload", newcb );
17 | }
18 |
19 | // This code is for browsers that don’t support onload
20 | // No support for onload (it'll bind but never fire):
21 | // * Android 4.3 (Samsung Galaxy S4, Browserstack)
22 | // * Android 4.2 Browser (Samsung Galaxy SIII Mini GT-I8200L)
23 | // * Android 2.3 (Pantech Burst P9070)
24 |
25 | // Weak inference targets Android < 4.4
26 | if( "isApplicationInstalled" in navigator && "onloadcssdefined" in ss ) {
27 | ss.onloadcssdefined( newcb );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/test/.htaccess:
--------------------------------------------------------------------------------
1 | # make sure our ssi includes work
2 | SSILegacyExprParser on
3 |
4 | # enabled SSI for .html files
5 | AddType text/html .html
6 | AddHandler server-parsed .html
7 | Options Indexes Includes FollowSymlinks
8 |
9 |
10 | # ----------------------------------------------------------------------
11 | # | Expires headers |
12 | # ----------------------------------------------------------------------
13 |
14 | # Serve resources with far-future expires headers.
15 | #
16 | # (!) If you don't control versioning with filename-based
17 | # cache busting, you should consider lowering the cache times
18 | # to something like one week.
19 | #
20 | # https://httpd.apache.org/docs/current/mod/mod_expires.html
21 |
22 |
23 |
24 | ExpiresActive on
25 | ExpiresDefault "access plus 1 minutes"
26 |
27 |
28 |
--------------------------------------------------------------------------------
/test/attributes.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Tests for "link" attributes
7 |
58 |
59 |
60 |
Tests for link attributes
61 |
Support for Subresource Integrity requires providing integrity and crossorigin attributes to the link Element.
62 |
By supplying an Object of attribue name/attribute value pairs as the fourth argument to loadCSS(), attributes can be set on the stylesheet's DOM Element.
This is a control test file to verify that an ordinary link to a slow-responding stylesheet will block render. If this text is not visible for 5 seconds or so, assumptions about CSS blocking render are valid in your browser.
This is a control test file to verify that a JavaScript appended link to a slow-responding stylesheet will block render. If this text is not visible for 5 seconds or so, assumptions about CSS blocking render are valid in your browser.
This is a control test file to verify that an ordinary link to a slow-responding stylesheet will block render. If this text is not visible for 5 seconds or so, assumptions about CSS blocking render are valid in your browser.
This is a control test file to verify that an ordinary link to a slow-responding stylesheet will block render. If this text is not visible for 5 seconds or so, assumptions about CSS blocking render are valid in your browser.
This directory contains a variety of files that employ various CSS loading techniques.
20 | Most files reference a CSS file that may include a 5-second server delay to mimic latency, when tested on a node server.
21 | The delay makes it easier to determine if content is rendered before the CSS loads.
This is a test page that references a stylesheet using a standard link[rel=stylesheet] with a print media type, which is swapped to all media when loaded..
Note: When run locally, the CSS file has a 5 second delay built into its server response time. If it is loaded in a non-blocking manner as desired, you should be able to read this text before the page is styled as white text on green background.
Instead of the usual print toggle, this page is loading CSS with rel="alternate stylesheet preload", which will load async at a high priority.
19 | On load, the rel is set to stylesheet and title is turned off, causing it to apply.
20 | What's nice about this pattern is you can lower the priority by removing the rel value of "preload" to decrease its priority from high to low. View low priority async demo
Instead of the usual print toggle, this page is loading CSS with rel="alternate stylesheet", which will load async at a low priority. Onload, the rel is set to stylesheet and title is turned off, causing it to apply.
19 | What's nice about this pattern is you can add another rel value of "preload" to increase its priority from low to high. View high priority async demo
";
974 |
975 | runLoggingCallbacks( "log", QUnit, details );
976 |
977 | config.current.assertions.push({
978 | result: false,
979 | message: output
980 | });
981 | },
982 |
983 | url: function( params ) {
984 | params = extend( extend( {}, QUnit.urlParams ), params );
985 | var key,
986 | querystring = "?";
987 |
988 | for ( key in params ) {
989 | if ( !hasOwn.call( params, key ) ) {
990 | continue;
991 | }
992 | querystring += encodeURIComponent( key ) + "=" +
993 | encodeURIComponent( params[ key ] ) + "&";
994 | }
995 | return window.location.protocol + "//" + window.location.host +
996 | window.location.pathname + querystring.slice( 0, -1 );
997 | },
998 |
999 | extend: extend,
1000 | id: id,
1001 | addEvent: addEvent
1002 | // load, equiv, jsDump, diff: Attached later
1003 | });
1004 |
1005 | /**
1006 | * @deprecated: Created for backwards compatibility with test runner that set the hook function
1007 | * into QUnit.{hook}, instead of invoking it and passing the hook function.
1008 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here.
1009 | * Doing this allows us to tell if the following methods have been overwritten on the actual
1010 | * QUnit object.
1011 | */
1012 | extend( QUnit.constructor.prototype, {
1013 |
1014 | // Logging callbacks; all receive a single argument with the listed properties
1015 | // run test/logs.html for any related changes
1016 | begin: registerLoggingCallback( "begin" ),
1017 |
1018 | // done: { failed, passed, total, runtime }
1019 | done: registerLoggingCallback( "done" ),
1020 |
1021 | // log: { result, actual, expected, message }
1022 | log: registerLoggingCallback( "log" ),
1023 |
1024 | // testStart: { name }
1025 | testStart: registerLoggingCallback( "testStart" ),
1026 |
1027 | // testDone: { name, failed, passed, total, duration }
1028 | testDone: registerLoggingCallback( "testDone" ),
1029 |
1030 | // moduleStart: { name }
1031 | moduleStart: registerLoggingCallback( "moduleStart" ),
1032 |
1033 | // moduleDone: { name, failed, passed, total }
1034 | moduleDone: registerLoggingCallback( "moduleDone" )
1035 | });
1036 |
1037 | if ( typeof document === "undefined" || document.readyState === "complete" ) {
1038 | config.autorun = true;
1039 | }
1040 |
1041 | QUnit.load = function() {
1042 | runLoggingCallbacks( "begin", QUnit, {} );
1043 |
1044 | // Initialize the config, saving the execution queue
1045 | var banner, filter, i, label, len, main, ol, toolbar, userAgent, val,
1046 | urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter,
1047 | numModules = 0,
1048 | moduleFilterHtml = "",
1049 | urlConfigHtml = "",
1050 | oldconfig = extend( {}, config );
1051 |
1052 | QUnit.init();
1053 | extend(config, oldconfig);
1054 |
1055 | config.blocking = false;
1056 |
1057 | len = config.urlConfig.length;
1058 |
1059 | for ( i = 0; i < len; i++ ) {
1060 | val = config.urlConfig[i];
1061 | if ( typeof val === "string" ) {
1062 | val = {
1063 | id: val,
1064 | label: val,
1065 | tooltip: "[no tooltip available]"
1066 | };
1067 | }
1068 | config[ val.id ] = QUnit.urlParams[ val.id ];
1069 | urlConfigHtml += "";
1075 | }
1076 |
1077 | moduleFilterHtml += "";
1090 |
1091 | // `userAgent` initialized at top of scope
1092 | userAgent = id( "qunit-userAgent" );
1093 | if ( userAgent ) {
1094 | userAgent.innerHTML = navigator.userAgent;
1095 | }
1096 |
1097 | // `banner` initialized at top of scope
1098 | banner = id( "qunit-header" );
1099 | if ( banner ) {
1100 | banner.innerHTML = "" + banner.innerHTML + " ";
1101 | }
1102 |
1103 | // `toolbar` initialized at top of scope
1104 | toolbar = id( "qunit-testrunner-toolbar" );
1105 | if ( toolbar ) {
1106 | // `filter` initialized at top of scope
1107 | filter = document.createElement( "input" );
1108 | filter.type = "checkbox";
1109 | filter.id = "qunit-filter-pass";
1110 |
1111 | addEvent( filter, "click", function() {
1112 | var tmp,
1113 | ol = document.getElementById( "qunit-tests" );
1114 |
1115 | if ( filter.checked ) {
1116 | ol.className = ol.className + " hidepass";
1117 | } else {
1118 | tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " ";
1119 | ol.className = tmp.replace( / hidepass /, " " );
1120 | }
1121 | if ( defined.sessionStorage ) {
1122 | if (filter.checked) {
1123 | sessionStorage.setItem( "qunit-filter-passed-tests", "true" );
1124 | } else {
1125 | sessionStorage.removeItem( "qunit-filter-passed-tests" );
1126 | }
1127 | }
1128 | });
1129 |
1130 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) {
1131 | filter.checked = true;
1132 | // `ol` initialized at top of scope
1133 | ol = document.getElementById( "qunit-tests" );
1134 | ol.className = ol.className + " hidepass";
1135 | }
1136 | toolbar.appendChild( filter );
1137 |
1138 | // `label` initialized at top of scope
1139 | label = document.createElement( "label" );
1140 | label.setAttribute( "for", "qunit-filter-pass" );
1141 | label.setAttribute( "title", "Only show tests and assertons that fail. Stored in sessionStorage." );
1142 | label.innerHTML = "Hide passed tests";
1143 | toolbar.appendChild( label );
1144 |
1145 | urlConfigCheckboxesContainer = document.createElement("span");
1146 | urlConfigCheckboxesContainer.innerHTML = urlConfigHtml;
1147 | urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input");
1148 | // For oldIE support:
1149 | // * Add handlers to the individual elements instead of the container
1150 | // * Use "click" instead of "change"
1151 | // * Fallback from event.target to event.srcElement
1152 | addEvents( urlConfigCheckboxes, "click", function( event ) {
1153 | var params = {},
1154 | target = event.target || event.srcElement;
1155 | params[ target.name ] = target.checked ? true : undefined;
1156 | window.location = QUnit.url( params );
1157 | });
1158 | toolbar.appendChild( urlConfigCheckboxesContainer );
1159 |
1160 | if (numModules > 1) {
1161 | moduleFilter = document.createElement( 'span' );
1162 | moduleFilter.setAttribute( 'id', 'qunit-modulefilter-container' );
1163 | moduleFilter.innerHTML = moduleFilterHtml;
1164 | addEvent( moduleFilter.lastChild, "change", function() {
1165 | var selectBox = moduleFilter.getElementsByTagName("select")[0],
1166 | selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value);
1167 |
1168 | window.location = QUnit.url( { module: ( selectedModule === "" ) ? undefined : selectedModule } );
1169 | });
1170 | toolbar.appendChild(moduleFilter);
1171 | }
1172 | }
1173 |
1174 | // `main` initialized at top of scope
1175 | main = id( "qunit-fixture" );
1176 | if ( main ) {
1177 | config.fixture = main.innerHTML;
1178 | }
1179 |
1180 | if ( config.autostart ) {
1181 | QUnit.start();
1182 | }
1183 | };
1184 |
1185 | addEvent( window, "load", QUnit.load );
1186 |
1187 | // `onErrorFnPrev` initialized at top of scope
1188 | // Preserve other handlers
1189 | onErrorFnPrev = window.onerror;
1190 |
1191 | // Cover uncaught exceptions
1192 | // Returning true will surpress the default browser handler,
1193 | // returning false will let it run.
1194 | window.onerror = function ( error, filePath, linerNr ) {
1195 | var ret = false;
1196 | if ( onErrorFnPrev ) {
1197 | ret = onErrorFnPrev( error, filePath, linerNr );
1198 | }
1199 |
1200 | // Treat return value as window.onerror itself does,
1201 | // Only do our handling if not surpressed.
1202 | if ( ret !== true ) {
1203 | if ( QUnit.config.current ) {
1204 | if ( QUnit.config.current.ignoreGlobalErrors ) {
1205 | return true;
1206 | }
1207 | QUnit.pushFailure( error, filePath + ":" + linerNr );
1208 | } else {
1209 | QUnit.test( "global failure", extend( function() {
1210 | QUnit.pushFailure( error, filePath + ":" + linerNr );
1211 | }, { validTest: validTest } ) );
1212 | }
1213 | return false;
1214 | }
1215 |
1216 | return ret;
1217 | };
1218 |
1219 | function done() {
1220 | config.autorun = true;
1221 |
1222 | // Log the last module results
1223 | if ( config.currentModule ) {
1224 | runLoggingCallbacks( "moduleDone", QUnit, {
1225 | name: config.currentModule,
1226 | failed: config.moduleStats.bad,
1227 | passed: config.moduleStats.all - config.moduleStats.bad,
1228 | total: config.moduleStats.all
1229 | });
1230 | }
1231 |
1232 | var i, key,
1233 | banner = id( "qunit-banner" ),
1234 | tests = id( "qunit-tests" ),
1235 | runtime = +new Date() - config.started,
1236 | passed = config.stats.all - config.stats.bad,
1237 | html = [
1238 | "Tests completed in ",
1239 | runtime,
1240 | " milliseconds. ",
1241 | "",
1242 | passed,
1243 | " assertions of ",
1244 | config.stats.all,
1245 | " passed, ",
1246 | config.stats.bad,
1247 | " failed."
1248 | ].join( "" );
1249 |
1250 | if ( banner ) {
1251 | banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" );
1252 | }
1253 |
1254 | if ( tests ) {
1255 | id( "qunit-testresult" ).innerHTML = html;
1256 | }
1257 |
1258 | if ( config.altertitle && typeof document !== "undefined" && document.title ) {
1259 | // show ✖ for good, ✔ for bad suite result in title
1260 | // use escape sequences in case file gets loaded with non-utf-8-charset
1261 | document.title = [
1262 | ( config.stats.bad ? "\u2716" : "\u2714" ),
1263 | document.title.replace( /^[\u2714\u2716] /i, "" )
1264 | ].join( " " );
1265 | }
1266 |
1267 | // clear own sessionStorage items if all tests passed
1268 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) {
1269 | // `key` & `i` initialized at top of scope
1270 | for ( i = 0; i < sessionStorage.length; i++ ) {
1271 | key = sessionStorage.key( i++ );
1272 | if ( key.indexOf( "qunit-test-" ) === 0 ) {
1273 | sessionStorage.removeItem( key );
1274 | }
1275 | }
1276 | }
1277 |
1278 | // scroll back to top to show results
1279 | if ( window.scrollTo ) {
1280 | window.scrollTo(0, 0);
1281 | }
1282 |
1283 | runLoggingCallbacks( "done", QUnit, {
1284 | failed: config.stats.bad,
1285 | passed: passed,
1286 | total: config.stats.all,
1287 | runtime: runtime
1288 | });
1289 | }
1290 |
1291 | /** @return Boolean: true if this test should be ran */
1292 | function validTest( test ) {
1293 | var include,
1294 | filter = config.filter && config.filter.toLowerCase(),
1295 | module = config.module && config.module.toLowerCase(),
1296 | fullName = (test.module + ": " + test.testName).toLowerCase();
1297 |
1298 | // Internally-generated tests are always valid
1299 | if ( test.callback && test.callback.validTest === validTest ) {
1300 | delete test.callback.validTest;
1301 | return true;
1302 | }
1303 |
1304 | if ( config.testNumber ) {
1305 | return test.testNumber === config.testNumber;
1306 | }
1307 |
1308 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) {
1309 | return false;
1310 | }
1311 |
1312 | if ( !filter ) {
1313 | return true;
1314 | }
1315 |
1316 | include = filter.charAt( 0 ) !== "!";
1317 | if ( !include ) {
1318 | filter = filter.slice( 1 );
1319 | }
1320 |
1321 | // If the filter matches, we need to honour include
1322 | if ( fullName.indexOf( filter ) !== -1 ) {
1323 | return include;
1324 | }
1325 |
1326 | // Otherwise, do the opposite
1327 | return !include;
1328 | }
1329 |
1330 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions)
1331 | // Later Safari and IE10 are supposed to support error.stack as well
1332 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
1333 | function extractStacktrace( e, offset ) {
1334 | offset = offset === undefined ? 3 : offset;
1335 |
1336 | var stack, include, i;
1337 |
1338 | if ( e.stacktrace ) {
1339 | // Opera
1340 | return e.stacktrace.split( "\n" )[ offset + 3 ];
1341 | } else if ( e.stack ) {
1342 | // Firefox, Chrome
1343 | stack = e.stack.split( "\n" );
1344 | if (/^error$/i.test( stack[0] ) ) {
1345 | stack.shift();
1346 | }
1347 | if ( fileName ) {
1348 | include = [];
1349 | for ( i = offset; i < stack.length; i++ ) {
1350 | if ( stack[ i ].indexOf( fileName ) !== -1 ) {
1351 | break;
1352 | }
1353 | include.push( stack[ i ] );
1354 | }
1355 | if ( include.length ) {
1356 | return include.join( "\n" );
1357 | }
1358 | }
1359 | return stack[ offset ];
1360 | } else if ( e.sourceURL ) {
1361 | // Safari, PhantomJS
1362 | // hopefully one day Safari provides actual stacktraces
1363 | // exclude useless self-reference for generated Error objects
1364 | if ( /qunit.js$/.test( e.sourceURL ) ) {
1365 | return;
1366 | }
1367 | // for actual exceptions, this is useful
1368 | return e.sourceURL + ":" + e.line;
1369 | }
1370 | }
1371 | function sourceFromStacktrace( offset ) {
1372 | try {
1373 | throw new Error();
1374 | } catch ( e ) {
1375 | return extractStacktrace( e, offset );
1376 | }
1377 | }
1378 |
1379 | /**
1380 | * Escape text for attribute or text content.
1381 | */
1382 | function escapeText( s ) {
1383 | if ( !s ) {
1384 | return "";
1385 | }
1386 | s = s + "";
1387 | // Both single quotes and double quotes (for attributes)
1388 | return s.replace( /['"<>&]/g, function( s ) {
1389 | switch( s ) {
1390 | case '\'':
1391 | return ''';
1392 | case '"':
1393 | return '"';
1394 | case '<':
1395 | return '<';
1396 | case '>':
1397 | return '>';
1398 | case '&':
1399 | return '&';
1400 | }
1401 | });
1402 | }
1403 |
1404 | function synchronize( callback, last ) {
1405 | config.queue.push( callback );
1406 |
1407 | if ( config.autorun && !config.blocking ) {
1408 | process( last );
1409 | }
1410 | }
1411 |
1412 | function process( last ) {
1413 | function next() {
1414 | process( last );
1415 | }
1416 | var start = new Date().getTime();
1417 | config.depth = config.depth ? config.depth + 1 : 1;
1418 |
1419 | while ( config.queue.length && !config.blocking ) {
1420 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) {
1421 | config.queue.shift()();
1422 | } else {
1423 | window.setTimeout( next, 13 );
1424 | break;
1425 | }
1426 | }
1427 | config.depth--;
1428 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
1429 | done();
1430 | }
1431 | }
1432 |
1433 | function saveGlobal() {
1434 | config.pollution = [];
1435 |
1436 | if ( config.noglobals ) {
1437 | for ( var key in window ) {
1438 | // in Opera sometimes DOM element ids show up here, ignore them
1439 | if ( !hasOwn.call( window, key ) || /^qunit-test-output/.test( key ) ) {
1440 | continue;
1441 | }
1442 | config.pollution.push( key );
1443 | }
1444 | }
1445 | }
1446 |
1447 | function checkPollution() {
1448 | var newGlobals,
1449 | deletedGlobals,
1450 | old = config.pollution;
1451 |
1452 | saveGlobal();
1453 |
1454 | newGlobals = diff( config.pollution, old );
1455 | if ( newGlobals.length > 0 ) {
1456 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") );
1457 | }
1458 |
1459 | deletedGlobals = diff( old, config.pollution );
1460 | if ( deletedGlobals.length > 0 ) {
1461 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") );
1462 | }
1463 | }
1464 |
1465 | // returns a new Array with the elements that are in a but not in b
1466 | function diff( a, b ) {
1467 | var i, j,
1468 | result = a.slice();
1469 |
1470 | for ( i = 0; i < result.length; i++ ) {
1471 | for ( j = 0; j < b.length; j++ ) {
1472 | if ( result[i] === b[j] ) {
1473 | result.splice( i, 1 );
1474 | i--;
1475 | break;
1476 | }
1477 | }
1478 | }
1479 | return result;
1480 | }
1481 |
1482 | function extend( a, b ) {
1483 | for ( var prop in b ) {
1484 | if ( b[ prop ] === undefined ) {
1485 | delete a[ prop ];
1486 |
1487 | // Avoid "Member not found" error in IE8 caused by setting window.constructor
1488 | } else if ( prop !== "constructor" || a !== window ) {
1489 | a[ prop ] = b[ prop ];
1490 | }
1491 | }
1492 |
1493 | return a;
1494 | }
1495 |
1496 | /**
1497 | * @param {HTMLElement} elem
1498 | * @param {string} type
1499 | * @param {Function} fn
1500 | */
1501 | function addEvent( elem, type, fn ) {
1502 | // Standards-based browsers
1503 | if ( elem.addEventListener ) {
1504 | elem.addEventListener( type, fn, false );
1505 | // IE
1506 | } else {
1507 | elem.attachEvent( "on" + type, fn );
1508 | }
1509 | }
1510 |
1511 | /**
1512 | * @param {Array|NodeList} elems
1513 | * @param {string} type
1514 | * @param {Function} fn
1515 | */
1516 | function addEvents( elems, type, fn ) {
1517 | var i = elems.length;
1518 | while ( i-- ) {
1519 | addEvent( elems[i], type, fn );
1520 | }
1521 | }
1522 |
1523 | function hasClass( elem, name ) {
1524 | return (" " + elem.className + " ").indexOf(" " + name + " ") > -1;
1525 | }
1526 |
1527 | function addClass( elem, name ) {
1528 | if ( !hasClass( elem, name ) ) {
1529 | elem.className += (elem.className ? " " : "") + name;
1530 | }
1531 | }
1532 |
1533 | function removeClass( elem, name ) {
1534 | var set = " " + elem.className + " ";
1535 | // Class name may appear multiple times
1536 | while ( set.indexOf(" " + name + " ") > -1 ) {
1537 | set = set.replace(" " + name + " " , " ");
1538 | }
1539 | // If possible, trim it for prettiness, but not neccecarily
1540 | elem.className = window.jQuery ? jQuery.trim( set ) : ( set.trim ? set.trim() : set );
1541 | }
1542 |
1543 | function id( name ) {
1544 | return !!( typeof document !== "undefined" && document && document.getElementById ) &&
1545 | document.getElementById( name );
1546 | }
1547 |
1548 | function registerLoggingCallback( key ) {
1549 | return function( callback ) {
1550 | config[key].push( callback );
1551 | };
1552 | }
1553 |
1554 | // Supports deprecated method of completely overwriting logging callbacks
1555 | function runLoggingCallbacks( key, scope, args ) {
1556 | var i, callbacks;
1557 | if ( QUnit.hasOwnProperty( key ) ) {
1558 | QUnit[ key ].call(scope, args );
1559 | } else {
1560 | callbacks = config[ key ];
1561 | for ( i = 0; i < callbacks.length; i++ ) {
1562 | callbacks[ i ].call( scope, args );
1563 | }
1564 | }
1565 | }
1566 |
1567 | // Test for equality any JavaScript type.
1568 | // Author: Philippe Rathé
1569 | QUnit.equiv = (function() {
1570 |
1571 | // Call the o related callback with the given arguments.
1572 | function bindCallbacks( o, callbacks, args ) {
1573 | var prop = QUnit.objectType( o );
1574 | if ( prop ) {
1575 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) {
1576 | return callbacks[ prop ].apply( callbacks, args );
1577 | } else {
1578 | return callbacks[ prop ]; // or undefined
1579 | }
1580 | }
1581 | }
1582 |
1583 | // the real equiv function
1584 | var innerEquiv,
1585 | // stack to decide between skip/abort functions
1586 | callers = [],
1587 | // stack to avoiding loops from circular referencing
1588 | parents = [],
1589 |
1590 | getProto = Object.getPrototypeOf || function ( obj ) {
1591 | return obj.__proto__;
1592 | },
1593 | callbacks = (function () {
1594 |
1595 | // for string, boolean, number and null
1596 | function useStrictEquality( b, a ) {
1597 | /*jshint eqeqeq:false */
1598 | if ( b instanceof a.constructor || a instanceof b.constructor ) {
1599 | // to catch short annotaion VS 'new' annotation of a
1600 | // declaration
1601 | // e.g. var i = 1;
1602 | // var j = new Number(1);
1603 | return a == b;
1604 | } else {
1605 | return a === b;
1606 | }
1607 | }
1608 |
1609 | return {
1610 | "string": useStrictEquality,
1611 | "boolean": useStrictEquality,
1612 | "number": useStrictEquality,
1613 | "null": useStrictEquality,
1614 | "undefined": useStrictEquality,
1615 |
1616 | "nan": function( b ) {
1617 | return isNaN( b );
1618 | },
1619 |
1620 | "date": function( b, a ) {
1621 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf();
1622 | },
1623 |
1624 | "regexp": function( b, a ) {
1625 | return QUnit.objectType( b ) === "regexp" &&
1626 | // the regex itself
1627 | a.source === b.source &&
1628 | // and its modifers
1629 | a.global === b.global &&
1630 | // (gmi) ...
1631 | a.ignoreCase === b.ignoreCase &&
1632 | a.multiline === b.multiline &&
1633 | a.sticky === b.sticky;
1634 | },
1635 |
1636 | // - skip when the property is a method of an instance (OOP)
1637 | // - abort otherwise,
1638 | // initial === would have catch identical references anyway
1639 | "function": function() {
1640 | var caller = callers[callers.length - 1];
1641 | return caller !== Object && typeof caller !== "undefined";
1642 | },
1643 |
1644 | "array": function( b, a ) {
1645 | var i, j, len, loop;
1646 |
1647 | // b could be an object literal here
1648 | if ( QUnit.objectType( b ) !== "array" ) {
1649 | return false;
1650 | }
1651 |
1652 | len = a.length;
1653 | if ( len !== b.length ) {
1654 | // safe and faster
1655 | return false;
1656 | }
1657 |
1658 | // track reference to avoid circular references
1659 | parents.push( a );
1660 | for ( i = 0; i < len; i++ ) {
1661 | loop = false;
1662 | for ( j = 0; j < parents.length; j++ ) {
1663 | if ( parents[j] === a[i] ) {
1664 | loop = true;// dont rewalk array
1665 | }
1666 | }
1667 | if ( !loop && !innerEquiv(a[i], b[i]) ) {
1668 | parents.pop();
1669 | return false;
1670 | }
1671 | }
1672 | parents.pop();
1673 | return true;
1674 | },
1675 |
1676 | "object": function( b, a ) {
1677 | var i, j, loop,
1678 | // Default to true
1679 | eq = true,
1680 | aProperties = [],
1681 | bProperties = [];
1682 |
1683 | // comparing constructors is more strict than using
1684 | // instanceof
1685 | if ( a.constructor !== b.constructor ) {
1686 | // Allow objects with no prototype to be equivalent to
1687 | // objects with Object as their constructor.
1688 | if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) ||
1689 | ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) {
1690 | return false;
1691 | }
1692 | }
1693 |
1694 | // stack constructor before traversing properties
1695 | callers.push( a.constructor );
1696 | // track reference to avoid circular references
1697 | parents.push( a );
1698 |
1699 | for ( i in a ) { // be strict: don't ensures hasOwnProperty
1700 | // and go deep
1701 | loop = false;
1702 | for ( j = 0; j < parents.length; j++ ) {
1703 | if ( parents[j] === a[i] ) {
1704 | // don't go down the same path twice
1705 | loop = true;
1706 | }
1707 | }
1708 | aProperties.push(i); // collect a's properties
1709 |
1710 | if (!loop && !innerEquiv( a[i], b[i] ) ) {
1711 | eq = false;
1712 | break;
1713 | }
1714 | }
1715 |
1716 | callers.pop(); // unstack, we are done
1717 | parents.pop();
1718 |
1719 | for ( i in b ) {
1720 | bProperties.push( i ); // collect b's properties
1721 | }
1722 |
1723 | // Ensures identical properties name
1724 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() );
1725 | }
1726 | };
1727 | }());
1728 |
1729 | innerEquiv = function() { // can take multiple arguments
1730 | var args = [].slice.apply( arguments );
1731 | if ( args.length < 2 ) {
1732 | return true; // end transition
1733 | }
1734 |
1735 | return (function( a, b ) {
1736 | if ( a === b ) {
1737 | return true; // catch the most you can
1738 | } else if ( a === null || b === null || typeof a === "undefined" ||
1739 | typeof b === "undefined" ||
1740 | QUnit.objectType(a) !== QUnit.objectType(b) ) {
1741 | return false; // don't lose time with error prone cases
1742 | } else {
1743 | return bindCallbacks(a, callbacks, [ b, a ]);
1744 | }
1745 |
1746 | // apply transition with (1..n) arguments
1747 | }( args[0], args[1] ) && arguments.callee.apply( this, args.splice(1, args.length - 1 )) );
1748 | };
1749 |
1750 | return innerEquiv;
1751 | }());
1752 |
1753 | /**
1754 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com |
1755 | * http://flesler.blogspot.com Licensed under BSD
1756 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008
1757 | *
1758 | * @projectDescription Advanced and extensible data dumping for Javascript.
1759 | * @version 1.0.0
1760 | * @author Ariel Flesler
1761 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
1762 | */
1763 | QUnit.jsDump = (function() {
1764 | function quote( str ) {
1765 | return '"' + str.toString().replace( /"/g, '\\"' ) + '"';
1766 | }
1767 | function literal( o ) {
1768 | return o + "";
1769 | }
1770 | function join( pre, arr, post ) {
1771 | var s = jsDump.separator(),
1772 | base = jsDump.indent(),
1773 | inner = jsDump.indent(1);
1774 | if ( arr.join ) {
1775 | arr = arr.join( "," + s + inner );
1776 | }
1777 | if ( !arr ) {
1778 | return pre + post;
1779 | }
1780 | return [ pre, inner + arr, base + post ].join(s);
1781 | }
1782 | function array( arr, stack ) {
1783 | var i = arr.length, ret = new Array(i);
1784 | this.up();
1785 | while ( i-- ) {
1786 | ret[i] = this.parse( arr[i] , undefined , stack);
1787 | }
1788 | this.down();
1789 | return join( "[", ret, "]" );
1790 | }
1791 |
1792 | var reName = /^function (\w+)/,
1793 | jsDump = {
1794 | // type is used mostly internally, you can fix a (custom)type in advance
1795 | parse: function( obj, type, stack ) {
1796 | stack = stack || [ ];
1797 | var inStack, res,
1798 | parser = this.parsers[ type || this.typeOf(obj) ];
1799 |
1800 | type = typeof parser;
1801 | inStack = inArray( obj, stack );
1802 |
1803 | if ( inStack !== -1 ) {
1804 | return "recursion(" + (inStack - stack.length) + ")";
1805 | }
1806 | if ( type === "function" ) {
1807 | stack.push( obj );
1808 | res = parser.call( this, obj, stack );
1809 | stack.pop();
1810 | return res;
1811 | }
1812 | return ( type === "string" ) ? parser : this.parsers.error;
1813 | },
1814 | typeOf: function( obj ) {
1815 | var type;
1816 | if ( obj === null ) {
1817 | type = "null";
1818 | } else if ( typeof obj === "undefined" ) {
1819 | type = "undefined";
1820 | } else if ( QUnit.is( "regexp", obj) ) {
1821 | type = "regexp";
1822 | } else if ( QUnit.is( "date", obj) ) {
1823 | type = "date";
1824 | } else if ( QUnit.is( "function", obj) ) {
1825 | type = "function";
1826 | } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) {
1827 | type = "window";
1828 | } else if ( obj.nodeType === 9 ) {
1829 | type = "document";
1830 | } else if ( obj.nodeType ) {
1831 | type = "node";
1832 | } else if (
1833 | // native arrays
1834 | toString.call( obj ) === "[object Array]" ||
1835 | // NodeList objects
1836 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) )
1837 | ) {
1838 | type = "array";
1839 | } else if ( obj.constructor === Error.prototype.constructor ) {
1840 | type = "error";
1841 | } else {
1842 | type = typeof obj;
1843 | }
1844 | return type;
1845 | },
1846 | separator: function() {
1847 | return this.multiline ? this.HTML ? " " : "\n" : this.HTML ? " " : " ";
1848 | },
1849 | // extra can be a number, shortcut for increasing-calling-decreasing
1850 | indent: function( extra ) {
1851 | if ( !this.multiline ) {
1852 | return "";
1853 | }
1854 | var chr = this.indentChar;
1855 | if ( this.HTML ) {
1856 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " );
1857 | }
1858 | return new Array( this._depth_ + (extra||0) ).join(chr);
1859 | },
1860 | up: function( a ) {
1861 | this._depth_ += a || 1;
1862 | },
1863 | down: function( a ) {
1864 | this._depth_ -= a || 1;
1865 | },
1866 | setParser: function( name, parser ) {
1867 | this.parsers[name] = parser;
1868 | },
1869 | // The next 3 are exposed so you can use them
1870 | quote: quote,
1871 | literal: literal,
1872 | join: join,
1873 | //
1874 | _depth_: 1,
1875 | // This is the list of parsers, to modify them, use jsDump.setParser
1876 | parsers: {
1877 | window: "[Window]",
1878 | document: "[Document]",
1879 | error: function(error) {
1880 | return "Error(\"" + error.message + "\")";
1881 | },
1882 | unknown: "[Unknown]",
1883 | "null": "null",
1884 | "undefined": "undefined",
1885 | "function": function( fn ) {
1886 | var ret = "function",
1887 | // functions never have name in IE
1888 | name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1];
1889 |
1890 | if ( name ) {
1891 | ret += " " + name;
1892 | }
1893 | ret += "( ";
1894 |
1895 | ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" );
1896 | return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" );
1897 | },
1898 | array: array,
1899 | nodelist: array,
1900 | "arguments": array,
1901 | object: function( map, stack ) {
1902 | var ret = [ ], keys, key, val, i;
1903 | QUnit.jsDump.up();
1904 | keys = [];
1905 | for ( key in map ) {
1906 | keys.push( key );
1907 | }
1908 | keys.sort();
1909 | for ( i = 0; i < keys.length; i++ ) {
1910 | key = keys[ i ];
1911 | val = map[ key ];
1912 | ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) );
1913 | }
1914 | QUnit.jsDump.down();
1915 | return join( "{", ret, "}" );
1916 | },
1917 | node: function( node ) {
1918 | var len, i, val,
1919 | open = QUnit.jsDump.HTML ? "<" : "<",
1920 | close = QUnit.jsDump.HTML ? ">" : ">",
1921 | tag = node.nodeName.toLowerCase(),
1922 | ret = open + tag,
1923 | attrs = node.attributes;
1924 |
1925 | if ( attrs ) {
1926 | for ( i = 0, len = attrs.length; i < len; i++ ) {
1927 | val = attrs[i].nodeValue;
1928 | // IE6 includes all attributes in .attributes, even ones not explicitly set.
1929 | // Those have values like undefined, null, 0, false, "" or "inherit".
1930 | if ( val && val !== "inherit" ) {
1931 | ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" );
1932 | }
1933 | }
1934 | }
1935 | ret += close;
1936 |
1937 | // Show content of TextNode or CDATASection
1938 | if ( node.nodeType === 3 || node.nodeType === 4 ) {
1939 | ret += node.nodeValue;
1940 | }
1941 |
1942 | return ret + open + "/" + tag + close;
1943 | },
1944 | // function calls it internally, it's the arguments part of the function
1945 | functionArgs: function( fn ) {
1946 | var args,
1947 | l = fn.length;
1948 |
1949 | if ( !l ) {
1950 | return "";
1951 | }
1952 |
1953 | args = new Array(l);
1954 | while ( l-- ) {
1955 | // 97 is 'a'
1956 | args[l] = String.fromCharCode(97+l);
1957 | }
1958 | return " " + args.join( ", " ) + " ";
1959 | },
1960 | // object calls it internally, the key part of an item in a map
1961 | key: quote,
1962 | // function calls it internally, it's the content of the function
1963 | functionCode: "[code]",
1964 | // node calls it internally, it's an html attribute value
1965 | attribute: quote,
1966 | string: quote,
1967 | date: quote,
1968 | regexp: literal,
1969 | number: literal,
1970 | "boolean": literal
1971 | },
1972 | // if true, entities are escaped ( <, >, \t, space and \n )
1973 | HTML: false,
1974 | // indentation unit
1975 | indentChar: " ",
1976 | // if true, items in a collection, are separated by a \n, else just a space.
1977 | multiline: true
1978 | };
1979 |
1980 | return jsDump;
1981 | }());
1982 |
1983 | // from jquery.js
1984 | function inArray( elem, array ) {
1985 | if ( array.indexOf ) {
1986 | return array.indexOf( elem );
1987 | }
1988 |
1989 | for ( var i = 0, length = array.length; i < length; i++ ) {
1990 | if ( array[ i ] === elem ) {
1991 | return i;
1992 | }
1993 | }
1994 |
1995 | return -1;
1996 | }
1997 |
1998 | /*
1999 | * Javascript Diff Algorithm
2000 | * By John Resig (http://ejohn.org/)
2001 | * Modified by Chu Alan "sprite"
2002 | *
2003 | * Released under the MIT license.
2004 | *
2005 | * More Info:
2006 | * http://ejohn.org/projects/javascript-diff-algorithm/
2007 | *
2008 | * Usage: QUnit.diff(expected, actual)
2009 | *
2010 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over"
2011 | */
2012 | QUnit.diff = (function() {
2013 | /*jshint eqeqeq:false, eqnull:true */
2014 | function diff( o, n ) {
2015 | var i,
2016 | ns = {},
2017 | os = {};
2018 |
2019 | for ( i = 0; i < n.length; i++ ) {
2020 | if ( !hasOwn.call( ns, n[i] ) ) {
2021 | ns[ n[i] ] = {
2022 | rows: [],
2023 | o: null
2024 | };
2025 | }
2026 | ns[ n[i] ].rows.push( i );
2027 | }
2028 |
2029 | for ( i = 0; i < o.length; i++ ) {
2030 | if ( !hasOwn.call( os, o[i] ) ) {
2031 | os[ o[i] ] = {
2032 | rows: [],
2033 | n: null
2034 | };
2035 | }
2036 | os[ o[i] ].rows.push( i );
2037 | }
2038 |
2039 | for ( i in ns ) {
2040 | if ( !hasOwn.call( ns, i ) ) {
2041 | continue;
2042 | }
2043 | if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) {
2044 | n[ ns[i].rows[0] ] = {
2045 | text: n[ ns[i].rows[0] ],
2046 | row: os[i].rows[0]
2047 | };
2048 | o[ os[i].rows[0] ] = {
2049 | text: o[ os[i].rows[0] ],
2050 | row: ns[i].rows[0]
2051 | };
2052 | }
2053 | }
2054 |
2055 | for ( i = 0; i < n.length - 1; i++ ) {
2056 | if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null &&
2057 | n[ i + 1 ] == o[ n[i].row + 1 ] ) {
2058 |
2059 | n[ i + 1 ] = {
2060 | text: n[ i + 1 ],
2061 | row: n[i].row + 1
2062 | };
2063 | o[ n[i].row + 1 ] = {
2064 | text: o[ n[i].row + 1 ],
2065 | row: i + 1
2066 | };
2067 | }
2068 | }
2069 |
2070 | for ( i = n.length - 1; i > 0; i-- ) {
2071 | if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null &&
2072 | n[ i - 1 ] == o[ n[i].row - 1 ]) {
2073 |
2074 | n[ i - 1 ] = {
2075 | text: n[ i - 1 ],
2076 | row: n[i].row - 1
2077 | };
2078 | o[ n[i].row - 1 ] = {
2079 | text: o[ n[i].row - 1 ],
2080 | row: i - 1
2081 | };
2082 | }
2083 | }
2084 |
2085 | return {
2086 | o: o,
2087 | n: n
2088 | };
2089 | }
2090 |
2091 | return function( o, n ) {
2092 | o = o.replace( /\s+$/, "" );
2093 | n = n.replace( /\s+$/, "" );
2094 |
2095 | var i, pre,
2096 | str = "",
2097 | out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ),
2098 | oSpace = o.match(/\s+/g),
2099 | nSpace = n.match(/\s+/g);
2100 |
2101 | if ( oSpace == null ) {
2102 | oSpace = [ " " ];
2103 | }
2104 | else {
2105 | oSpace.push( " " );
2106 | }
2107 |
2108 | if ( nSpace == null ) {
2109 | nSpace = [ " " ];
2110 | }
2111 | else {
2112 | nSpace.push( " " );
2113 | }
2114 |
2115 | if ( out.n.length === 0 ) {
2116 | for ( i = 0; i < out.o.length; i++ ) {
2117 | str += "" + out.o[i] + oSpace[i] + "";
2118 | }
2119 | }
2120 | else {
2121 | if ( out.n[0].text == null ) {
2122 | for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) {
2123 | str += "" + out.o[n] + oSpace[n] + "";
2124 | }
2125 | }
2126 |
2127 | for ( i = 0; i < out.n.length; i++ ) {
2128 | if (out.n[i].text == null) {
2129 | str += "" + out.n[i] + nSpace[i] + "";
2130 | }
2131 | else {
2132 | // `pre` initialized at top of scope
2133 | pre = "";
2134 |
2135 | for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) {
2136 | pre += "" + out.o[n] + oSpace[n] + "";
2137 | }
2138 | str += " " + out.n[i].text + nSpace[i] + pre;
2139 | }
2140 | }
2141 | }
2142 |
2143 | return str;
2144 | };
2145 | }());
2146 |
2147 | // for CommonJS enviroments, export everything
2148 | if ( typeof exports !== "undefined" ) {
2149 | extend( exports, QUnit );
2150 | }
2151 |
2152 | // get at whatever the global object is, like window in browsers
2153 | }( (function() {return this;}.call()) ));
2154 |
--------------------------------------------------------------------------------
/test/qunit/tests.js:
--------------------------------------------------------------------------------
1 | (function(window) {
2 | /*
3 | ======== A Handy Little QUnit Reference ========
4 | http://api.qunitjs.com/
5 |
6 | Test methods:
7 | module(name, {[setup][ ,teardown]})
8 | test(name, callback)
9 | expect(numberOfAssertions)
10 | stop(increment)
11 | start(decrement)
12 | Test assertions:
13 | ok(value, [message])
14 | equal(actual, expected, [message])
15 | notEqual(actual, expected, [message])
16 | deepEqual(actual, expected, [message])
17 | notDeepEqual(actual, expected, [message])
18 | strictEqual(actual, expected, [message])
19 | notStrictEqual(actual, expected, [message])
20 | throws(block, [expected], [message])
21 | */
22 |
23 | // initial value of media attr
24 | var initialMedia = "only x";
25 |
26 | test( 'function loadCSS exists', function(){
27 | expect(2);
28 | ok( window.loadCSS, "loadCSS should exist on the window object" );
29 | ok( typeof window.loadCSS === "function", "loadCSS should be a function" );
30 | });
31 |
32 | asyncTest( 'loadCSS loads a CSS file', function(){
33 | expect(1);
34 | var ss = loadCSS("files/test.css");
35 | onloadCSS( ss, function(){
36 | ok("stylesheet loaded successfully");
37 | start();
38 | });
39 | });
40 |
41 | asyncTest( 'loadCSS loads a CSS file with a relative path', function(){
42 | expect(1);
43 | var ss = loadCSS("../../test/qunit/files/test.css");
44 | onloadCSS( ss, function(){
45 | ok("stylesheet loaded successfully");
46 | start();
47 | });
48 | });
49 |
50 | asyncTest( 'loadCSS loads a CSS file with specific attributes', function(){
51 | expect(3);
52 | var attributes = {
53 | title: "Default Style",
54 | type: "text/css"
55 | };
56 | var ss = loadCSS("files/test.css", null, null, attributes);
57 | onloadCSS( ss, function(){
58 | ok("stylesheet loaded successfully");
59 | equal(ss.title, attributes.title, "'title' attribute should be '" + attributes.title + "'");
60 | equal(ss.type, attributes.type, "'type' attribute should be '" + attributes.type + "'");
61 | start();
62 | });
63 | });
64 |
65 | asyncTest( 'loadCSS sets media type before and after the stylesheet is loaded', function(){
66 | expect(2);
67 | var ss = loadCSS("files/test.css");
68 | ok(ss.media, initialMedia, "media type begins as" + initialMedia );
69 | onloadCSS( ss, function(){
70 | equal(ss.media, "all", "media type is all");
71 | start();
72 | });
73 | });
74 |
75 | asyncTest( 'loadCSS sets media type to a custom value if specified, after load', function(){
76 | expect(2);
77 | var med = "print";
78 | var ss = loadCSS("files/test.css", null, med);
79 | ok(ss.media, initialMedia, "media type begins as " + initialMedia );
80 | onloadCSS( ss, function(){
81 | equal(ss.media, med, "media type is " + med);
82 | start();
83 | });
84 | });
85 |
86 | test( 'loadCSS injects before a particular specified element', function(){
87 | expect(1);
88 | var elem = window.document.getElementById("before-test");
89 | var ss = loadCSS("files/test.css", elem);
90 | equal(ss.nextElementSibling, elem );
91 | });
92 |
93 | asyncTest( 'onloadCSS callback fires after css is loaded', function(){
94 | expect(1);
95 | var getStyles = window.getComputedStyle ? function (node) { return window.getComputedStyle(node, null); } : function (node) { return node.currentStyle; };
96 | var elem = window.document.createElement("div");
97 | elem.className = "bar";
98 | document.body.appendChild( elem );
99 | var ss = loadCSS("files/test.css?1");
100 | onloadCSS( ss, function(){
101 | equal(getStyles(elem).backgroundColor, 'rgb(0, 128, 0)', 'background is green' );
102 | start();
103 | } );
104 | });
105 |
106 |
107 | }(window));
108 |
--------------------------------------------------------------------------------
/test/recommended.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Recommended Pattern Test
5 |
6 |
7 |
8 |
9 |
This is a test file to demonstrate the recommended pattern for loading a stylesheet asynchronously.
10 |
11 |
12 |
--------------------------------------------------------------------------------
/test/slow.css:
--------------------------------------------------------------------------------
1 | /* This file was delivered after a purposeful 5 second delay to demonstrate latency. */
2 | body { background: green; color: #fff; }
3 | a { color: #fff;}
--------------------------------------------------------------------------------
/test/test-onload.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Blocking Test
5 |
6 |
14 |
15 |
16 |
This is a test to see whether or not a loadCSS call will block render. This page uses loadCss to load a CSS file that has a 5 second delay built into its server response time. If loadCSS works properly, you should be able to read this text before the page is styled as white text on green background.
This is a test to see whether or not a loadCSS call will block render. This page uses loadCss to load a CSS file that has a 5 second delay built into its server response time. If loadCSS works properly, you should be able to read this text before the page is styled as white text on green background.