diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..2740db33
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+# This file is for unifying the coding style for different editors and IDEs
+# editorconfig.org
+
+root = true
+
+[*]
+indent_style = tab
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.yml]
+indent_style = space
+indent_size = 2
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..aa2f7456
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,13 @@
+version: 2
+updates:
+ - package-ecosystem: github-actions
+ directory: "/"
+ schedule:
+ interval: monthly
+
+ # Group all dependabot version update PRs into one
+ groups:
+ github-actions:
+ applies-to: version-updates
+ patterns:
+ - "*"
diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml
new file mode 100644
index 00000000..d075b4af
--- /dev/null
+++ b/.github/workflows/browser-tests.yml
@@ -0,0 +1,119 @@
+name: Browser Tests
+
+on:
+ pull_request:
+ push:
+ branches:
+ - main
+
+env:
+ NODE_VERSION: 22.x
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ name: ${{ matrix.NAME || matrix.BROWSER }} (${{ matrix.MIGRATE_VERSION }} Migrate)
+ strategy:
+ fail-fast: false
+ matrix:
+ NAME: [""]
+ BROWSER: ["chrome", "firefox"]
+ MIGRATE_VERSION: ["min"]
+ include:
+ - NAME: ""
+ BROWSER: "chrome"
+ MIGRATE_VERSION: "esmodules"
+ - NAME: "Firefox ESR (new)"
+ BROWSER: "firefox"
+ MIGRATE_VERSION: "min"
+ - NAME: "Firefox ESR (old)"
+ BROWSER: "firefox"
+ MIGRATE_VERSION: "min"
+ steps:
+ - name: Checkout
+ uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
+
+ - name: Use Node.js ${{ env.NODE_VERSION }}
+ uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+ cache-dependency-path: '**/package-lock.json'
+
+ - name: Set download URL for Firefox ESR (old)
+ run: |
+ echo "FIREFOX_SOURCE_URL=https://download.mozilla.org/?product=firefox-esr-latest-ssl&lang=en-US&os=linux64" >> "$GITHUB_ENV"
+ if: contains(matrix.NAME, 'Firefox ESR (old)')
+
+ - name: Set download URL for Firefox ESR (new)
+ run: |
+ echo "FIREFOX_SOURCE_URL=https://download.mozilla.org/?product=firefox-esr-next-latest-ssl&lang=en-US&os=linux64" >> "$GITHUB_ENV"
+ if: contains(matrix.NAME, 'Firefox ESR (new)')
+
+ - name: Install Firefox ESR
+ run: |
+ # Support: Firefox <135 only
+ # Older Firefox used to be compressed using bzip2, newer using xz. Try
+ # to uncompress using xz, fall back to bzip2 if that fails.
+ # Note: this will download the old Firefox ESR twice, but it will still work
+ # when the current ESR version starts to use xz with no changes to the code.
+ wget --no-verbose "$FIREFOX_SOURCE_URL" -O - | tar -Jx -C "$HOME" || \
+ wget --no-verbose "$FIREFOX_SOURCE_URL" -O - | tar -jx -C "$HOME"
+ echo "PATH=${HOME}/firefox:$PATH" >> "$GITHUB_ENV"
+ echo "FIREFOX_BIN=${HOME}/firefox/firefox" >> "$GITHUB_ENV"
+ if: contains(matrix.NAME, 'Firefox ESR')
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Run tests
+ run: |
+ npm run pretest
+ npm run build:all
+ npm run test:unit -- -c jtr-local.yml \
+ --headless -b ${{ matrix.BROWSER }} -f plugin=${{ matrix.MIGRATE_VERSION }}
+
+ ie:
+ runs-on: windows-latest
+ name: Edge in IE mode (min Migrate)
+ env:
+ MIGRATE_VERSION: "min"
+ steps:
+ - name: Checkout
+ uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
+
+ - name: Use Node.js ${{ env.NODE_VERSION }}
+ uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+ cache-dependency-path: '**/package-lock.json'
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Run tests
+ shell: cmd
+ run: npm run test:ie -- -c jtr-local.yml -f plugin=${{ env.MIGRATE_VERSION }}
+
+ safari:
+ runs-on: macos-latest
+ name: Safari (min Migrate)
+ env:
+ MIGRATE_VERSION: "min"
+ steps:
+ - name: Checkout
+ uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
+
+ - name: Use Node.js ${{ env.NODE_VERSION }}
+ uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+ cache-dependency-path: '**/package-lock.json'
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Run tests
+ run: npm run test:safari -- -c jtr-local.yml -f plugin=${{ env.MIGRATE_VERSION }}
diff --git a/.github/workflows/browserstack.yml b/.github/workflows/browserstack.yml
new file mode 100644
index 00000000..611ecbd5
--- /dev/null
+++ b/.github/workflows/browserstack.yml
@@ -0,0 +1,69 @@
+name: Browserstack
+
+on:
+ push:
+ branches:
+ - main
+ # Once a week every Tuesday
+ schedule:
+ - cron: "42 1 * * 2"
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ environment: browserstack
+ env:
+ BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
+ BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
+ NODE_VERSION: 22.x
+ name: ${{ matrix.BROWSER }}
+ concurrency:
+ group: ${{ matrix.BROWSER }} - ${{ github.sha }}
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ BROWSER:
+ - 'IE_11'
+ - 'Safari_latest'
+ # JTR doesn't take into account the jump from Safari 18 to 26,
+ # so we need to specify versions explicitly.
+ # See https://github.com/jquery/jquery-test-runner/issues/17
+ - 'Safari_18'
+ - 'Chrome_latest'
+ - 'Chrome_latest-1'
+ - 'Opera_latest'
+ - 'Edge_latest'
+ - 'Edge_latest-1'
+ - 'Firefox_latest'
+ - 'Firefox_latest-1'
+ - '_:iPhone 17_iOS_26'
+ - '_:iPhone 16_iOS_18'
+ - '_:iPhone 15 Pro_iOS_17'
+ - '_:iPad Air 13 2025_iOS_26'
+ - '_:iPad Air 13 2025_iOS_18'
+ steps:
+ - name: Checkout
+ uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
+
+ - name: Use Node.js ${{ env.NODE_VERSION }}
+ uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+ cache-dependency-path: '**/package-lock.json'
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Build jQuery
+ run: npm run build:all
+
+ - name: Pretest script
+ run: npm run pretest
+
+ - name: Test
+ run: |
+ npm run test:unit -- -v -c jtr-ci.yml \
+ --browserstack "${{ matrix.BROWSER }}" \
+ --run-id ${{ github.run_id }} \
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 00000000..ade56c94
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,58 @@
+name: "Code scanning - action"
+
+on:
+ pull_request:
+ push:
+ branches-ignore: "dependabot/**"
+ schedule:
+ - cron: "0 4 * * 6"
+
+permissions:
+ contents: read # to fetch code (actions/checkout)
+
+jobs:
+ CodeQL-Build:
+ permissions:
+ contents: read # to fetch code (actions/checkout)
+ security-events: write # (github/codeql-action/autobuild)
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
+ with:
+ # We must fetch at least the immediate parents so that if this is
+ # a pull request then we can checkout the head.
+ fetch-depth: 2
+
+ # If this run was triggered by a pull request event, then checkout
+ # the head of the pull request instead of the merge commit.
+ - run: git checkout HEAD^2
+ if: ${{ github.event_name == 'pull_request' }}
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
+ # Override language selection by uncommenting this and choosing your languages
+ # with:
+ # languages: go, javascript, csharp, python, cpp, java
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
+
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+
+ # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+ # and modify them (or add more) to build your code if your project
+ # uses a compiled language
+
+ #- run: |
+ # make bootstrap
+ # make release
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
diff --git a/.github/workflows/filestash.yml b/.github/workflows/filestash.yml
new file mode 100644
index 00000000..845e5754
--- /dev/null
+++ b/.github/workflows/filestash.yml
@@ -0,0 +1,45 @@
+name: Filestash
+
+on:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: read # to fetch code (actions/checkout)
+
+jobs:
+ update:
+ runs-on: ubuntu-latest
+ environment: filestash
+ env:
+ NODE_VERSION: 22.x
+ name: Update Filestash
+ steps:
+ - name: Checkout
+ uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
+
+ - name: Use Node.js ${{ env.NODE_VERSION }}
+ uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+ cache-dependency-path: '**/package-lock.json'
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Build
+ run: npm run build
+
+ - name: Set up SSH
+ run: |
+ install --directory ~/.ssh --mode 700
+ base64 --decode <<< "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
+ chmod 600 ~/.ssh/id_ed25519
+ ssh-keyscan -t ed25519 -H "${{ secrets.FILESTASH_SERVER }}" >> ~/.ssh/known_hosts
+
+ - name: Upload to Filestash
+ run: |
+ rsync dist/jquery-migrate.js filestash@"${{ secrets.FILESTASH_SERVER }}":jquery-migrate-git.js
+ rsync dist/jquery-migrate.min.js filestash@"${{ secrets.FILESTASH_SERVER }}":jquery-migrate-git.min.js
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
new file mode 100644
index 00000000..6e9aca1a
--- /dev/null
+++ b/.github/workflows/node.js.yml
@@ -0,0 +1,37 @@
+name: Node
+
+on:
+ pull_request:
+ push:
+ branches-ignore: "dependabot/**"
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+ name: ${{ matrix.NPM_SCRIPT }} - ${{ matrix.NAME }} (${{ matrix.NODE_VERSION }})
+ strategy:
+ fail-fast: false
+ matrix:
+ NAME: ["Node"]
+ NODE_VERSION: [20.x, 22.x, 24.x]
+ NPM_SCRIPT: ["test:browserless"]
+ include:
+ - NAME: "Node"
+ NODE_VERSION: "22.x"
+ NPM_SCRIPT: "lint"
+ steps:
+ - name: Checkout
+ uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
+
+ - name: Use Node.js ${{ env.NODE_VERSION }}
+ uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
+ with:
+ node-version: ${{ matrix.NODE_VERSION }}
+ cache: npm
+ cache-dependency-path: '**/package-lock.json'
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Run tests
+ run: npm run ${{ matrix.NPM_SCRIPT }}
diff --git a/.gitignore b/.gitignore
index b411653e..70266163 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
-dist
+coverage
+CDN
.project
.settings
*~
@@ -6,4 +7,24 @@ dist
*.patch
/*.html
.DS_Store
-node_modules
\ No newline at end of file
+.sizecache.json
+.eslintcache
+
+# Ignore built files in `dist` folder
+# Leave the package.json and wrappers
+/dist/*
+!/dist/package.json
+!/dist/wrappers
+
+# Ignore built files in `dist-module` folder
+# Leave the package.json and wrappers
+/dist-module/*
+!/dist-module/package.json
+!/dist-module/wrappers
+
+/external
+/node_modules
+
+# Ignore BrowserStack files
+local.log
+browserstack.err
diff --git a/.husky/commit-msg b/.husky/commit-msg
new file mode 100755
index 00000000..6adb6cef
--- /dev/null
+++ b/.husky/commit-msg
@@ -0,0 +1 @@
+npx commitplease .git/COMMIT_EDITMSG
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 00000000..3867a0fe
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1 @@
+npm run lint
diff --git a/.jshintrc b/.jshintrc
deleted file mode 100644
index 13a9e01b..00000000
--- a/.jshintrc
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "boss": true,
- "curly": true,
- "eqeqeq": true,
- "eqnull": true,
- "expr": true,
- "immed": true,
- "noarg": true,
- "onevar": true,
- "quotmark": "double",
- "smarttabs": true,
- "trailing": true,
- "undef": true,
- "unused": true,
-
- "node": true
-}
\ No newline at end of file
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 00000000..3b93b434
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,2 @@
+Igor Kalashnikov
+Michał Gołębiowski-Owczarek
diff --git a/.npmignore b/.npmignore
index f7db6ce4..0539dcc4 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,3 +1,5 @@
+CDN
+coverage
.project
.settings
*~
@@ -5,4 +7,5 @@
*.patch
/*.html
.DS_Store
+external
node_modules
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 00000000..cffe8cde
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+save-exact=true
diff --git a/AUTHORS.txt b/AUTHORS.txt
deleted file mode 100644
index 478380d5..00000000
--- a/AUTHORS.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-Authors ordered by first contribution.
-
-Dave Methvin
-Richard Gibson
-Igor Kalashnikov
-Max Riveiro
-
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0c9aea78..cb18176c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,200 +1,177 @@
-# Contributing to jQuery
+# Contributing to jQuery Migrate
1. [Getting Involved](#getting-involved)
-2. [Discussion](#discussion)
+2. [Questions and Discussion](#questions-and-discussion)
3. [How To Report Bugs](#how-to-report-bugs)
-4. [Core Style Guide](#jquery-core-style-guide)
-5. [Tips For Bug Patching](#tips-for-jquery-bug-patching)
-
+4. [Tips for Bug Patching](#tips-for-bug-patching)
## Getting Involved
-There are a number of ways to get involved with the development of jQuery core. Even if you've never contributed code to an Open Source project before, we're always looking for help identifying bugs, writing and reducing test cases and documentation.
-
-This is the best way to contribute to jQuery core. Please read through the full guide detailing [How to Report Bugs](#How-to-Report-Bugs).
+[API design principles](https://github.com/jquery/jquery/wiki/API-design-guidelines)
-## Discussion
+We're always looking for help [identifying bugs](#how-to-report-bugs), writing and reducing test cases, and improving documentation. And although new features are rare, anything passing our [guidelines](https://github.com/jquery/jquery/wiki/Adding-new-features) will be considered.
-### Forum and IRC
+More information on how to contribute to this and other jQuery organization projects is at [contribute.jquery.org](https://contribute.jquery.org), including a short guide with tips, tricks, and ideas on [getting started with open source](https://contribute.jquery.org/open-source/). Please review our [commit & pull request guide](https://contribute.jquery.org/commits-and-pull-requests/) and [style guides](https://contribute.jquery.org/style-guide/) for instructions on how to maintain a fork and submit patches.
-The jQuery core development team frequently tracks posts on the [jQuery Development Forum](http://forum.jquery.com/developing-jquery-core). If you have longer posts or questions please feel free to post them there. If you think you've found a bug please [file it in the bug tracker](#How-to-Report-Bugs).
+When opening a pull request, you'll be asked to sign our Contributor License Agreement. Both the Corporate and Individual agreements can be [previewed on GitHub](https://github.com/openjs-foundation/easycla).
-Additionally most of the jQuery core development team can be found in the [#jquery-dev](http://webchat.freenode.net/?channels=jquery-dev) IRC channel on irc.freenode.net.
+If you're looking for some good issues to start with, [here are some issues labeled "help wanted" or "patch welcome"](https://github.com/jquery/jquery-migrate/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22%2C%22Patch+Welcome%22).
-### Weekly Status Meetings
+## Questions and Discussion
-Every week (unless otherwise noted) the jQuery core dev team has a meeting to discuss the progress of current work and to bring forward possible new blocker bugs for discussion.
+### Looking for help?
-The meeting is held in the [#jquery-meeting](http://webchat.freenode.net/?channels=jquery-meeting) IRC channel on irc.freenode.net at [Noon EST](http://www.timeanddate.com/worldclock/fixedtime.html?month=1&day=17&year=2011&hour=12&min=0&sec=0&p1=43) on Mondays.
+jQuery is so popular that many developers have knowledge of its capabilities and limitations. Most questions about using jQuery can be answered on popular forums such as [Stack Overflow](https://stackoverflow.com). Please start there when you have questions, even if you think you've found a bug.
-[Past Meeting Notes](https://docs.google.com/document/d/1MrLFvoxW7GMlH9KK-bwypn77cC98jUnz7sMW1rg_TP4/edit?hl=en)
+The jQuery Core team watches [jQuery GitHub Discussions](https://github.com/jquery/jquery/discussions). If you have longer posts or questions that can't be answered in places such as Stack Overflow, please feel free to post them there. If you think you've found a bug, please [file it in the bug tracker](#how-to-report-bugs). The Core team can be found in the [#jquery/dev](https://matrix.to/#/#jquery_dev:gitter.im) Matrix channel on gitter.im.
+### Weekly Status Meetings
-## How to Report Bugs
+The jQuery Core team has a weekly meeting to discuss the progress of current work. The meeting is held in the [#jquery/meeting](hhttps://matrix.to/#/#jquery_meeting:gitter.im) Matrix channel on gitter.im at [Noon EST](https://www.timeanddate.com/worldclock/fixedtime.html?month=10&day=7&year=2024&hour=12&min=0&sec=0&p1=43) on Mondays.
-### Make sure it is a jQuery bug
+[jQuery Core Meeting Notes](https://meetings.jquery.org/category/core/)
-Many bugs reported to our bug tracker are actually bugs in user code, not in jQuery code. Keep in mind that just because your code throws an error and the console points to a line number inside of jQuery, this does *not* mean the bug is a jQuery bug; more often than not, these errors result from providing incorrect arguments when calling a jQuery function.
-If you are new to jQuery, it is usually a much better idea to ask for help first in the [Using jQuery Forum](http://forum.jquery.com/using-jquery) or the [jQuery IRC channel](http://webchat.freenode.net/?channels=%23jquery). You will get much quicker support, and you will help avoid tying up the jQuery team with invalid bug reports. These same resources can also be useful if you want to confirm that your bug is indeed a bug in jQuery before filing any tickets.
+## How to Report Bugs
+### Make sure it is a jQuery Migrate bug
-### Disable any browser extensions
+Most bugs reported to our bug tracker are actually bugs in user code, not in jQuery Migrate code. Keep in mind that just because your code throws an error inside of jQuery Migrate, this does *not* mean the bug is a jQuery Migrate bug.
-Make sure you have reproduced the bug with all browser extensions and add-ons disabled, as these can sometimes cause things to break in interesting and unpredictable ways. Try using incognito, stealth or anonymous browsing modes.
+Ask for help first on a discussion forum like [Stack Overflow](https://stackoverflow.com/). You will get much quicker support, and you will help avoid tying up the jQuery team with invalid bug reports.
+### Disable browser extensions
-### Try the latest version of jQuery
+Make sure you have reproduced the bug with all browser extensions and add-ons disabled, as these can sometimes cause things to break in interesting and unpredictable ways. Try using incognito, stealth or anonymous browsing modes.
-Bugs in old versions of jQuery may have already been fixed. In order to avoid reporting known issues, make sure you are always testing against the [latest build](http://code.jquery.com/jquery.js).
+### Try the latest version of jQuery Migrate
-### Try an older version of jQuery
+Bugs in old versions of jQuery Migrate may have already been fixed. In order to avoid reporting known issues, make sure you are always testing against the [latest build](https://releases.jquery.com/git/jquery-migrate-git.js). We cannot fix bugs in older released files, if a bug has been fixed in a subsequent version of jQuery Migrate the site should upgrade.
-Sometimes, bugs are introduced in newer versions of jQuery that do not exist in previous versions. When possible, it can be useful to try testing with an older release.
+### Simplify the test case
-### Reduce, reduce, reduce!
+When experiencing a problem, [reduce your code](https://webkit.org/test-case-reduction/) to the bare minimum required to reproduce the issue. This makes it *much* easier to isolate and fix the offending code. Bugs reported without reduced test cases take on average 9001% longer to fix than bugs that are submitted with them, so you really should try to do this if at all possible.
-When you are experiencing a problem, the most useful thing you can possibly do is to [reduce your code](http://webkit.org/quality/reduction.html) to the bare minimum required to reproduce the issue. This makes it *much* easier to isolate and fix the offending code. Bugs that are reported without reduced test cases take on average 9001% longer to fix than bugs that are submitted with them, so you really should try to do this if at all possible.
+### Search for related or duplicate issues
-## jQuery Core Style Guide
+Go to the [jQuery Migrate issue tracker](https://github.com/jquery/jquery-migrate/issues) and make sure the problem hasn't already been reported. If not, create a new issue there and include your test case.
-See: [jQuery Core Style Guide](http://docs.jquery.com/JQuery_Core_Style_Guidelines)
## Tips For Bug Patching
+We *love* when people contribute back to the project by patching the bugs they find. Since jQuery is used by so many people, we are cautious about the patches we accept and want to be sure they don't have a negative impact on the millions of people using jQuery each day. For that reason it can take a while for any suggested patch to work its way through the review and release process. The reward for you is knowing that the problem you fixed will improve things for millions of sites and billions of visits per day.
-### Environment: localhost
-
-To test the plugin you will need:
+### Build a Local Copy of jQuery Migrate
-* Some kind of localhost server(any will do)
-* Node.js
-* NPM (comes with the latest version of Node.js)
-* Grunt (install with: `npm install grunt -g`)
+Create a fork of the jQuery Migrate repo on GitHub at https://github.com/jquery/jquery-migrate
-
-### Build a Local Copy of the plugin
-
-Create a fork of the plugin repo on github at http://github.com/jquery/jquery-migrate
-
-Change directory to your web root directory, whatever that might be:
-
-```bash
-$ cd /path/to/your/www/root/
-```
-
-Clone your plugin fork to work locally
+Clone your jQuery fork to work locally:
```bash
$ git clone git@github.com:username/jquery-migrate.git
```
-Change directory to the newly created dir jquery-migrate/
+Change directory to the newly created dir `jquery-migrate/`:
```bash
$ cd jquery-migrate
```
-Add the jquery-migrate master as a remote. I label mine "upstream"
+Add the jQuery Migrate `main` as a remote (e.g. `upstream`):
```bash
-$ git remote add upstream git://github.com/jquery/jquery-migrate.git
+$ git remote add upstream git@github.com:jquery/jquery-migrate.git
```
-Get in the habit of pulling in the "upstream" master to stay up to date as jQuery receives new commits
+Get in the habit of pulling in the "upstream" main to stay up to date as jQuery Migrate receives new commits:
```bash
-$ git pull upstream master
+$ git pull upstream main
```
-Run the Grunt tools:
+Install the necessary dependencies:
```bash
-$ grunt && grunt watch
+$ npm install
```
-Now open the jQuery test suite in a browser at http://localhost/test. If there is a port, be sure to include it.
-
-Success! You just built and tested jQuery!
+Build all jQuery Migrate files:
+```bash
+$ npm run build
+```
-### Fix a bug from a ticket filed at bugs.jquery.com:
-
-**NEVER write your patches to the master branch**
-
-**ALWAYS USE A "TOPIC" BRANCH!** Like so (#### = the ticket #)...
-
-Make sure you start with your up-to-date master:
+Start a test server:
```bash
-$ git checkout master
+$ npm run test:server
```
-Create and checkout a new branch that includes the ticket #
+Now open the jQuery test suite in a browser at http://localhost:3000/test/.
-```bash
-$ git checkout -b bug_####
+Success! You just built and tested jQuery Migrate!
-# ( Explanation: this useful command will:
-# "checkout" a "-b" (branch) by the name of "bug_####"
-# or create it if it doesn't exist )
-```
+### Test Suite Tips...
-Now you're on branch: bug_####
+During the process of writing your patch, you will run the test suite MANY times. You can speed up the process by narrowing the running test suite down to the module you are testing by either double-clicking the title of the test or appending it to the url. The following examples assume you're working on a local repo, hosted on your localhost server.
-Determine the module/file you'll be working in...
+Example:
-Open up the corresponding /test/unit/?????.js and add the initial failing unit tests. This may seem awkward at first, but in the long run it will make sense. To truly and efficiently patch a bug, you need to be working against that bug.
+http://localhost:3000/test/?module=css
-Next, open the module files and make your changes
+This will only run the "css" module tests. This will significantly speed up your development and debugging.
-Run http://localhost/test --> **ALL TESTS MUST PASS**
+**ALWAYS RUN THE FULL SUITE BEFORE COMMITTING AND PUSHING A PATCH!**
-Once you're satisfied with your patch...
+#### Change the test server port
-Stage the files to be tracked:
+The default port for the test server is 3000. You can change the port by setting the `PORT` environment variable.
```bash
-$ git add filename
-# (you can use "git status" to list the files you've changed)
+$ PORT=3001 npm run test:server
```
+#### Loading changes on the test page
-( I recommend NEVER, EVER using "git add . " )
-
-Once you've staged all of your changed files, go ahead and commit them
+Rather than rebuilding jQuery Migrate with `npm run build` every time you make a change, you can use the included watch task to rebuild distribution files whenever a file is saved.
```bash
-$ git commit -m "Brief description of fix. Fixes #0000"
+$ npm start
```
-For a multiple line commit message, leave off the `-m "description"`.
+Alternatively, you can **load tests as ECMAScript modules** to avoid the need for rebuilding altogether.
+
+Set the `jquery-migrate:` field to `esmodules` after loading the test page.
-You will then be led into vi (or the text editor that you have set up) to complete your commit message.
+#### Running the test suite from the command line
-Then, push your branch with the bug fix commits to your github fork
+You can also run the test suite from the command line.
+
+First, prepare the tests:
```bash
-$ git push origin -u bug_####
+$ npm run pretest
```
-Before you tackle your next bug patch, return to the master:
+Make sure jQuery Migrate is built (`npm run build`) and run the tests:
```bash
-$ git checkout master
+$ npm run test:unit
```
+This will run each module in its own browser instance and report the results in the terminal.
-### Test Suite Tips...
-
-By default the plugin runs against the current (jquery-git WIP) version of jQuery. You can select a different version by specifying it in the URL. Files are always retrieved from code.jquery.com.
+View the full help for the test suite for more info on running the tests from the command line:
-Example:
+```bash
+$ npm run test:unit -- --help
+```
-http://localhost/jquery-migrate/test/?jquery=1.7.2
+### Repo organization
-This will run the plugin with version 1.7.2 of jQuery, taken from http://code.jquery.com/jquery-1.7.2.js.
+The jQuery Migrate source is organized with ECMAScript modules and then compiled into one file at build time.
-**ALWAYS RUN THE FULL TEST SUITE BEFORE COMMITTING AND PUSHING A PATCH!**
+### Browser support
+Remember that jQuery Migrate supports multiple browsers and their versions; any contributed code must work in all of them. You can refer to the ["Version compatibility" section in Migrate README](https://github.com/jquery/jquery-migrate/blob/main/README.md#version-compatibility) for the current list of supported browsers.
diff --git a/Gruntfile.js b/Gruntfile.js
deleted file mode 100644
index c358ad98..00000000
--- a/Gruntfile.js
+++ /dev/null
@@ -1,211 +0,0 @@
-/*global module:false*/
-module.exports = function(grunt) {
-
- "use strict";
-
- // The concatenated file won't pass onevar but our modules can
- var readOptionalJSON = function( filepath ) {
- var data = {};
- try {
- data = grunt.file.readJSON( filepath );
- } catch(e) {}
- return data;
- },
- srcHintOptions = readOptionalJSON("src/.jshintrc");
- delete srcHintOptions.onevar;
-
- // Project configuration.
- grunt.initConfig({
- pkg: grunt.file.readJSON("package.json"),
- files: [
- "src/intro.js",
- "src/migrate.js",
- "src/attributes.js",
- "src/core.js",
- "src/css.js",
- "src/ajax.js",
- "src/data.js",
- "src/manipulation.js",
- "src/event.js",
- "src/traversing.js",
- "src/deferred.js",
- "src/outro.js"
- ],
- tests: {
- "jquery-compat": [
- "dev+compat-git",
- "min+compat-git",
- "dev+1.11.1",
- "dev+1.10.2",
- "dev+1.9.1",
- "dev+1.8.3",
- "dev+1.7.2",
- "dev+1.6.4"
- ],
- jquery: [
- "dev+git",
- "min+git",
- "dev+2.1.1",
- "dev+2.0.3"
- ]
- },
- banners: {
- tiny: "/*! <%= pkg.name %> <%= pkg.version %> - <%= pkg.homepage %> */"
- },
- concat: {
- options: {
- banner: "/*!\n * <%= pkg.title || pkg.name %> - v<%= pkg.version %> - " +
- "<%= grunt.template.today('yyyy-mm-dd') %>\n" +
- " * Copyright <%= pkg.author.name %>\n */\n"
- },
- dist: {
- src: "<%= files %>",
- dest: "dist/<%= pkg.name %>.js"
- }
- },
- qunit: {
- files: [ "test/**/*.html" ]
- },
- npmcopy: {
- all: {
- options: {
- destPrefix: "external"
- },
- files: {
- "qunit/qunit.js": "qunitjs/qunit/qunit.js",
- "qunit/qunit.css": "qunitjs/qunit/qunit.css",
- "qunit/LICENSE.txt": "qunitjs/LICENSE.txt" }
- }
- },
- jshint: {
- dist: {
- src: [ "dist/jquery-migrate.js" ],
- options: srcHintOptions
- },
- tests: {
- src: [ "test/*.js" ],
- options: {
- jshintrc: "test/.jshintrc"
- }
- },
- grunt: {
- src: [ "Gruntfile.js" ],
- options: {
- jshintrc: ".jshintrc"
- }
- }
- },
- uglify: {
- all: {
- files: {
- "dist/jquery-migrate.min.js": [ "src/migratemute.js", "dist/jquery-migrate.js" ]
- }
- },
- options: {
- banner: "/*! jQuery Migrate v<%= pkg.version %> | (c) <%= pkg.author.name %> | jquery.org/license */\n",
- beautify: {
- ascii_only: true
- }
- }
-
- }
- });
-
- // Load grunt tasks from NPM packages
- grunt.loadNpmTasks("grunt-git-authors");
- grunt.loadNpmTasks("grunt-contrib-concat");
- grunt.loadNpmTasks("grunt-contrib-watch");
- grunt.loadNpmTasks("grunt-contrib-jshint");
- grunt.loadNpmTasks("grunt-contrib-uglify");
- grunt.loadNpmTasks("grunt-contrib-qunit");
- grunt.loadNpmTasks("grunt-npmcopy");
-
- // Default task.
- grunt.registerTask( "default", [ "concat", "uglify", "jshint", "qunit" ] );
-
- // Skip unit tests, used by testswarm
- grunt.registerTask( "buildnounit", [ "concat", "uglify", "jshint" ] );
-
- // Testswarm
- grunt.registerTask( "testswarm", function( commit, configFile, destName ) {
- var jobName,
- testswarm = require( "testswarm" ),
- runs = {},
- done = this.async(),
- pull = /PR-(\d+)/.exec( commit ),
- config = grunt.file.readJSON( configFile ).jquerymigrate,
- tests = grunt.config( "tests" )[ destName ],
- browserSets = destName || config.browserSets;
-
- if ( browserSets[ 0 ] === "[" ) {
- // We got an array, parse it
- browserSets = JSON.parse( browserSets );
- }
-
- if ( pull ) {
- jobName = "Pull #" + pull[ 1 ] + "";
- } else {
- jobName = "Commit " + commit.substr( 0, 10 ) + "";
- }
-
- tests.forEach(function( test ) {
- var plugin_jquery = test.split("+");
- runs[test] = config.testUrl + commit + "/test/index.html?plugin=" +
- plugin_jquery[0] + "&jquery=" + plugin_jquery[1];
- });
-
- // TODO: create separate job for compat-git/git so we can do different browsersets
- testswarm.createClient( {
- url: config.swarmUrl
- } )
- .addReporter( testswarm.reporters.cli )
- .auth( {
- id: config.authUsername,
- token: config.authToken
- })
- .addjob(
- {
- name: jobName,
- runs: runs,
- runMax: config.runMax,
- browserSets: browserSets,
- timeout: 1000 * 60 * 30
- }, function( err, passed ) {
- if ( err ) {
- grunt.log.error( err );
- }
- done( passed );
- }
- );
- });
-
- // Update manifest for jQuery plugin registry
- grunt.registerTask( "manifest", function() {
- var pkg = grunt.config( "pkg" );
- grunt.file.write( "migrate.jquery.json", JSON.stringify({
- name: "migrate",
- title: pkg.title,
- description: pkg.description,
- keywords: pkg.keywords,
- version: pkg.version,
- author: {
- name: pkg.author.name,
- url: pkg.author.url.replace( "master", pkg.version )
- },
- maintainers: pkg.maintainers,
- licenses: pkg.licenses.map(function( license ) {
- license.url = license.url.replace( "master", pkg.version );
- return license;
- }),
- bugs: pkg.bugs,
- homepage: pkg.homepage,
- docs: pkg.homepage,
- download: "https://github.com/jquery/jquery-migrate/blob/" + pkg.version + "/README.md#download",
- dependencies: {
- jquery: ">=1.6.4"
- }
- }, null, "\t" ) );
- });
-};
diff --git a/LICENSE.txt b/LICENSE.txt
index 9dfa0714..c0f86cb1 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright jQuery Foundation and other contributors, https://jquery.org/
+Copyright OpenJS Foundation and other contributors, https://openjsf.org/
This software consists of voluntary contributions made by many
individuals. For exact contribution history, see the revision history
diff --git a/README.md b/README.md
index fddbbaa3..f560be4a 100644
--- a/README.md
+++ b/README.md
@@ -1,81 +1,112 @@
-# jquery-migrate: Migrate older jQuery code to jQuery 1.9+
+
-This plugin can be used to detect and restore APIs or features that have been deprecated in jQuery and removed as of version 1.9.
-See the [warnings page](https://github.com/jquery/jquery-migrate/blob/master/warnings.md) for more information regarding messages the plugin generates.
-For more information about the changes made in jQuery 1.9, see the [upgrade guide](http://jquery.com/upgrade-guide/1.9/) and [blog post](http://blog.jquery.com/2013/01/15/jquery-1-9-final-jquery-2-0-beta-migrate-final-released/).
+#### NOTE: To upgrade to jQuery 4.x, you first need version 3.x. If you're using an older version, first upgrade to jQuery 3.x using [jQuery Migrate 3.x](https://github.com/jquery/jquery-migrate/tree/3.x-stable#readme), to resolve any compatibility issues. For more information about the changes made in jQuery 4.0, see the [upgrade guide](https://jquery.com/upgrade-guide/4.0/) and [blog post](https://blog.jquery.com/2025/08/11/jquery-4-0-0-release-candidate-1/).
-In your web page, load this plugin *after* the script for jQuery:
+# jQuery Migrate
+Upgrading libraries such as jQuery can be a lot of work, when breaking changes have been introduced. jQuery Migrate makes this easier, by restoring the APIs that were removed, and additionally shows warnings in the browser console (development version of jQuery Migrate only) when removed and/or deprecated APIs are used.
+
+That way you can spot and fix what otherwise would have been errors, until you no longer need jQuery Migrate and can remove it.
+
+## Version compatibility
+
+The following table indicates which jQuery Migrate versions can be used with which jQuery versions:
+
+| jQuery version | jQuery Migrate version |
+|----------------|------------------------|
+| 1.x | 1.x |
+| 2.x | 1.x |
+| 3.x | 3.x |
+| 4.x | 4.x |
+
+Each jQuery Migrate version supports the same browsers that the jQuery version used with it. See the [jQuery Browser Support page](https://jquery.com/browser-support/) for more information.
+
+## Usage
+
+In your web page, load this plugin *after* the script tag for jQuery, for example:
```html
-
-
+
+
```
-The plugin can be included with versions of jQuery as old as 1.6.4 to identify potential upgrade issues via its JQMIGRATE console warnings.
-However, the plugin is only required for version 1.9.0 or higher to restore deprecated and removed functionality.
-
## Download
### Development vs. Production versions
-To make it easier for jQuery developers to find and remove deprecated functionality, the development version of the plugin displays warnings on the browser's console. In browsers that don't support the console interface such as IE7, no messages are generated unless you include a debugging library such as [Firebug Lite](https://getfirebug.com/firebuglite) before including the jQuery Migrate plugin. Developers can also inspect the `jQuery.migrateWarnings` array to see what error messages have been generated.
-
-All warnings generated by this plugin start with the string "JQMIGRATE". A list of the warnings you may see are in [warnings.md](https://github.com/jquery/jquery-migrate/blob/master/warnings.md).
+The production build is minified and does not generate console warnings. It will only generate a console log message upon loading, or if it detects an error such as an outdated version of jQuery that it does not support. Do not use this file for development or debugging, it will make your life miserable.
-### Development version
+| | Development | Production |
+|--|-------------|------------|
+| Debugging enabled |
✓
| |
+| Minified | |
✓
|
+| Latest release (*may be hotlinked if desired*) | [jquery-migrate-3.5.2.js](https://code.jquery.com/jquery-migrate-3.5.2.js) | [jquery-migrate-3.5.2.min.js](https://code.jquery.com/jquery-migrate-3.5.2.min.js) |
+| \* Latest work-in-progress build | [jquery-migrate-git.js](https://releases.jquery.com/git/jquery-migrate-git.js) | [jquery-migrate-git.min.js](https://releases.jquery.com/git/jquery-migrate-git.min.js) |
-This version provides console warning messages when deprecated and/or removed APIs are used. Use this version during development and debugging, and whenever you are reporting bugs to the jQuery team.
-**Latest released development version:** This file is hosted on jQuery's CDN, and can be hotlinked if desired.
-[http://code.jquery.com/jquery-migrate-1.2.1.js](http://code.jquery.com/jquery-migrate-1.2.1.js)
+\* **Work-in-progress build:** Although this file represents the most recent updates to the plugin, it may not have been thoroughly tested. We do not recommend using this file on production sites since it may be unstable; use the released production version instead.
-**Current work-in-progress build:** Although this file represents the most recent updates to the plugin, it may not have been thorougly tested.
-[http://code.jquery.com/jquery-migrate-git.js](http://code.jquery.com/jquery-migrate-git.js)
-### Production version
+## Debugging
-The minified production file is compressed and does not generate console warnings. Do not use this file for development or debugging, it will make your life miserable.
+The development version of the plugin displays warnings in the browser console. Developers can also inspect the `jQuery.migrateMessages` array to see what error messages have been generated.
-**Latest released production version:**
-[http://code.jquery.com/jquery-migrate-1.2.1.min.js](http://code.jquery.com/jquery-migrate-1.2.1.min.js)
- This file is hosted on jQuery's CDN, and can be hotlinked if desired.
+All warnings generated by this plugin start with the string "JQMIGRATE". A list of the warnings you may see are in [warnings.md](https://github.com/jquery/jquery-migrate/blob/main/warnings.md).
-**Current work-in-progress build:** Although this file represents the most recent updates to the plugin, it may not have been thorougly tested. We do not recommend using this file on production sites since it may be unstable; use the released production version above.
-[http://code.jquery.com/jquery-migrate-git.min.js](http://code.jquery.com/jquery-migrate-git.min.js)
## Migrate Plugin API
-This plugin adds three properties to the `jQuery` object that can be used to programmatically control and examine its behavior:
+This plugin adds some properties to the `jQuery` object that can be used to programmatically control and examine its behavior:
+
+`jQuery.migrateMessages`: This property is an array of string warning messages that have been generated by the code on the page, in the order they were generated. Messages appear in the array only once, even if the condition has occurred multiple times, unless `jQuery.migrateReset()` is called.
+
+`jQuery.migrateMute`: Set this property to `true` to prevent console warnings from being generated in the development version. The `jQuery.migrateMessages` array is still maintained when this property is set, which allows programmatic inspection without console output.
-`jQuery.migrateWarnings`: This property is an array of string warning messages that have been generated by the code on the page, in the order they were generated. Messages appear in the array only once, even if the condition has occurred multiple times, unless `jQuery.migrateReset()` is called.
+`jQuery.migrateTrace`: Set this property to `false` if you want warnings but do not want stack traces to appear on the console.
-`jQuery.migrateMute`: Set this property to `true` to prevent console warnings from being generated in the debugging version. The `jQuery.migrateWarnings` array is still maintained when this property is set, which allows programmatic inspection without console output.
+`jQuery.migrateReset()`: This method clears the `jQuery.migrateMessages` array and "forgets" the list of messages that have been seen already.
-`jQuery.migrateTrace`: Set this property to `false` if you want warnings but do not want traces to appear on the console.
+`jQuery.migrateVersion`: This string property indicates the version of Migrate in use.
-`jQuery.migrateReset()`: This method clears the `jQuery.migrateWarnings` array and "forgets" the list of messages that have been seen already.
+`jQuery.migrateDeduplicateMessages`: By default, Migrate only gives a specific warning once. If you set this property to `false` it will give a warning for every occurrence each time it happens. Note that this can generate a lot of output, for example when a warning occurs in a loop.
+
+`jQuery.migrateDisablePatches`: Disables patches by their codes. You can find a code for each patch in square brackets in [warnings.md](https://github.com/jquery/jquery-migrate/blob/main/warnings.md). A limited number of warnings doesn't have codes defined and cannot be disabled. These are mostly setup issues like using an incorrect version of jQuery or loading Migrate multiple times.
+
+`jQuery.migrateDisablePatches`: Disables patches by their codes.
+
+`jQuery.migrateIsPatchEnabled`: Returns `true` if a patch of a provided code is enabled and `false` otherwise.
+
+`jQuery.UNSAFE_restoreLegacyHtmlPrefilter`: A deprecated alias of `jQuery.migrateEnablePatches( "self-closed-tags" )`
## Reporting problems
-Bugs that only occur when the jQuery Migrate plugin is used should be reported in the [jQuery Migrate Issue Tracker](https://github.com/jquery/jquery-migrate/issues) and *must* be accompanied by an executable test case that demonstrates the bug. The easiest way to do this is via an online test tool such as [jsFiddle.net](http://jsFiddle.net/) or [jsbin.com](http://jsbin.com). You can use [this jsFiddle template](http://jsfiddle.net/4ZwWv/) or [this jsbin template](http://jsbin.com/emuwuy/4/show) as a starting point; they already contain links to the work-in-progress versions of both jQuery and the jQuery Migrate plugin. Add your code there and post a link to it with your bug report.
+Bugs that only occur when the jQuery Migrate plugin is used should be reported in the [jQuery Migrate Issue Tracker](https://github.com/jquery/jquery-migrate/issues) and should be accompanied by an executable test case that demonstrates the bug. The easiest way to do this is via an online test tool such as [jsFiddle.net](https://jsFiddle.net/) or [jsbin.com](https://jsbin.com). Use the development version when you are reporting bugs.
-Bugs in jQuery itself should be reported on the [jQuery Core bug tracker](http://bugs.jquery.com/) and again should be accompanied by a test case from [jsFiddle.net](http://jsFiddle.net/) or [jsbin.com](http://jsbin.com) so that we can reproduce the issue.
+Bugs in jQuery itself should be reported on the [jQuery Core bug tracker](https://bugs.jquery.com/) and again should be accompanied by a test case from [jsFiddle.net](https://jsFiddle.net/) or [jsbin.com](http://jsbin.com) so that we can reproduce the issue.
For other questions about the plugin that aren't bugs, ask on the [jQuery Forum](http://forum.jquery.com).
-How to run the tests:
+Build and run tests:
====================================================
-Clone this repo, install `grunt`:
+
+## Build with `npm` commands
```sh
-git clone git://github.com/jquery/jquery-migrate.git
-cd jquery-migrate
-npm install
-npm install -g grunt-cli
+$ git clone git://github.com/jquery/jquery-migrate.git
+$ cd jquery-migrate
+$ npm install
+$ npm run build
```
-Run `grunt` to `jshint`, `qunit` and `uglify` release.
+### Run tests
```sh
-grunt
+$ npm test
```
+
+### Or
+
+```sh
+$ npm run test:server
+```
+
+and open http://localhost:3000/test/ in your browser.
diff --git a/build/command.js b/build/command.js
new file mode 100755
index 00000000..96a9a620
--- /dev/null
+++ b/build/command.js
@@ -0,0 +1,45 @@
+import yargs from "yargs/yargs";
+import { build } from "./tasks/build.js";
+
+const argv = yargs( process.argv.slice( 2 ) )
+ .version( false )
+ .command( {
+ command: "[options]",
+ describe: "Build a jQuery Migrate bundle"
+ } )
+ .option( "filename", {
+ alias: "f",
+ type: "string",
+ description:
+ "Set the filename of the built file. Defaults to jquery.js."
+ } )
+ .option( "dir", {
+ alias: "d",
+ type: "string",
+ description:
+ "Set the dir to which to output the built file. Defaults to /dist."
+ } )
+ .option( "version", {
+ alias: "v",
+ type: "string",
+ description:
+ "Set the version to include in the built file. " +
+ "Defaults to the version in package.json plus the " +
+ "short commit SHA and any excluded modules."
+ } )
+ .option( "watch", {
+ alias: "w",
+ type: "boolean",
+ description:
+ "Watch the source files and rebuild when they change."
+ } )
+ .option( "esm", {
+ type: "boolean",
+ description:
+ "Build an ES module (ESM) bundle. " +
+ "By default, a UMD bundle is built."
+ } )
+ .help()
+ .argv;
+
+build( argv );
diff --git a/build/copygit.sh b/build/copygit.sh
deleted file mode 100644
index 37ca8bee..00000000
--- a/build/copygit.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#
-scp ../dist/jquery-migrate.js jqadmin@code.origin.jquery.com:/var/www/html/code.jquery.com/jquery-migrate-git.js
-curl http://code.origin.jquery.com/jquery-migrate-git.js?reload
-
-scp ../dist/jquery-migrate.min.js jqadmin@code.origin.jquery.com:/var/www/html/code.jquery.com/jquery-migrate-git.min.js
-curl http://code.origin.jquery.com/jquery-migrate-git.min.js?reload
-
diff --git a/build/release.js b/build/release.js
index 098d82b7..92242abe 100644
--- a/build/release.js
+++ b/build/release.js
@@ -1,60 +1,72 @@
#!/usr/bin/env node
/*
- * jQuery Migrate Plugin Release Management
+ * JQuery Migrate Plugin Release Management
*/
// Debugging variables
-var debug = false,
+var dryrun = false,
skipRemote = false;
-var fs = require("fs"),
- child = require("child_process"),
- path = require("path");
+import fs from "node:fs";
+import child from "node:child_process";
+import path from "node:path";
+import chalk from "chalk";
+import enquirer from "enquirer";
+import { build } from "./tasks/build.js";
var releaseVersion,
nextVersion,
- finalFiles,
isBeta,
pkg,
- scpURL = "jqadmin@code.origin.jquery.com:/var/www/html/code.jquery.com/",
- cdnURL = "http://code.origin.jquery.com/",
+ prompt = enquirer.prompt,
+
repoURL = "git@github.com:jquery/jquery-migrate.git",
- branch = "master",
+ branch = "main",
// Windows needs the .cmd version but will find the non-.cmd
- // On Windows, ensure the HOME environment variable is set
- gruntCmd = process.platform === "win32" ? "grunt.cmd" : "grunt",
+ // On Windows, also ensure the HOME environment variable is set
+ npmCmd = process.platform === "win32" ? "npm.cmd" : "npm",
readmeFile = "README.md",
packageFile = "package.json",
- devFile = "dist/jquery-migrate.js",
- minFile = "dist/jquery-migrate.min.js",
+ versionFile = path.join( "src", "version.js" ),
- releaseFiles = {
- "jquery-migrate-VER.js": devFile,
- "jquery-migrate-VER.min.js": minFile
- };
+ releaseDir = "CDN",
+ distDir = "dist";
steps(
initialize,
checkGitStatus,
- updateReadme,
+ buildRelease,
+ updateVersions,
tagReleaseVersion,
- gruntBuild,
+ buildRelease,
makeReleaseCopies,
+ publishToNPM,
setNextVersion,
- uploadToCDN,
pushToRemote,
+ remindAboutCDN,
+ remindAboutSites,
exit
);
function initialize( next ) {
- if ( process.argv[2] === "-d" ) {
+ // -d dryrun mode, no commands are executed at all
+ if ( process.argv[ 2 ] === "-d" ) {
+ process.argv.shift();
+ dryrun = true;
+ console.warn( "=== DRY RUN MODE ===" );
+ }
+
+ // -r skip remote mode, no remote commands are executed
+ // (git push, npm publish, cdn copy)
+ // Reset with `git reset --hard HEAD~2 && git tag -d (version) && npm run build`
+ if ( process.argv[ 2 ] === "-r" ) {
process.argv.shift();
- debug = true;
- console.warn("=== DEBUG MODE ===" );
+ skipRemote = true;
+ console.warn( "=== SKIPREMOTE MODE ===" );
}
// First arg should be the version number being released; this is a proper subset
@@ -62,18 +74,20 @@ function initialize( next ) {
// Examples: 1.0.1, 1.0.1-pre, 1.0.1-rc1, 1.0.1-rc1.1
var newver, oldver,
rsemver = /^(\d+)\.(\d+)\.(\d+)(?:-([\dA-Za-z\-]+(?:\.[\dA-Za-z\-]+)*))?$/,
- version = rsemver.exec( process.argv[2] || "" ) || [],
- major = version[1],
- minor = version[2],
- patch = version[3],
- xbeta = version[4];
-
+ version = rsemver.exec( process.argv[ 2 ] || "" ) || [],
+ major = version[ 1 ],
+ minor = version[ 2 ],
+ patch = version[ 3 ],
+ xbeta = version[ 4 ];
- releaseVersion = process.argv[2];
+ releaseVersion = process.argv[ 2 ];
isBeta = !!xbeta;
if ( !releaseVersion ) {
- die( "Usage: release [ -d ] releaseVersion" );
+ log( "Usage: release [ -d -r ] releaseVersion" );
+ log( " -d Dry-run; no commands are executed at all" );
+ log( " -r Skip-remote; nothing is pushed externally" );
+ die( "Invalid args" );
}
if ( !version.length ) {
die( "'" + releaseVersion + "' is not a valid semver!" );
@@ -81,14 +95,14 @@ function initialize( next ) {
if ( xbeta === "pre" ) {
die( "Cannot release a 'pre' version!" );
}
- if ( !(fs.existsSync || path.existsSync)( packageFile ) ) {
+ if ( !( fs.existsSync || path.existsSync )( packageFile ) ) {
die( "No " + packageFile + " in this directory" );
}
- pkg = JSON.parse( fs.readFileSync( packageFile ) );
+ pkg = JSON.parse( fs.readFileSync( packageFile, "utf8" ) );
- log( "Current version is " + pkg.version + "; generating release " + releaseVersion );
+ status( "Current version is " + pkg.version + "; generating release " + releaseVersion );
version = rsemver.exec( pkg.version );
- oldver = ( +version[1] ) * 10000 + ( +version[2] * 100 ) + ( +version[3] )
+ oldver = ( +version[ 1 ] ) * 10000 + ( +version[ 2 ] * 100 ) + ( +version[ 3 ] );
newver = ( +major ) * 10000 + ( +minor * 100 ) + ( +patch );
if ( newver < oldver ) {
die( "Next version is older than current version!" );
@@ -104,133 +118,202 @@ function initialize( next ) {
// (look for " BRANCH pushes to BRANCH (up to date)")
function checkGitStatus( next ) {
- child.execFile( "git", [ "status" ], function( error, stdout, stderr ) {
- var onBranch = ((stdout||"").match( /On branch (\S+)/ ) || [])[1];
+ child.execFile( "git", [ "status" ], function( error, stdout ) {
+ if ( error ) {
+ throw error;
+ }
+ var onBranch = ( ( stdout || "" ).match( /On branch (\S+)/ ) || [] )[ 1 ];
if ( onBranch !== branch ) {
die( "Branches don't match: Wanted " + branch + ", got " + onBranch );
}
if ( /Changes to be committed/i.test( stdout ) ) {
- die("Please commit changed files before attemping to push a release.");
+ die( "Please commit changed files before attemping to push a release." );
}
if ( /Changes not staged for commit/i.test( stdout ) ) {
- die("Please stash files before attempting to push a release.");
+ die( "Please stash files before attempting to push a release." );
}
next();
- });
+ } );
}
function tagReleaseVersion( next ) {
- updatePackageVersion( releaseVersion );
- git( [ "commit", "-a", "-m", "Tagging the " + releaseVersion + " release." ], function(){
- git( [ "tag", releaseVersion ], next);
- });
+ git( [ "commit", "-a", "--no-verify", "-m", "Tagging the " + releaseVersion + " release." ],
+ function() {
+ git( [ "tag", releaseVersion ], next );
+ }
+ );
}
-function updateReadme( next ) {
- var readme = fs.readFileSync( readmeFile, "utf8" );
-
- // Change version references from the old version to the new one;
- // Only release versions should be updated which simplifies the regex
- if ( isBeta ) {
- log( "Skipping " + readmeFile + " update (beta release)" );
- } else {
- log( "Updating " + readmeFile );
- readme = readme
- .replace( /jquery-migrate-\d+\.\d+\.\d+/g, "jquery-migrate-" + releaseVersion );
- if ( !debug ) {
- fs.writeFileSync( readmeFile, readme );
- }
- }
+function updateVersions( next ) {
+ updateSourceVersion( releaseVersion );
+ updateReadmeVersion( releaseVersion );
+ updatePackageVersion( releaseVersion );
next();
}
-function gruntBuild( next ) {
- exec( gruntCmd, [], function( error, stdout ) {
- if ( error ) {
- die( error + stderr );
- }
- log( stdout || "(no output)" );
- next();
- });
+async function buildRelease( next ) {
+ await build( { version: releaseVersion } );
+ next();
}
function makeReleaseCopies( next ) {
- finalFiles = {};
- Object.keys( releaseFiles ).forEach(function( key ) {
- var builtFile = releaseFiles[ key ],
- releaseFile = key.replace( /VER/g, releaseVersion );
-
- copy( builtFile, releaseFile );
- finalFiles[ releaseFile ] = builtFile;
- });
+ if ( !fs.existsSync( releaseDir ) ) {
+ fs.mkdirSync( releaseDir );
+ }
+ var passThrough = function( t ) {
+ return t;
+ };
+ var releaseFiles = {
+ "jquery-migrate-VER.js": passThrough,
+ "jquery-migrate-VER.min.js": fixMinRef,
+ "jquery-migrate-VER.min.map": fixMapRef
+ };
+ Object.keys( releaseFiles ).forEach( function( key ) {
+ var distFile = key.replace( /-VER/g, "" ),
+ distPath = path.join( distDir, distFile ),
+ releaseFile = key.replace( /VER/g, releaseVersion ),
+ releasePath = path.join( releaseDir, releaseFile );
+
+ // Remove Windows CRLF if it's there, on *nix this is a no-op
+ log( "Processing " + distPath + " => " + releasePath );
+ var distText = fs.readFileSync( distPath, "utf8" ).replace( /\r\n/g, "\n" );
+ var releaseText = releaseFiles[ key ]( distText, releaseFile );
+ if ( !dryrun ) {
+ fs.writeFileSync( releasePath, releaseText );
+ }
+ } );
next();
}
-function setNextVersion( next ) {
- updatePackageVersion( nextVersion, "master" );
- git( [ "commit", "-a", "-m", "Updating the source version to " + nextVersion ], next );
-}
+async function publishToNPM( next ) {
+ const { input: otp } = await prompt( {
+ type: "input",
+ name: "input",
+ message: "Enter one-time password if you have 2FA enabled and press Enter.\n" +
+ "Otherwise, just press Enter."
+ } );
-function uploadToCDN( next ) {
- var cmds = [];
+ // Don't update "latest" if this is a beta
+ if ( isBeta ) {
+ exec( npmCmd, [
+ "publish",
+ "--tag",
+ "beta",
+ ...( otp ? [ "--otp", otp ] : [] )
+ ], next, skipRemote );
+ } else {
+ exec( npmCmd, [
+ "publish",
+ ...( otp ? [ "--otp", otp ] : [] )
+ ], next, skipRemote );
+ }
+}
- Object.keys( finalFiles ).forEach(function( name ) {
- cmds.push(
- function( nxt ){
- exec( "scp", [ name, scpURL ], nxt, skipRemote );
- },
- function( nxt ){
- exec( "curl", [ cdnURL + name + "?reload" ], nxt, skipRemote );
- }
- );
- });
- cmds.push( next );
-
- steps.apply( this, cmds );
+function setNextVersion( next ) {
+ updateSourceVersion( nextVersion );
+ updatePackageVersion( nextVersion, "main" );
+ git( [ "commit", "-a", "--no-verify", "-m", "Updating the source version to " + nextVersion ],
+ next );
}
function pushToRemote( next ) {
git( [ "push", "--tags", repoURL, branch ], next, skipRemote );
}
+function remindAboutCDN( next ) {
+ console.log( chalk.red( "TODO: Update CDN with jquery-migrate." +
+ releaseVersion + " files (min and regular)" ) );
+ console.log( chalk.red( " clone codeorigin.jquery.com, git add files, commit, push" ) );
+ next();
+}
+
+function remindAboutSites( next ) {
+ console.log( chalk.red( "TODO: Update jquery.com download page to " + releaseVersion ) );
+ next();
+}
+
//==============================
function steps() {
var cur = 0,
steps = arguments;
- (function next(){
- process.nextTick(function(){
+ ( function next() {
+ process.nextTick( function() {
steps[ cur++ ]( next );
- });
- })();
+ } );
+ } )();
}
function updatePackageVersion( ver, blobVer ) {
- log( "Updating " + packageFile + " version to " + ver );
+ status( "Updating " + packageFile + " version to " + ver );
blobVer = blobVer || ver;
pkg.version = ver;
pkg.author.url = setBlobVersion( pkg.author.url, blobVer );
- pkg.licenses[0].url = setBlobVersion( pkg.licenses[0].url, blobVer );
writeJsonSync( packageFile, pkg );
}
+function updateSourceVersion( ver ) {
+ var stmt = "\njQuery.migrateVersion = \"" + ver + "\";\n";
+
+ status( "Updating " + stmt.replace( /\n/g, "" ) );
+ if ( !dryrun ) {
+ fs.writeFileSync( versionFile, stmt );
+ }
+}
+
+function updateReadmeVersion() {
+ var readme = fs.readFileSync( readmeFile, "utf8" );
+
+ // Change version references from the old version to the new one.
+ // The regex can update beta versions in case it was changed manually.
+ if ( isBeta ) {
+ status( "Skipping " + readmeFile + " update (beta release)" );
+ } else {
+ status( "Updating " + readmeFile );
+ readme = readme.replace(
+ /jquery-migrate-\d+\.\d+\.\d+(?:-\w+)?/g,
+ "jquery-migrate-" + releaseVersion
+ );
+ if ( !dryrun ) {
+ fs.writeFileSync( readmeFile, readme );
+ }
+ }
+}
+
function setBlobVersion( s, v ) {
- return s.replace( /\/blob\/(?:(\d+\.\d+[^\/]+)|master)/, "/blob/" + v );
+ return s.replace( /\/blob\/(?:(\d+\.\d+[^\/]+)|main)/, "/blob/" + v );
}
function writeJsonSync( fname, json ) {
- if ( debug ) {
+ if ( dryrun ) {
console.log( JSON.stringify( json, null, " " ) );
} else {
fs.writeFileSync( fname, JSON.stringify( json, null, "\t" ) + "\n" );
}
}
-function copy( oldFile, newFile ) {
- log( "Copying " + oldFile + " to " + newFile );
- if ( !debug ) {
- fs.writeFileSync( newFile, fs.readFileSync( oldFile, "utf8" ) );
+function fixMinRef( oldText ) {
+ var mapRef = new RegExp( "^//# sourceMappingURL=jquery-migrate.min.map\\n?", "m" );
+
+ // Remove the ref for now rather than try to fix it
+ var newText = oldText.replace( mapRef, "" );
+ if ( oldText === newText ) {
+ throw Error( "fixMinRef: Unable to find the sourceMappingURL" );
}
+ return newText;
+}
+
+function fixMapRef( oldText, newFile ) {
+ var mapJSON = JSON.parse( oldText );
+ var sources = mapJSON.sources;
+ if ( sources.join() !== "../src/migratemute.js,jquery-migrate.js" ) {
+ throw Error( "fixMapRef: Unexpected sources entry: " + sources );
+ }
+
+ // This file isn't published, not sure the best way to deal with that
+ sources[ 0 ] = "migratemute.js";
+ sources[ 1 ] = newFile.replace( /\.map$/, ".js" );
+ return JSON.stringify( mapJSON );
}
function git( args, fn, skip ) {
@@ -238,12 +321,12 @@ function git( args, fn, skip ) {
}
function exec( cmd, args, fn, skip ) {
- if ( debug || skip ) {
- log( "# " + cmd + " " + args.join(" ") );
+ if ( dryrun || skip ) {
+ log( chalk.black.bgBlue( "# " + cmd + " " + args.join( " " ) ) );
fn();
} else {
- log( cmd + " " + args.join(" ") );
- child.execFile( cmd, args, { env: process.env },
+ log( chalk.green( cmd + " " + args.join( " " ) ) );
+ child.execFile( cmd, args, { env: process.env },
function( err, stdout, stderr ) {
if ( err ) {
die( stderr || stdout || err );
@@ -254,12 +337,16 @@ function exec( cmd, args, fn, skip ) {
}
}
+function status( msg ) {
+ console.log( chalk.black.bgGreen( msg ) );
+}
+
function log( msg ) {
console.log( msg );
}
function die( msg ) {
- console.error( "ERROR: " + msg );
+ console.error( chalk.red( "ERROR: " + msg ) );
process.exit( 1 );
}
diff --git a/build/tasks/build.js b/build/tasks/build.js
new file mode 100644
index 00000000..bc168681
--- /dev/null
+++ b/build/tasks/build.js
@@ -0,0 +1,165 @@
+/**
+ * A build task that compiles jQuery Migrate JS modules into one bundle.
+ */
+
+import path from "node:path";
+import { mkdir, readFile, writeFile } from "node:fs/promises";
+import * as rollup from "rollup";
+import { minify } from "./minify.js";
+import { getTimestamp } from "./lib/getTimestamp.js";
+import { compareSize } from "./compare_size.js";
+import util from "node:util";
+import { exec as nodeExec } from "node:child_process";
+import { isCleanWorkingDir } from "./lib/isCleanWorkingDir.js";
+
+const exec = util.promisify( nodeExec );
+
+function read( filename ) {
+ return readFile( filename, "utf8" );
+}
+
+async function readJSON( filename ) {
+ return JSON.parse( await read( filename ) );
+}
+
+async function getOutputRollupOptions( {
+ esm = false
+} = {} ) {
+ const wrapperFilePath = path.join( "src", `wrapper${
+ esm ? "-esm" : ""
+ }.js` );
+
+ const wrapperSource = await read( wrapperFilePath );
+
+ // Catch `// @CODE` and subsequent comment lines event if they don't start
+ // in the first column.
+ const wrapper = wrapperSource.split(
+ /[\x20\t]*\/\/ @CODE\n(?:[\x20\t]*\/\/[^\n]+\n)*/
+ );
+
+ return {
+
+ // The ESM format is not actually used as we strip it during the
+ // build, inserting our own wrappers; it's just that it doesn't
+ // generate any extra wrappers so there's nothing for us to remove.
+ format: "esm",
+
+ intro: wrapper[ 0 ].replace( /\n*$/, "" ),
+ outro: wrapper[ 1 ].replace( /^\n*/, "" )
+ };
+}
+
+async function writeCompiled( { code, dir, filename, version } ) {
+ const compiledContents = code
+
+ // Embed Version
+ .replace( /@VERSION/g, version )
+
+ // Embed Date
+ // yyyy-mm-ddThh:mmZ
+ .replace( /@DATE/g, new Date().toISOString().replace( /:\d+\.\d+Z$/, "Z" ) );
+
+ await writeFile( path.join( dir, filename ), compiledContents );
+ console.log( `[${ getTimestamp() }] ${ filename } v${ version } created.` );
+}
+
+export async function build( {
+ dir = "dist",
+ filename = "jquery-migrate.js",
+ esm = false,
+ watch = false,
+ version
+} = {} ) {
+
+ const pkg = await readJSON( "package.json" );
+
+ // Add the short commit hash to the version string
+ // when the version is not for a release.
+ if ( !version ) {
+ const { stdout } = await exec( "git rev-parse --short HEAD" );
+ const isClean = await isCleanWorkingDir();
+
+ // "+SHA" is semantically correct
+ // Add ".dirty" as well if the working dir is not clean
+ version = `${ pkg.version }+${ stdout.trim() }${
+ isClean ? "" : ".dirty"
+ }`;
+ }
+
+ const inputRollupOptions = {};
+ const outputRollupOptions = await getOutputRollupOptions( { esm } );
+ const src = "src/migrate.js";
+
+ inputRollupOptions.input = path.resolve( src );
+
+ await mkdir( dir, { recursive: true } );
+
+ if ( watch ) {
+ const watcher = rollup.watch( {
+ ...inputRollupOptions,
+ output: [ outputRollupOptions ],
+ watch: {
+ include: "src/**",
+ skipWrite: true
+ }
+ } );
+
+ watcher.on( "event", async( event ) => {
+ switch ( event.code ) {
+ case "ERROR":
+ console.error( event.error );
+ break;
+ case "BUNDLE_END":
+ const {
+ output: [ { code } ]
+ } = await event.result.generate( outputRollupOptions );
+
+ await writeCompiled( {
+ code,
+ dir,
+ filename,
+ version
+ } );
+ break;
+ }
+ } );
+
+ return watcher;
+ } else {
+ const bundle = await rollup.rollup( inputRollupOptions );
+
+ const {
+ output: [ { code } ]
+ } = await bundle.generate( outputRollupOptions );
+
+ await writeCompiled( { code, dir, filename, version } );
+ await minify( { dir, filename, version } );
+ }
+}
+
+export async function buildDefaultFiles( {
+ version = process.env.VERSION,
+ watch
+} = {} ) {
+ await Promise.all( [
+ build( { version, watch } ),
+ build( {
+ dir: "dist-module",
+ filename: "jquery-migrate.module.js",
+ esm: true,
+ version,
+ watch
+ } )
+ ] );
+
+ if ( watch ) {
+ console.log( "Watching files..." );
+ } else {
+ return compareSize( {
+ files: [
+ "dist/jquery-migrate.min.js",
+ "dist-module/jquery-migrate.module.min.js"
+ ]
+ } );
+ }
+}
diff --git a/build/tasks/compare_size.js b/build/tasks/compare_size.js
new file mode 100644
index 00000000..3c2e8bf6
--- /dev/null
+++ b/build/tasks/compare_size.js
@@ -0,0 +1,193 @@
+import chalk from "chalk";
+import fs from "node:fs/promises";
+import { promisify } from "node:util";
+import zlib from "node:zlib";
+import { exec as nodeExec } from "node:child_process";
+import { isCleanWorkingDir } from "./lib/isCleanWorkingDir.js";
+
+const VERSION = 1;
+const lastRunBranch = " last run";
+
+const gzip = promisify( zlib.gzip );
+const exec = promisify( nodeExec );
+
+async function getBranchName() {
+ const { stdout } = await exec( "git rev-parse --abbrev-ref HEAD" );
+ return stdout.trim();
+}
+
+async function getCommitHash() {
+ const { stdout } = await exec( "git rev-parse HEAD" );
+ return stdout.trim();
+}
+
+function getBranchHeader( branch, commit ) {
+ let branchHeader = branch.trim();
+ if ( commit ) {
+ branchHeader = chalk.bold( branchHeader ) + chalk.gray( ` @${ commit }` );
+ } else {
+ branchHeader = chalk.italic( branchHeader );
+ }
+ return branchHeader;
+}
+
+async function getCache( loc ) {
+ let cache;
+ try {
+ const contents = await fs.readFile( loc, "utf8" );
+ cache = JSON.parse( contents );
+ } catch ( err ) {
+ return {};
+ }
+
+ const lastRun = cache[ lastRunBranch ];
+ if ( !lastRun || !lastRun.meta || lastRun.meta.version !== VERSION ) {
+ console.log( "Compare cache version mismatch. Rewriting..." );
+ return {};
+ }
+ return cache;
+}
+
+function cacheResults( results ) {
+ const files = Object.create( null );
+ results.forEach( function( result ) {
+ files[ result.filename ] = {
+ raw: result.raw,
+ gz: result.gz
+ };
+ } );
+ return files;
+}
+
+function saveCache( loc, cache ) {
+
+ // Keep cache readable for manual edits
+ return fs.writeFile( loc, JSON.stringify( cache, null, " " ) + "\n" );
+}
+
+function compareSizes( existing, current, padLength ) {
+ if ( typeof current !== "number" ) {
+ return chalk.grey( `${ existing }`.padStart( padLength ) );
+ }
+ const delta = current - existing;
+ if ( delta > 0 ) {
+ return chalk.red( `+${ delta }`.padStart( padLength ) );
+ }
+ return chalk.green( `${ delta }`.padStart( padLength ) );
+}
+
+function sortBranches( a, b ) {
+ if ( a === lastRunBranch ) {
+ return 1;
+ }
+ if ( b === lastRunBranch ) {
+ return -1;
+ }
+ if ( a < b ) {
+ return -1;
+ }
+ if ( a > b ) {
+ return 1;
+ }
+ return 0;
+}
+
+export async function compareSize( { cache = ".sizecache.json", files } = {} ) {
+ if ( !files || !files.length ) {
+ throw new Error( "No files specified" );
+ }
+
+ const branch = await getBranchName();
+ const commit = await getCommitHash();
+ const sizeCache = await getCache( cache );
+
+ let rawPadLength = 0;
+ let gzPadLength = 0;
+ const results = await Promise.all(
+ files.map( async function( filename ) {
+
+ let contents = await fs.readFile( filename, "utf8" );
+
+ // Remove the short SHA and .dirty from comparisons.
+ // The short SHA so commits can be compared against each other
+ // and .dirty to compare with the existing branch during development.
+ const sha = /jQuery Migrate v\d+.\d+.\d+(?:-\w+)?(?:\+slim\.|\+)?([^ \.]+(?:\.dirty)?)?/.exec( contents )[ 1 ];
+ contents = contents.replace( new RegExp( sha, "g" ), "" );
+
+ const size = Buffer.byteLength( contents, "utf8" );
+ const gzippedSize = ( await gzip( contents ) ).length;
+
+ // Add one to give space for the `+` or `-` in the comparison
+ rawPadLength = Math.max( rawPadLength, size.toString().length + 1 );
+ gzPadLength = Math.max( gzPadLength, gzippedSize.toString().length + 1 );
+
+ return { filename, raw: size, gz: gzippedSize };
+ } )
+ );
+
+ const sizeHeader = "raw".padStart( rawPadLength ) +
+ "gz".padStart( gzPadLength + 1 ) +
+ " Filename";
+
+ const sizes = results.map( function( result ) {
+ const rawSize = result.raw.toString().padStart( rawPadLength );
+ const gzSize = result.gz.toString().padStart( gzPadLength );
+ return `${ rawSize } ${ gzSize } ${ result.filename }`;
+ } );
+
+ const comparisons = Object.keys( sizeCache ).sort( sortBranches ).map( function( branch ) {
+ const meta = sizeCache[ branch ].meta || {};
+ const commit = meta.commit;
+
+ const files = sizeCache[ branch ].files;
+ const branchSizes = Object.keys( files ).map( function( filename ) {
+ const branchResult = files[ filename ];
+ const compareResult = results.find( function( result ) {
+ return result.filename === filename;
+ } ) || {};
+
+ const compareRaw = compareSizes( branchResult.raw, compareResult.raw, rawPadLength );
+ const compareGz = compareSizes( branchResult.gz, compareResult.gz, gzPadLength );
+ return `${ compareRaw } ${ compareGz } ${ filename }`;
+ } );
+
+ return [
+ "", // New line before each branch
+ getBranchHeader( branch, commit ),
+ sizeHeader,
+ ...branchSizes
+ ].join( "\n" );
+ } );
+
+ const output = [
+ "", // Opening new line
+ chalk.bold( "Sizes" ),
+ sizeHeader,
+ ...sizes,
+ ...comparisons,
+ "" // Closing new line
+ ].join( "\n" );
+
+ console.log( output );
+
+ // Always save the last run
+ // Save version under last run
+ sizeCache[ lastRunBranch ] = {
+ meta: { version: VERSION },
+ files: cacheResults( results )
+ };
+
+ // Only save cache for the current branch
+ // if the working directory is clean.
+ if ( await isCleanWorkingDir() ) {
+ sizeCache[ branch ] = {
+ meta: { commit },
+ files: cacheResults( results )
+ };
+ console.log( `Saved cache for ${ branch }.` );
+ }
+
+ await saveCache( cache, sizeCache );
+
+ return results;
+}
diff --git a/build/tasks/dist.js b/build/tasks/dist.js
new file mode 100644
index 00000000..e23f7811
--- /dev/null
+++ b/build/tasks/dist.js
@@ -0,0 +1,30 @@
+
+// Process files for distribution.
+export function processForDist( text, filename ) {
+ if ( !text ) {
+ throw new Error( "text required for processForDist" );
+ }
+
+ if ( !filename ) {
+ throw new Error( "filename required for processForDist" );
+ }
+
+ // Ensure files use only \n for line endings, not \r\n
+ if ( /\x0d\x0a/.test( text ) ) {
+ throw new Error( filename + ": Incorrect line endings (\\r\\n)" );
+ }
+
+ // Ensure only ASCII chars so script tags don't need a charset attribute
+ if ( text.length !== Buffer.byteLength( text, "utf8" ) ) {
+ let message = filename + ": Non-ASCII characters detected:\n";
+ for ( let i = 0; i < text.length; i++ ) {
+ const c = text.charCodeAt( i );
+ if ( c > 127 ) {
+ message += "- position " + i + ": " + c + "\n";
+ message += "==> " + text.substring( i - 20, i + 20 );
+ break;
+ }
+ }
+ throw new Error( message );
+ }
+}
diff --git a/build/tasks/lib/getTimestamp.js b/build/tasks/lib/getTimestamp.js
new file mode 100644
index 00000000..738a99bb
--- /dev/null
+++ b/build/tasks/lib/getTimestamp.js
@@ -0,0 +1,7 @@
+export function getTimestamp() {
+ const now = new Date();
+ const hours = now.getHours().toString().padStart( 2, "0" );
+ const minutes = now.getMinutes().toString().padStart( 2, "0" );
+ const seconds = now.getSeconds().toString().padStart( 2, "0" );
+ return `${ hours }:${ minutes }:${ seconds }`;
+}
diff --git a/build/tasks/lib/isCleanWorkingDir.js b/build/tasks/lib/isCleanWorkingDir.js
new file mode 100644
index 00000000..f2e1f5e2
--- /dev/null
+++ b/build/tasks/lib/isCleanWorkingDir.js
@@ -0,0 +1,8 @@
+import util from "node:util";
+import { exec as nodeExec } from "node:child_process";
+const exec = util.promisify( nodeExec );
+
+export async function isCleanWorkingDir() {
+ const { stdout } = await exec( "git status --untracked-files=no --porcelain" );
+ return !stdout.trim();
+}
diff --git a/build/tasks/minify.js b/build/tasks/minify.js
new file mode 100644
index 00000000..e59851b0
--- /dev/null
+++ b/build/tasks/minify.js
@@ -0,0 +1,72 @@
+import UglifyJS from "uglify-js";
+import path from "node:path";
+import { getTimestamp } from "./lib/getTimestamp.js";
+import { readFile, writeFile } from "node:fs/promises";
+import { processForDist } from "./dist.js";
+
+const rjs = /\.js$/;
+
+export async function minify( { dir, filename, version } ) {
+
+ // Prepend migratemute.js to the minified file
+ const muteFilename = "migratemute.js";
+ const muteContents = await readFile( path.join( "src", muteFilename ), "utf8" );
+
+ const contents = await readFile( path.join( dir, filename ), "utf8" );
+ const banner =
+ `/*! jQuery Migrate v${ version }` +
+ " | (c) OpenJS Foundation and other contributors" +
+ " | jquery.com/license */";
+
+ const minFilename = filename.replace( rjs, ".min.js" );
+ const mapFilename = filename.replace( rjs, ".min.map" );
+
+ const {
+ code,
+ error,
+ map,
+ warning
+ } = UglifyJS.minify(
+ {
+ "../src/migratemute.js": muteContents,
+ [ filename ]: contents
+ },
+ {
+ ie: true,
+ compress: {
+ hoist_funs: false,
+ loops: false
+ },
+ output: {
+ ascii_only: true,
+ preamble: banner
+ },
+ sourceMap: {
+ filename: minFilename,
+ url: mapFilename
+ }
+ }
+ );
+
+ if ( error ) {
+ throw new Error( error );
+ }
+
+ if ( warning ) {
+ console.warn( warning );
+ }
+
+ await Promise.all( [
+ writeFile( path.join( dir, minFilename ), code ),
+ writeFile( path.join( dir, mapFilename ), map )
+ ] );
+
+ processForDist( muteContents, muteFilename );
+ processForDist( contents, filename );
+ processForDist( code, minFilename );
+ processForDist( map, mapFilename );
+
+ console.log(
+ `[${ getTimestamp() }] ${ minFilename } ${ version } with ${ mapFilename } created.`
+ );
+}
diff --git a/build/tasks/npmcopy.js b/build/tasks/npmcopy.js
new file mode 100644
index 00000000..47c943ab
--- /dev/null
+++ b/build/tasks/npmcopy.js
@@ -0,0 +1,31 @@
+import { copyFile, mkdir } from "node:fs/promises";
+import path from "node:path";
+
+const projectDir = path.resolve( "." );
+
+const files = {
+ "npo/npo.js": "native-promise-only/lib/npo.src.js",
+
+ "qunit/qunit.js": "qunit/qunit/qunit.js",
+ "qunit/qunit.css": "qunit/qunit/qunit.css",
+ "qunit/LICENSE.txt": "qunit/LICENSE.txt",
+
+ "sinon/sinon.js": "sinon/pkg/sinon.js",
+ "sinon/LICENSE.txt": "sinon/LICENSE"
+};
+
+async function npmcopy() {
+ await mkdir( path.resolve( projectDir, "external" ), {
+ recursive: true
+ } );
+ for ( const [ dest, source ] of Object.entries( files ) ) {
+ const from = path.resolve( projectDir, "node_modules", source );
+ const to = path.resolve( projectDir, "external", dest );
+ const toDir = path.dirname( to );
+ await mkdir( toDir, { recursive: true } );
+ await copyFile( from, to );
+ console.log( `${ source } → ${ dest }` );
+ }
+}
+
+npmcopy();
diff --git a/dist-module/wrappers/jquery-migrate.node-module-wrapper.js b/dist-module/wrappers/jquery-migrate.node-module-wrapper.js
new file mode 100644
index 00000000..5012bb4f
--- /dev/null
+++ b/dist-module/wrappers/jquery-migrate.node-module-wrapper.js
@@ -0,0 +1,5 @@
+// Node.js is able to import from a CommonJS module in an ESM one.
+import jQuery from "../../dist/jquery-migrate.js";
+
+export { jQuery, jQuery as $ };
+export default jQuery;
diff --git a/dist/package.json b/dist/package.json
new file mode 100644
index 00000000..a0df0c86
--- /dev/null
+++ b/dist/package.json
@@ -0,0 +1,3 @@
+{
+ "type": "commonjs"
+}
diff --git a/dist/wrappers/jquery-migrate.bundler-require-wrapper.js b/dist/wrappers/jquery-migrate.bundler-require-wrapper.js
new file mode 100644
index 00000000..ad414483
--- /dev/null
+++ b/dist/wrappers/jquery-migrate.bundler-require-wrapper.js
@@ -0,0 +1,5 @@
+"use strict";
+
+// Bundlers are able to synchronously require an ESM module from a CommonJS one.
+const { jQuery } = require( "../../dist-module/jquery-migrate.module.js" );
+module.exports = jQuery;
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 00000000..31947c0d
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,255 @@
+import jqueryConfig from "eslint-config-jquery";
+import importPlugin from "eslint-plugin-import";
+import globals from "globals";
+
+export default [
+ {
+
+ // Only global ignores will bypass the parser
+ // and avoid JS parsing errors
+ // See https://github.com/eslint/eslint/discussions/17412
+ ignores: [ "external", "null.json", "simple.json" ]
+ },
+
+ {
+ files: [
+ "eslint.config.js",
+ ".release-it.cjs",
+ "build/**",
+ "test/node_smoke_tests/**",
+ "test/bundler_smoke_tests/**/*"
+ ],
+ languageOptions: {
+ ecmaVersion: "latest",
+ globals: {
+ ...globals.node
+ }
+ },
+ rules: jqueryConfig.rules
+ },
+
+ {
+ files: [ "src/**" ],
+ plugins: {
+ import: importPlugin
+ },
+ languageOptions: {
+ ecmaVersion: 2015,
+
+ // The browser env is not enabled on purpose so that code takes
+ // all browser-only globals from window instead of assuming
+ // they're available as globals. This makes it possible to use
+ // jQuery with tools like jsdom which provide a custom window
+ // implementation.
+ globals: {
+ jQuery: false,
+ window: false
+ }
+ },
+ rules: {
+ ...jqueryConfig.rules,
+ "import/extensions": [ "error", "always" ],
+ "import/no-cycle": "error",
+ indent: [
+ "error",
+ "tab",
+ {
+ outerIIFEBody: 0,
+
+ // This makes it so code within if statements checking
+ // for jQuery features is not indented.
+ ignoredNodes: [ "Program > IfStatement > *" ]
+ }
+ ],
+ "one-var": [ "error", { var: "always" } ],
+ strict: [ "error", "function" ]
+ }
+ },
+
+ {
+ files: [
+ "src/wrapper.js",
+ "src/wrapper-esm.js",
+ "src/wrapper-factory.js",
+ "src/wrapper-factory-esm.js"
+ ],
+ languageOptions: {
+ globals: {
+ jQuery: false
+ }
+ },
+ rules: {
+ "no-unused-vars": "off",
+ indent: [
+ "error",
+ "tab",
+ {
+
+ // This makes it so code within the wrapper is not indented.
+ ignoredNodes: [
+ "Program > FunctionDeclaration > *"
+ ]
+ }
+ ]
+ }
+ },
+
+ {
+ files: [ "src/wrapper.js" ],
+ languageOptions: {
+ sourceType: "script",
+ globals: {
+ define: false,
+ module: false
+ }
+ },
+ rules: {
+ indent: [
+ "error",
+ "tab",
+ {
+
+ // This makes it so code within the wrapper is not indented.
+ ignoredNodes: [
+ "Program > ExpressionStatement > CallExpression > :last-child > *"
+ ]
+ }
+ ]
+ }
+ },
+
+ {
+ files: [ "test/unit/**" ],
+ languageOptions: {
+ ecmaVersion: 5,
+ sourceType: "script",
+ globals: {
+ ...globals.browser,
+ Promise: false,
+ Symbol: false,
+ jQuery: false,
+ QUnit: false,
+ sinon: false,
+ url: false,
+ expectMessage: false,
+ expectNoMessage: false,
+ compareVersions: false,
+ jQueryVersionSince: false,
+ startIframeTest: false,
+ TestManager: false
+ }
+ },
+ rules: {
+ ...jqueryConfig.rules,
+ "no-unused-vars": [
+ "error",
+ { args: "after-used", argsIgnorePattern: "^_" }
+ ]
+ }
+ },
+
+ {
+ files: [ "test/data/**" ],
+ ignores: [ "test/data/jquery-*.js", "test/data/qunit-start.js" ],
+ languageOptions: {
+ ecmaVersion: 5,
+ sourceType: "script",
+ globals: {
+ ...globals.browser,
+ Promise: false,
+ Symbol: false,
+ global: false,
+ jQuery: false,
+ QUnit: false,
+ url: true,
+ compareVersions: true,
+ jQueryVersionSince: false,
+ expectMessage: true,
+ expectNoMessage: true,
+ startIframeTest: true,
+ TestManager: true
+ }
+ },
+ rules: {
+ ...jqueryConfig.rules,
+ strict: [ "error", "global" ]
+ }
+ },
+
+ {
+ files: [ "test/runner/**" ],
+ languageOptions: {
+ ecmaVersion: "latest",
+ globals: {
+ ...globals.node
+ },
+ sourceType: "module"
+ },
+ rules: {
+ ...jqueryConfig.rules
+ }
+ },
+
+ {
+ files: [ "test/runner/listeners.js" ],
+ languageOptions: {
+ ecmaVersion: 5,
+ globals: {
+ ...globals.browser,
+ QUnit: false,
+ Symbol: false
+ },
+ sourceType: "script"
+ },
+ rules: {
+ ...jqueryConfig.rules,
+ strict: [ "error", "function" ]
+ }
+ },
+
+ {
+ files: [ "dist/jquery-migrate.js" ],
+ languageOptions: {
+ globals: {
+ define: false,
+ jQuery: false,
+ module: false,
+ Proxy: false,
+ Reflect: false,
+ window: false
+ }
+ },
+ rules: {
+ ...jqueryConfig.rules,
+ strict: [ "error", "function" ],
+
+ // These are fine for the built version
+ "no-multiple-empty-lines": "off",
+ "one-var": "off"
+ }
+ },
+
+ {
+ files: [ "dist/**" ],
+ languageOptions: {
+ ecmaVersion: 5,
+ sourceType: "script"
+ }
+ },
+
+ {
+ files: [ "dist-module/**" ],
+ languageOptions: {
+ ecmaVersion: 2015,
+ sourceType: "module"
+ }
+ },
+
+ {
+ files: [ "dist/wrappers/*.js" ],
+ languageOptions: {
+ ecmaVersion: 2015,
+ sourceType: "commonjs"
+ }
+ }
+];
diff --git a/external/qunit/LICENSE.txt b/external/qunit/LICENSE.txt
deleted file mode 100644
index fb928a54..00000000
--- a/external/qunit/LICENSE.txt
+++ /dev/null
@@ -1,36 +0,0 @@
-Copyright jQuery Foundation and other contributors, https://jquery.org/
-
-This software consists of voluntary contributions made by many
-individuals. For exact contribution history, see the revision history
-available at https://github.com/jquery/qunit
-
-The following license applies to all parts of this software except as
-documented below:
-
-====
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-====
-
-All files located in the node_modules and external directories are
-externally maintained libraries used by this software which have their
-own licenses; we recommend you read them, as their terms may differ from
-the terms above.
diff --git a/external/qunit/qunit.css b/external/qunit/qunit.css
deleted file mode 100644
index 0eb0b017..00000000
--- a/external/qunit/qunit.css
+++ /dev/null
@@ -1,280 +0,0 @@
-/*!
- * QUnit 1.17.1
- * http://qunitjs.com/
- *
- * Copyright jQuery Foundation and other contributors
- * Released under the MIT license
- * http://jquery.org/license
- *
- * Date: 2015-01-20T19:39Z
- */
-
-/** Font Family and Sizes */
-
-#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
- font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
-}
-
-#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
-#qunit-tests { font-size: smaller; }
-
-
-/** Resets */
-
-#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
- margin: 0;
- padding: 0;
-}
-
-
-/** Header */
-
-#qunit-header {
- padding: 0.5em 0 0.5em 1em;
-
- color: #8699A4;
- background-color: #0D3349;
-
- font-size: 1.5em;
- line-height: 1em;
- font-weight: 400;
-
- border-radius: 5px 5px 0 0;
-}
-
-#qunit-header a {
- text-decoration: none;
- color: #C2CCD1;
-}
-
-#qunit-header a:hover,
-#qunit-header a:focus {
- color: #FFF;
-}
-
-#qunit-testrunner-toolbar label {
- display: inline-block;
- padding: 0 0.5em 0 0.1em;
-}
-
-#qunit-banner {
- height: 5px;
-}
-
-#qunit-testrunner-toolbar {
- padding: 0.5em 1em 0.5em 1em;
- color: #5E740B;
- background-color: #EEE;
- overflow: hidden;
-}
-
-#qunit-userAgent {
- padding: 0.5em 1em 0.5em 1em;
- background-color: #2B81AF;
- color: #FFF;
- text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
-}
-
-#qunit-modulefilter-container {
- float: right;
- padding: 0.2em;
-}
-
-.qunit-url-config {
- display: inline-block;
- padding: 0.1em;
-}
-
-.qunit-filter {
- display: block;
- float: right;
- margin-left: 1em;
-}
-
-/** Tests: Pass/Fail */
-
-#qunit-tests {
- list-style-position: inside;
-}
-
-#qunit-tests li {
- padding: 0.4em 1em 0.4em 1em;
- border-bottom: 1px solid #FFF;
- list-style-position: inside;
-}
-
-#qunit-tests > li {
- display: none;
-}
-
-#qunit-tests li.running,
-#qunit-tests li.pass,
-#qunit-tests li.fail,
-#qunit-tests li.skipped {
- display: list-item;
-}
-
-#qunit-tests.hidepass li.running,
-#qunit-tests.hidepass li.pass {
- display: none;
-}
-
-#qunit-tests li strong {
- cursor: pointer;
-}
-
-#qunit-tests li.skipped strong {
- cursor: default;
-}
-
-#qunit-tests li a {
- padding: 0.5em;
- color: #C2CCD1;
- text-decoration: none;
-}
-#qunit-tests li a:hover,
-#qunit-tests li a:focus {
- color: #000;
-}
-
-#qunit-tests li .runtime {
- float: right;
- font-size: smaller;
-}
-
-.qunit-assert-list {
- margin-top: 0.5em;
- padding: 0.5em;
-
- background-color: #FFF;
-
- border-radius: 5px;
-}
-
-.qunit-collapsed {
- display: none;
-}
-
-#qunit-tests table {
- border-collapse: collapse;
- margin-top: 0.2em;
-}
-
-#qunit-tests th {
- text-align: right;
- vertical-align: top;
- padding: 0 0.5em 0 0;
-}
-
-#qunit-tests td {
- vertical-align: top;
-}
-
-#qunit-tests pre {
- margin: 0;
- white-space: pre-wrap;
- word-wrap: break-word;
-}
-
-#qunit-tests del {
- background-color: #E0F2BE;
- color: #374E0C;
- text-decoration: none;
-}
-
-#qunit-tests ins {
- background-color: #FFCACA;
- color: #500;
- text-decoration: none;
-}
-
-/*** Test Counts */
-
-#qunit-tests b.counts { color: #000; }
-#qunit-tests b.passed { color: #5E740B; }
-#qunit-tests b.failed { color: #710909; }
-
-#qunit-tests li li {
- padding: 5px;
- background-color: #FFF;
- border-bottom: none;
- list-style-position: inside;
-}
-
-/*** Passing Styles */
-
-#qunit-tests li li.pass {
- color: #3C510C;
- background-color: #FFF;
- border-left: 10px solid #C6E746;
-}
-
-#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
-#qunit-tests .pass .test-name { color: #366097; }
-
-#qunit-tests .pass .test-actual,
-#qunit-tests .pass .test-expected { color: #999; }
-
-#qunit-banner.qunit-pass { background-color: #C6E746; }
-
-/*** Failing Styles */
-
-#qunit-tests li li.fail {
- color: #710909;
- background-color: #FFF;
- border-left: 10px solid #EE5757;
- white-space: pre;
-}
-
-#qunit-tests > li:last-child {
- border-radius: 0 0 5px 5px;
-}
-
-#qunit-tests .fail { color: #000; background-color: #EE5757; }
-#qunit-tests .fail .test-name,
-#qunit-tests .fail .module-name { color: #000; }
-
-#qunit-tests .fail .test-actual { color: #EE5757; }
-#qunit-tests .fail .test-expected { color: #008000; }
-
-#qunit-banner.qunit-fail { background-color: #EE5757; }
-
-/*** Skipped tests */
-
-#qunit-tests .skipped {
- background-color: #EBECE9;
-}
-
-#qunit-tests .qunit-skipped-label {
- background-color: #F4FF77;
- display: inline-block;
- font-style: normal;
- color: #366097;
- line-height: 1.8em;
- padding: 0 0.5em;
- margin: -0.4em 0.4em -0.4em 0;
-}
-
-/** Result */
-
-#qunit-testresult {
- padding: 0.5em 1em 0.5em 1em;
-
- color: #2B81AF;
- background-color: #D2E0E6;
-
- border-bottom: 1px solid #FFF;
-}
-#qunit-testresult .module-name {
- font-weight: 700;
-}
-
-/** Fixture */
-
-#qunit-fixture {
- position: absolute;
- top: -10000px;
- left: -10000px;
- width: 1000px;
- height: 1000px;
-}
diff --git a/external/qunit/qunit.js b/external/qunit/qunit.js
deleted file mode 100644
index 006ca474..00000000
--- a/external/qunit/qunit.js
+++ /dev/null
@@ -1,2875 +0,0 @@
-/*!
- * QUnit 1.17.1
- * http://qunitjs.com/
- *
- * Copyright jQuery Foundation and other contributors
- * Released under the MIT license
- * http://jquery.org/license
- *
- * Date: 2015-01-20T19:39Z
- */
-
-(function( window ) {
-
-var QUnit,
- config,
- onErrorFnPrev,
- loggingCallbacks = {},
- fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ),
- toString = Object.prototype.toString,
- hasOwn = Object.prototype.hasOwnProperty,
- // Keep a local reference to Date (GH-283)
- Date = window.Date,
- now = Date.now || function() {
- return new Date().getTime();
- },
- globalStartCalled = false,
- runStarted = false,
- setTimeout = window.setTimeout,
- clearTimeout = window.clearTimeout,
- defined = {
- document: window.document !== undefined,
- setTimeout: window.setTimeout !== undefined,
- sessionStorage: (function() {
- var x = "qunit-test-string";
- try {
- sessionStorage.setItem( x, x );
- sessionStorage.removeItem( x );
- return true;
- } catch ( e ) {
- return false;
- }
- }())
- },
- /**
- * Provides a normalized error string, correcting an issue
- * with IE 7 (and prior) where Error.prototype.toString is
- * not properly implemented
- *
- * Based on http://es5.github.com/#x15.11.4.4
- *
- * @param {String|Error} error
- * @return {String} error message
- */
- errorString = function( error ) {
- var name, message,
- errorString = error.toString();
- if ( errorString.substring( 0, 7 ) === "[object" ) {
- name = error.name ? error.name.toString() : "Error";
- message = error.message ? error.message.toString() : "";
- if ( name && message ) {
- return name + ": " + message;
- } else if ( name ) {
- return name;
- } else if ( message ) {
- return message;
- } else {
- return "Error";
- }
- } else {
- return errorString;
- }
- },
- /**
- * Makes a clone of an object using only Array or Object as base,
- * and copies over the own enumerable properties.
- *
- * @param {Object} obj
- * @return {Object} New object with only the own properties (recursively).
- */
- objectValues = function( obj ) {
- var key, val,
- vals = QUnit.is( "array", obj ) ? [] : {};
- for ( key in obj ) {
- if ( hasOwn.call( obj, key ) ) {
- val = obj[ key ];
- vals[ key ] = val === Object( val ) ? objectValues( val ) : val;
- }
- }
- return vals;
- };
-
-QUnit = {};
-
-/**
- * Config object: Maintain internal state
- * Later exposed as QUnit.config
- * `config` initialized at top of scope
- */
-config = {
- // The queue of tests to run
- queue: [],
-
- // block until document ready
- blocking: true,
-
- // by default, run previously failed tests first
- // very useful in combination with "Hide passed tests" checked
- reorder: true,
-
- // by default, modify document.title when suite is done
- altertitle: true,
-
- // by default, scroll to top of the page when suite is done
- scrolltop: true,
-
- // when enabled, all tests must call expect()
- requireExpects: false,
-
- // add checkboxes that are persisted in the query-string
- // when enabled, the id is set to `true` as a `QUnit.config` property
- urlConfig: [
- {
- id: "hidepassed",
- label: "Hide passed tests",
- tooltip: "Only show tests and assertions that fail. Stored as query-strings."
- },
- {
- id: "noglobals",
- label: "Check for Globals",
- tooltip: "Enabling this will test if any test introduces new properties on the " +
- "`window` object. Stored as query-strings."
- },
- {
- id: "notrycatch",
- label: "No try-catch",
- tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " +
- "exceptions in IE reasonable. Stored as query-strings."
- }
- ],
-
- // Set of all modules.
- modules: [],
-
- // The first unnamed module
- currentModule: {
- name: "",
- tests: []
- },
-
- callbacks: {}
-};
-
-// Push a loose unnamed module to the modules collection
-config.modules.push( config.currentModule );
-
-// Initialize more QUnit.config and QUnit.urlParams
-(function() {
- var i, current,
- location = window.location || { search: "", protocol: "file:" },
- params = location.search.slice( 1 ).split( "&" ),
- length = params.length,
- urlParams = {};
-
- if ( params[ 0 ] ) {
- for ( i = 0; i < length; i++ ) {
- current = params[ i ].split( "=" );
- current[ 0 ] = decodeURIComponent( current[ 0 ] );
-
- // allow just a key to turn on a flag, e.g., test.html?noglobals
- current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
- if ( urlParams[ current[ 0 ] ] ) {
- urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] );
- } else {
- urlParams[ current[ 0 ] ] = current[ 1 ];
- }
- }
- }
-
- if ( urlParams.filter === true ) {
- delete urlParams.filter;
- }
-
- QUnit.urlParams = urlParams;
-
- // String search anywhere in moduleName+testName
- config.filter = urlParams.filter;
-
- config.testId = [];
- if ( urlParams.testId ) {
-
- // Ensure that urlParams.testId is an array
- urlParams.testId = [].concat( urlParams.testId );
- for ( i = 0; i < urlParams.testId.length; i++ ) {
- config.testId.push( urlParams.testId[ i ] );
- }
- }
-
- // Figure out if we're running the tests from a server or not
- QUnit.isLocal = location.protocol === "file:";
-}());
-
-// Root QUnit object.
-// `QUnit` initialized at top of scope
-extend( QUnit, {
-
- // call on start of module test to prepend name to all tests
- module: function( name, testEnvironment ) {
- var currentModule = {
- name: name,
- testEnvironment: testEnvironment,
- tests: []
- };
-
- // DEPRECATED: handles setup/teardown functions,
- // beforeEach and afterEach should be used instead
- if ( testEnvironment && testEnvironment.setup ) {
- testEnvironment.beforeEach = testEnvironment.setup;
- delete testEnvironment.setup;
- }
- if ( testEnvironment && testEnvironment.teardown ) {
- testEnvironment.afterEach = testEnvironment.teardown;
- delete testEnvironment.teardown;
- }
-
- config.modules.push( currentModule );
- config.currentModule = currentModule;
- },
-
- // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0.
- asyncTest: function( testName, expected, callback ) {
- if ( arguments.length === 2 ) {
- callback = expected;
- expected = null;
- }
-
- QUnit.test( testName, expected, callback, true );
- },
-
- test: function( testName, expected, callback, async ) {
- var test;
-
- if ( arguments.length === 2 ) {
- callback = expected;
- expected = null;
- }
-
- test = new Test({
- testName: testName,
- expected: expected,
- async: async,
- callback: callback
- });
-
- test.queue();
- },
-
- skip: function( testName ) {
- var test = new Test({
- testName: testName,
- skip: true
- });
-
- test.queue();
- },
-
- // DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0.
- // In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior.
- start: function( count ) {
- var globalStartAlreadyCalled = globalStartCalled;
-
- if ( !config.current ) {
- globalStartCalled = true;
-
- if ( runStarted ) {
- throw new Error( "Called start() outside of a test context while already started" );
- } else if ( globalStartAlreadyCalled || count > 1 ) {
- throw new Error( "Called start() outside of a test context too many times" );
- } else if ( config.autostart ) {
- throw new Error( "Called start() outside of a test context when " +
- "QUnit.config.autostart was true" );
- } else if ( !config.pageLoaded ) {
-
- // The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it
- config.autostart = true;
- return;
- }
- } else {
-
- // If a test is running, adjust its semaphore
- config.current.semaphore -= count || 1;
-
- // Don't start until equal number of stop-calls
- if ( config.current.semaphore > 0 ) {
- return;
- }
-
- // throw an Error if start is called more often than stop
- if ( config.current.semaphore < 0 ) {
- config.current.semaphore = 0;
-
- QUnit.pushFailure(
- "Called start() while already started (test's semaphore was 0 already)",
- sourceFromStacktrace( 2 )
- );
- return;
- }
- }
-
- resumeProcessing();
- },
-
- // DEPRECATED: QUnit.stop() will be removed in QUnit 2.0.
- stop: function( count ) {
-
- // If there isn't a test running, don't allow QUnit.stop() to be called
- if ( !config.current ) {
- throw new Error( "Called stop() outside of a test context" );
- }
-
- // If a test is running, adjust its semaphore
- config.current.semaphore += count || 1;
-
- pauseProcessing();
- },
-
- config: config,
-
- // Safe object type checking
- is: function( type, obj ) {
- return QUnit.objectType( obj ) === type;
- },
-
- objectType: function( obj ) {
- if ( typeof obj === "undefined" ) {
- return "undefined";
- }
-
- // Consider: typeof null === object
- if ( obj === null ) {
- return "null";
- }
-
- var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ),
- type = match && match[ 1 ] || "";
-
- switch ( type ) {
- case "Number":
- if ( isNaN( obj ) ) {
- return "nan";
- }
- return "number";
- case "String":
- case "Boolean":
- case "Array":
- case "Date":
- case "RegExp":
- case "Function":
- return type.toLowerCase();
- }
- if ( typeof obj === "object" ) {
- return "object";
- }
- return undefined;
- },
-
- extend: extend,
-
- load: function() {
- config.pageLoaded = true;
-
- // Initialize the configuration options
- extend( config, {
- stats: { all: 0, bad: 0 },
- moduleStats: { all: 0, bad: 0 },
- started: 0,
- updateRate: 1000,
- autostart: true,
- filter: ""
- }, true );
-
- config.blocking = false;
-
- if ( config.autostart ) {
- resumeProcessing();
- }
- }
-});
-
-// Register logging callbacks
-(function() {
- var i, l, key,
- callbacks = [ "begin", "done", "log", "testStart", "testDone",
- "moduleStart", "moduleDone" ];
-
- function registerLoggingCallback( key ) {
- var loggingCallback = function( callback ) {
- if ( QUnit.objectType( callback ) !== "function" ) {
- throw new Error(
- "QUnit logging methods require a callback function as their first parameters."
- );
- }
-
- config.callbacks[ key ].push( callback );
- };
-
- // DEPRECATED: This will be removed on QUnit 2.0.0+
- // Stores the registered functions allowing restoring
- // at verifyLoggingCallbacks() if modified
- loggingCallbacks[ key ] = loggingCallback;
-
- return loggingCallback;
- }
-
- for ( i = 0, l = callbacks.length; i < l; i++ ) {
- key = callbacks[ i ];
-
- // Initialize key collection of logging callback
- if ( QUnit.objectType( config.callbacks[ key ] ) === "undefined" ) {
- config.callbacks[ key ] = [];
- }
-
- QUnit[ key ] = registerLoggingCallback( key );
- }
-})();
-
-// `onErrorFnPrev` initialized at top of scope
-// Preserve other handlers
-onErrorFnPrev = window.onerror;
-
-// Cover uncaught exceptions
-// Returning true will suppress the default browser handler,
-// returning false will let it run.
-window.onerror = function( error, filePath, linerNr ) {
- var ret = false;
- if ( onErrorFnPrev ) {
- ret = onErrorFnPrev( error, filePath, linerNr );
- }
-
- // Treat return value as window.onerror itself does,
- // Only do our handling if not suppressed.
- if ( ret !== true ) {
- if ( QUnit.config.current ) {
- if ( QUnit.config.current.ignoreGlobalErrors ) {
- return true;
- }
- QUnit.pushFailure( error, filePath + ":" + linerNr );
- } else {
- QUnit.test( "global failure", extend(function() {
- QUnit.pushFailure( error, filePath + ":" + linerNr );
- }, { validTest: true } ) );
- }
- return false;
- }
-
- return ret;
-};
-
-function done() {
- var runtime, passed;
-
- config.autorun = true;
-
- // Log the last module results
- if ( config.previousModule ) {
- runLoggingCallbacks( "moduleDone", {
- name: config.previousModule.name,
- tests: config.previousModule.tests,
- failed: config.moduleStats.bad,
- passed: config.moduleStats.all - config.moduleStats.bad,
- total: config.moduleStats.all,
- runtime: now() - config.moduleStats.started
- });
- }
- delete config.previousModule;
-
- runtime = now() - config.started;
- passed = config.stats.all - config.stats.bad;
-
- runLoggingCallbacks( "done", {
- failed: config.stats.bad,
- passed: passed,
- total: config.stats.all,
- runtime: runtime
- });
-}
-
-// Doesn't support IE6 to IE9
-// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
-function extractStacktrace( e, offset ) {
- offset = offset === undefined ? 4 : offset;
-
- var stack, include, i;
-
- if ( e.stacktrace ) {
-
- // Opera 12.x
- return e.stacktrace.split( "\n" )[ offset + 3 ];
- } else if ( e.stack ) {
-
- // Firefox, Chrome, Safari 6+, IE10+, PhantomJS and Node
- stack = e.stack.split( "\n" );
- if ( /^error$/i.test( stack[ 0 ] ) ) {
- stack.shift();
- }
- if ( fileName ) {
- include = [];
- for ( i = offset; i < stack.length; i++ ) {
- if ( stack[ i ].indexOf( fileName ) !== -1 ) {
- break;
- }
- include.push( stack[ i ] );
- }
- if ( include.length ) {
- return include.join( "\n" );
- }
- }
- return stack[ offset ];
- } else if ( e.sourceURL ) {
-
- // Safari < 6
- // exclude useless self-reference for generated Error objects
- if ( /qunit.js$/.test( e.sourceURL ) ) {
- return;
- }
-
- // for actual exceptions, this is useful
- return e.sourceURL + ":" + e.line;
- }
-}
-
-function sourceFromStacktrace( offset ) {
- var e = new Error();
- if ( !e.stack ) {
- try {
- throw e;
- } catch ( err ) {
- // This should already be true in most browsers
- e = err;
- }
- }
- return extractStacktrace( e, offset );
-}
-
-function synchronize( callback, last ) {
- if ( QUnit.objectType( callback ) === "array" ) {
- while ( callback.length ) {
- synchronize( callback.shift() );
- }
- return;
- }
- config.queue.push( callback );
-
- if ( config.autorun && !config.blocking ) {
- process( last );
- }
-}
-
-function process( last ) {
- function next() {
- process( last );
- }
- var start = now();
- config.depth = ( config.depth || 0 ) + 1;
-
- while ( config.queue.length && !config.blocking ) {
- if ( !defined.setTimeout || config.updateRate <= 0 ||
- ( ( now() - start ) < config.updateRate ) ) {
- if ( config.current ) {
-
- // Reset async tracking for each phase of the Test lifecycle
- config.current.usedAsync = false;
- }
- config.queue.shift()();
- } else {
- setTimeout( next, 13 );
- break;
- }
- }
- config.depth--;
- if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
- done();
- }
-}
-
-function begin() {
- var i, l,
- modulesLog = [];
-
- // If the test run hasn't officially begun yet
- if ( !config.started ) {
-
- // Record the time of the test run's beginning
- config.started = now();
-
- verifyLoggingCallbacks();
-
- // Delete the loose unnamed module if unused.
- if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) {
- config.modules.shift();
- }
-
- // Avoid unnecessary information by not logging modules' test environments
- for ( i = 0, l = config.modules.length; i < l; i++ ) {
- modulesLog.push({
- name: config.modules[ i ].name,
- tests: config.modules[ i ].tests
- });
- }
-
- // The test run is officially beginning now
- runLoggingCallbacks( "begin", {
- totalTests: Test.count,
- modules: modulesLog
- });
- }
-
- config.blocking = false;
- process( true );
-}
-
-function resumeProcessing() {
- runStarted = true;
-
- // A slight delay to allow this iteration of the event loop to finish (more assertions, etc.)
- if ( defined.setTimeout ) {
- setTimeout(function() {
- if ( config.current && config.current.semaphore > 0 ) {
- return;
- }
- if ( config.timeout ) {
- clearTimeout( config.timeout );
- }
-
- begin();
- }, 13 );
- } else {
- begin();
- }
-}
-
-function pauseProcessing() {
- config.blocking = true;
-
- if ( config.testTimeout && defined.setTimeout ) {
- clearTimeout( config.timeout );
- config.timeout = setTimeout(function() {
- if ( config.current ) {
- config.current.semaphore = 0;
- QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) );
- } else {
- throw new Error( "Test timed out" );
- }
- resumeProcessing();
- }, config.testTimeout );
- }
-}
-
-function saveGlobal() {
- config.pollution = [];
-
- if ( config.noglobals ) {
- for ( var key in window ) {
- if ( hasOwn.call( window, key ) ) {
- // in Opera sometimes DOM element ids show up here, ignore them
- if ( /^qunit-test-output/.test( key ) ) {
- continue;
- }
- config.pollution.push( key );
- }
- }
- }
-}
-
-function checkPollution() {
- var newGlobals,
- deletedGlobals,
- old = config.pollution;
-
- saveGlobal();
-
- newGlobals = diff( config.pollution, old );
- if ( newGlobals.length > 0 ) {
- QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) );
- }
-
- deletedGlobals = diff( old, config.pollution );
- if ( deletedGlobals.length > 0 ) {
- QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) );
- }
-}
-
-// returns a new Array with the elements that are in a but not in b
-function diff( a, b ) {
- var i, j,
- result = a.slice();
-
- for ( i = 0; i < result.length; i++ ) {
- for ( j = 0; j < b.length; j++ ) {
- if ( result[ i ] === b[ j ] ) {
- result.splice( i, 1 );
- i--;
- break;
- }
- }
- }
- return result;
-}
-
-function extend( a, b, undefOnly ) {
- for ( var prop in b ) {
- if ( hasOwn.call( b, prop ) ) {
-
- // Avoid "Member not found" error in IE8 caused by messing with window.constructor
- if ( !( prop === "constructor" && a === window ) ) {
- if ( b[ prop ] === undefined ) {
- delete a[ prop ];
- } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) {
- a[ prop ] = b[ prop ];
- }
- }
- }
- }
-
- return a;
-}
-
-function runLoggingCallbacks( key, args ) {
- var i, l, callbacks;
-
- callbacks = config.callbacks[ key ];
- for ( i = 0, l = callbacks.length; i < l; i++ ) {
- callbacks[ i ]( args );
- }
-}
-
-// DEPRECATED: This will be removed on 2.0.0+
-// This function verifies if the loggingCallbacks were modified by the user
-// If so, it will restore it, assign the given callback and print a console warning
-function verifyLoggingCallbacks() {
- var loggingCallback, userCallback;
-
- for ( loggingCallback in loggingCallbacks ) {
- if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) {
-
- userCallback = QUnit[ loggingCallback ];
-
- // Restore the callback function
- QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ];
-
- // Assign the deprecated given callback
- QUnit[ loggingCallback ]( userCallback );
-
- if ( window.console && window.console.warn ) {
- window.console.warn(
- "QUnit." + loggingCallback + " was replaced with a new value.\n" +
- "Please, check out the documentation on how to apply logging callbacks.\n" +
- "Reference: http://api.qunitjs.com/category/callbacks/"
- );
- }
- }
- }
-}
-
-// from jquery.js
-function inArray( elem, array ) {
- if ( array.indexOf ) {
- return array.indexOf( elem );
- }
-
- for ( var i = 0, length = array.length; i < length; i++ ) {
- if ( array[ i ] === elem ) {
- return i;
- }
- }
-
- return -1;
-}
-
-function Test( settings ) {
- var i, l;
-
- ++Test.count;
-
- extend( this, settings );
- this.assertions = [];
- this.semaphore = 0;
- this.usedAsync = false;
- this.module = config.currentModule;
- this.stack = sourceFromStacktrace( 3 );
-
- // Register unique strings
- for ( i = 0, l = this.module.tests; i < l.length; i++ ) {
- if ( this.module.tests[ i ].name === this.testName ) {
- this.testName += " ";
- }
- }
-
- this.testId = generateHash( this.module.name, this.testName );
-
- this.module.tests.push({
- name: this.testName,
- testId: this.testId
- });
-
- if ( settings.skip ) {
-
- // Skipped tests will fully ignore any sent callback
- this.callback = function() {};
- this.async = false;
- this.expected = 0;
- } else {
- this.assert = new Assert( this );
- }
-}
-
-Test.count = 0;
-
-Test.prototype = {
- before: function() {
- if (
-
- // Emit moduleStart when we're switching from one module to another
- this.module !== config.previousModule ||
-
- // They could be equal (both undefined) but if the previousModule property doesn't
- // yet exist it means this is the first test in a suite that isn't wrapped in a
- // module, in which case we'll just emit a moduleStart event for 'undefined'.
- // Without this, reporters can get testStart before moduleStart which is a problem.
- !hasOwn.call( config, "previousModule" )
- ) {
- if ( hasOwn.call( config, "previousModule" ) ) {
- runLoggingCallbacks( "moduleDone", {
- name: config.previousModule.name,
- tests: config.previousModule.tests,
- failed: config.moduleStats.bad,
- passed: config.moduleStats.all - config.moduleStats.bad,
- total: config.moduleStats.all,
- runtime: now() - config.moduleStats.started
- });
- }
- config.previousModule = this.module;
- config.moduleStats = { all: 0, bad: 0, started: now() };
- runLoggingCallbacks( "moduleStart", {
- name: this.module.name,
- tests: this.module.tests
- });
- }
-
- config.current = this;
-
- this.testEnvironment = extend( {}, this.module.testEnvironment );
- delete this.testEnvironment.beforeEach;
- delete this.testEnvironment.afterEach;
-
- this.started = now();
- runLoggingCallbacks( "testStart", {
- name: this.testName,
- module: this.module.name,
- testId: this.testId
- });
-
- if ( !config.pollution ) {
- saveGlobal();
- }
- },
-
- run: function() {
- var promise;
-
- config.current = this;
-
- if ( this.async ) {
- QUnit.stop();
- }
-
- this.callbackStarted = now();
-
- if ( config.notrycatch ) {
- promise = this.callback.call( this.testEnvironment, this.assert );
- this.resolvePromise( promise );
- return;
- }
-
- try {
- promise = this.callback.call( this.testEnvironment, this.assert );
- this.resolvePromise( promise );
- } catch ( e ) {
- this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " +
- this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
-
- // else next test will carry the responsibility
- saveGlobal();
-
- // Restart the tests if they're blocking
- if ( config.blocking ) {
- QUnit.start();
- }
- }
- },
-
- after: function() {
- checkPollution();
- },
-
- queueHook: function( hook, hookName ) {
- var promise,
- test = this;
- return function runHook() {
- config.current = test;
- if ( config.notrycatch ) {
- promise = hook.call( test.testEnvironment, test.assert );
- test.resolvePromise( promise, hookName );
- return;
- }
- try {
- promise = hook.call( test.testEnvironment, test.assert );
- test.resolvePromise( promise, hookName );
- } catch ( error ) {
- test.pushFailure( hookName + " failed on " + test.testName + ": " +
- ( error.message || error ), extractStacktrace( error, 0 ) );
- }
- };
- },
-
- // Currently only used for module level hooks, can be used to add global level ones
- hooks: function( handler ) {
- var hooks = [];
-
- // Hooks are ignored on skipped tests
- if ( this.skip ) {
- return hooks;
- }
-
- if ( this.module.testEnvironment &&
- QUnit.objectType( this.module.testEnvironment[ handler ] ) === "function" ) {
- hooks.push( this.queueHook( this.module.testEnvironment[ handler ], handler ) );
- }
-
- return hooks;
- },
-
- finish: function() {
- config.current = this;
- if ( config.requireExpects && this.expected === null ) {
- this.pushFailure( "Expected number of assertions to be defined, but expect() was " +
- "not called.", this.stack );
- } else if ( this.expected !== null && this.expected !== this.assertions.length ) {
- this.pushFailure( "Expected " + this.expected + " assertions, but " +
- this.assertions.length + " were run", this.stack );
- } else if ( this.expected === null && !this.assertions.length ) {
- this.pushFailure( "Expected at least one assertion, but none were run - call " +
- "expect(0) to accept zero assertions.", this.stack );
- }
-
- var i,
- bad = 0;
-
- this.runtime = now() - this.started;
- config.stats.all += this.assertions.length;
- config.moduleStats.all += this.assertions.length;
-
- for ( i = 0; i < this.assertions.length; i++ ) {
- if ( !this.assertions[ i ].result ) {
- bad++;
- config.stats.bad++;
- config.moduleStats.bad++;
- }
- }
-
- runLoggingCallbacks( "testDone", {
- name: this.testName,
- module: this.module.name,
- skipped: !!this.skip,
- failed: bad,
- passed: this.assertions.length - bad,
- total: this.assertions.length,
- runtime: this.runtime,
-
- // HTML Reporter use
- assertions: this.assertions,
- testId: this.testId,
-
- // DEPRECATED: this property will be removed in 2.0.0, use runtime instead
- duration: this.runtime
- });
-
- // QUnit.reset() is deprecated and will be replaced for a new
- // fixture reset function on QUnit 2.0/2.1.
- // It's still called here for backwards compatibility handling
- QUnit.reset();
-
- config.current = undefined;
- },
-
- queue: function() {
- var bad,
- test = this;
-
- if ( !this.valid() ) {
- return;
- }
-
- function run() {
-
- // each of these can by async
- synchronize([
- function() {
- test.before();
- },
-
- test.hooks( "beforeEach" ),
-
- function() {
- test.run();
- },
-
- test.hooks( "afterEach" ).reverse(),
-
- function() {
- test.after();
- },
- function() {
- test.finish();
- }
- ]);
- }
-
- // `bad` initialized at top of scope
- // defer when previous test run passed, if storage is available
- bad = QUnit.config.reorder && defined.sessionStorage &&
- +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName );
-
- if ( bad ) {
- run();
- } else {
- synchronize( run, true );
- }
- },
-
- push: function( result, actual, expected, message ) {
- var source,
- details = {
- module: this.module.name,
- name: this.testName,
- result: result,
- message: message,
- actual: actual,
- expected: expected,
- testId: this.testId,
- runtime: now() - this.started
- };
-
- if ( !result ) {
- source = sourceFromStacktrace();
-
- if ( source ) {
- details.source = source;
- }
- }
-
- runLoggingCallbacks( "log", details );
-
- this.assertions.push({
- result: !!result,
- message: message
- });
- },
-
- pushFailure: function( message, source, actual ) {
- if ( !this instanceof Test ) {
- throw new Error( "pushFailure() assertion outside test context, was " +
- sourceFromStacktrace( 2 ) );
- }
-
- var details = {
- module: this.module.name,
- name: this.testName,
- result: false,
- message: message || "error",
- actual: actual || null,
- testId: this.testId,
- runtime: now() - this.started
- };
-
- if ( source ) {
- details.source = source;
- }
-
- runLoggingCallbacks( "log", details );
-
- this.assertions.push({
- result: false,
- message: message
- });
- },
-
- resolvePromise: function( promise, phase ) {
- var then, message,
- test = this;
- if ( promise != null ) {
- then = promise.then;
- if ( QUnit.objectType( then ) === "function" ) {
- QUnit.stop();
- then.call(
- promise,
- QUnit.start,
- function( error ) {
- message = "Promise rejected " +
- ( !phase ? "during" : phase.replace( /Each$/, "" ) ) +
- " " + test.testName + ": " + ( error.message || error );
- test.pushFailure( message, extractStacktrace( error, 0 ) );
-
- // else next test will carry the responsibility
- saveGlobal();
-
- // Unblock
- QUnit.start();
- }
- );
- }
- }
- },
-
- valid: function() {
- var include,
- filter = config.filter,
- module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(),
- fullName = ( this.module.name + ": " + this.testName ).toLowerCase();
-
- // Internally-generated tests are always valid
- if ( this.callback && this.callback.validTest ) {
- return true;
- }
-
- if ( config.testId.length > 0 && inArray( this.testId, config.testId ) < 0 ) {
- return false;
- }
-
- if ( module && ( !this.module.name || this.module.name.toLowerCase() !== module ) ) {
- return false;
- }
-
- if ( !filter ) {
- return true;
- }
-
- include = filter.charAt( 0 ) !== "!";
- if ( !include ) {
- filter = filter.toLowerCase().slice( 1 );
- }
-
- // If the filter matches, we need to honour include
- if ( fullName.indexOf( filter ) !== -1 ) {
- return include;
- }
-
- // Otherwise, do the opposite
- return !include;
- }
-
-};
-
-// Resets the test setup. Useful for tests that modify the DOM.
-/*
-DEPRECATED: Use multiple tests instead of resetting inside a test.
-Use testStart or testDone for custom cleanup.
-This method will throw an error in 2.0, and will be removed in 2.1
-*/
-QUnit.reset = function() {
-
- // Return on non-browser environments
- // This is necessary to not break on node tests
- if ( typeof window === "undefined" ) {
- return;
- }
-
- var fixture = defined.document && document.getElementById &&
- document.getElementById( "qunit-fixture" );
-
- if ( fixture ) {
- fixture.innerHTML = config.fixture;
- }
-};
-
-QUnit.pushFailure = function() {
- if ( !QUnit.config.current ) {
- throw new Error( "pushFailure() assertion outside test context, in " +
- sourceFromStacktrace( 2 ) );
- }
-
- // Gets current test obj
- var currentTest = QUnit.config.current;
-
- return currentTest.pushFailure.apply( currentTest, arguments );
-};
-
-// Based on Java's String.hashCode, a simple but not
-// rigorously collision resistant hashing function
-function generateHash( module, testName ) {
- var hex,
- i = 0,
- hash = 0,
- str = module + "\x1C" + testName,
- len = str.length;
-
- for ( ; i < len; i++ ) {
- hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i );
- hash |= 0;
- }
-
- // Convert the possibly negative integer hash code into an 8 character hex string, which isn't
- // strictly necessary but increases user understanding that the id is a SHA-like hash
- hex = ( 0x100000000 + hash ).toString( 16 );
- if ( hex.length < 8 ) {
- hex = "0000000" + hex;
- }
-
- return hex.slice( -8 );
-}
-
-function Assert( testContext ) {
- this.test = testContext;
-}
-
-// Assert helpers
-QUnit.assert = Assert.prototype = {
-
- // Specify the number of expected assertions to guarantee that failed test
- // (no assertions are run at all) don't slip through.
- expect: function( asserts ) {
- if ( arguments.length === 1 ) {
- this.test.expected = asserts;
- } else {
- return this.test.expected;
- }
- },
-
- // Increment this Test's semaphore counter, then return a single-use function that
- // decrements that counter a maximum of once.
- async: function() {
- var test = this.test,
- popped = false;
-
- test.semaphore += 1;
- test.usedAsync = true;
- pauseProcessing();
-
- return function done() {
- if ( !popped ) {
- test.semaphore -= 1;
- popped = true;
- resumeProcessing();
- } else {
- test.pushFailure( "Called the callback returned from `assert.async` more than once",
- sourceFromStacktrace( 2 ) );
- }
- };
- },
-
- // Exports test.push() to the user API
- push: function( /* result, actual, expected, message */ ) {
- var assert = this,
- currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current;
-
- // Backwards compatibility fix.
- // Allows the direct use of global exported assertions and QUnit.assert.*
- // Although, it's use is not recommended as it can leak assertions
- // to other tests from async tests, because we only get a reference to the current test,
- // not exactly the test where assertion were intended to be called.
- if ( !currentTest ) {
- throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) );
- }
-
- if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) {
- currentTest.pushFailure( "Assertion after the final `assert.async` was resolved",
- sourceFromStacktrace( 2 ) );
-
- // Allow this assertion to continue running anyway...
- }
-
- if ( !( assert instanceof Assert ) ) {
- assert = currentTest.assert;
- }
- return assert.test.push.apply( assert.test, arguments );
- },
-
- /**
- * Asserts rough true-ish result.
- * @name ok
- * @function
- * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
- */
- ok: function( result, message ) {
- message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " +
- QUnit.dump.parse( result ) );
- this.push( !!result, result, true, message );
- },
-
- /**
- * Assert that the first two arguments are equal, with an optional message.
- * Prints out both actual and expected values.
- * @name equal
- * @function
- * @example equal( format( "{0} bytes.", 2), "2 bytes.", "replaces {0} with next argument" );
- */
- equal: function( actual, expected, message ) {
- /*jshint eqeqeq:false */
- this.push( expected == actual, actual, expected, message );
- },
-
- /**
- * @name notEqual
- * @function
- */
- notEqual: function( actual, expected, message ) {
- /*jshint eqeqeq:false */
- this.push( expected != actual, actual, expected, message );
- },
-
- /**
- * @name propEqual
- * @function
- */
- propEqual: function( actual, expected, message ) {
- actual = objectValues( actual );
- expected = objectValues( expected );
- this.push( QUnit.equiv( actual, expected ), actual, expected, message );
- },
-
- /**
- * @name notPropEqual
- * @function
- */
- notPropEqual: function( actual, expected, message ) {
- actual = objectValues( actual );
- expected = objectValues( expected );
- this.push( !QUnit.equiv( actual, expected ), actual, expected, message );
- },
-
- /**
- * @name deepEqual
- * @function
- */
- deepEqual: function( actual, expected, message ) {
- this.push( QUnit.equiv( actual, expected ), actual, expected, message );
- },
-
- /**
- * @name notDeepEqual
- * @function
- */
- notDeepEqual: function( actual, expected, message ) {
- this.push( !QUnit.equiv( actual, expected ), actual, expected, message );
- },
-
- /**
- * @name strictEqual
- * @function
- */
- strictEqual: function( actual, expected, message ) {
- this.push( expected === actual, actual, expected, message );
- },
-
- /**
- * @name notStrictEqual
- * @function
- */
- notStrictEqual: function( actual, expected, message ) {
- this.push( expected !== actual, actual, expected, message );
- },
-
- "throws": function( block, expected, message ) {
- var actual, expectedType,
- expectedOutput = expected,
- ok = false;
-
- // 'expected' is optional unless doing string comparison
- if ( message == null && typeof expected === "string" ) {
- message = expected;
- expected = null;
- }
-
- this.test.ignoreGlobalErrors = true;
- try {
- block.call( this.test.testEnvironment );
- } catch (e) {
- actual = e;
- }
- this.test.ignoreGlobalErrors = false;
-
- if ( actual ) {
- expectedType = QUnit.objectType( expected );
-
- // we don't want to validate thrown error
- if ( !expected ) {
- ok = true;
- expectedOutput = null;
-
- // expected is a regexp
- } else if ( expectedType === "regexp" ) {
- ok = expected.test( errorString( actual ) );
-
- // expected is a string
- } else if ( expectedType === "string" ) {
- ok = expected === errorString( actual );
-
- // expected is a constructor, maybe an Error constructor
- } else if ( expectedType === "function" && actual instanceof expected ) {
- ok = true;
-
- // expected is an Error object
- } else if ( expectedType === "object" ) {
- ok = actual instanceof expected.constructor &&
- actual.name === expected.name &&
- actual.message === expected.message;
-
- // expected is a validation function which returns true if validation passed
- } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) {
- expectedOutput = null;
- ok = true;
- }
-
- this.push( ok, actual, expectedOutput, message );
- } else {
- this.test.pushFailure( message, null, "No exception was thrown." );
- }
- }
-};
-
-// Provide an alternative to assert.throws(), for enviroments that consider throws a reserved word
-// Known to us are: Closure Compiler, Narwhal
-(function() {
- /*jshint sub:true */
- Assert.prototype.raises = Assert.prototype[ "throws" ];
-}());
-
-// Test for equality any JavaScript type.
-// Author: Philippe Rathé
-QUnit.equiv = (function() {
-
- // Call the o related callback with the given arguments.
- function bindCallbacks( o, callbacks, args ) {
- var prop = QUnit.objectType( o );
- if ( prop ) {
- if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) {
- return callbacks[ prop ].apply( callbacks, args );
- } else {
- return callbacks[ prop ]; // or undefined
- }
- }
- }
-
- // the real equiv function
- var innerEquiv,
-
- // stack to decide between skip/abort functions
- callers = [],
-
- // stack to avoiding loops from circular referencing
- parents = [],
- parentsB = [],
-
- getProto = Object.getPrototypeOf || function( obj ) {
- /* jshint camelcase: false, proto: true */
- return obj.__proto__;
- },
- callbacks = (function() {
-
- // for string, boolean, number and null
- function useStrictEquality( b, a ) {
-
- /*jshint eqeqeq:false */
- if ( b instanceof a.constructor || a instanceof b.constructor ) {
-
- // to catch short annotation VS 'new' annotation of a
- // declaration
- // e.g. var i = 1;
- // var j = new Number(1);
- return a == b;
- } else {
- return a === b;
- }
- }
-
- return {
- "string": useStrictEquality,
- "boolean": useStrictEquality,
- "number": useStrictEquality,
- "null": useStrictEquality,
- "undefined": useStrictEquality,
-
- "nan": function( b ) {
- return isNaN( b );
- },
-
- "date": function( b, a ) {
- return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf();
- },
-
- "regexp": function( b, a ) {
- return QUnit.objectType( b ) === "regexp" &&
-
- // the regex itself
- a.source === b.source &&
-
- // and its modifiers
- a.global === b.global &&
-
- // (gmi) ...
- a.ignoreCase === b.ignoreCase &&
- a.multiline === b.multiline &&
- a.sticky === b.sticky;
- },
-
- // - skip when the property is a method of an instance (OOP)
- // - abort otherwise,
- // initial === would have catch identical references anyway
- "function": function() {
- var caller = callers[ callers.length - 1 ];
- return caller !== Object && typeof caller !== "undefined";
- },
-
- "array": function( b, a ) {
- var i, j, len, loop, aCircular, bCircular;
-
- // b could be an object literal here
- if ( QUnit.objectType( b ) !== "array" ) {
- return false;
- }
-
- len = a.length;
- if ( len !== b.length ) {
- // safe and faster
- return false;
- }
-
- // track reference to avoid circular references
- parents.push( a );
- parentsB.push( b );
- for ( i = 0; i < len; i++ ) {
- loop = false;
- for ( j = 0; j < parents.length; j++ ) {
- aCircular = parents[ j ] === a[ i ];
- bCircular = parentsB[ j ] === b[ i ];
- if ( aCircular || bCircular ) {
- if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
- loop = true;
- } else {
- parents.pop();
- parentsB.pop();
- return false;
- }
- }
- }
- if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
- parents.pop();
- parentsB.pop();
- return false;
- }
- }
- parents.pop();
- parentsB.pop();
- return true;
- },
-
- "object": function( b, a ) {
-
- /*jshint forin:false */
- var i, j, loop, aCircular, bCircular,
- // Default to true
- eq = true,
- aProperties = [],
- bProperties = [];
-
- // comparing constructors is more strict than using
- // instanceof
- if ( a.constructor !== b.constructor ) {
-
- // Allow objects with no prototype to be equivalent to
- // objects with Object as their constructor.
- if ( !( ( getProto( a ) === null && getProto( b ) === Object.prototype ) ||
- ( getProto( b ) === null && getProto( a ) === Object.prototype ) ) ) {
- return false;
- }
- }
-
- // stack constructor before traversing properties
- callers.push( a.constructor );
-
- // track reference to avoid circular references
- parents.push( a );
- parentsB.push( b );
-
- // be strict: don't ensure hasOwnProperty and go deep
- for ( i in a ) {
- loop = false;
- for ( j = 0; j < parents.length; j++ ) {
- aCircular = parents[ j ] === a[ i ];
- bCircular = parentsB[ j ] === b[ i ];
- if ( aCircular || bCircular ) {
- if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
- loop = true;
- } else {
- eq = false;
- break;
- }
- }
- }
- aProperties.push( i );
- if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
- eq = false;
- break;
- }
- }
-
- parents.pop();
- parentsB.pop();
- callers.pop(); // unstack, we are done
-
- for ( i in b ) {
- bProperties.push( i ); // collect b's properties
- }
-
- // Ensures identical properties name
- return eq && innerEquiv( aProperties.sort(), bProperties.sort() );
- }
- };
- }());
-
- innerEquiv = function() { // can take multiple arguments
- var args = [].slice.apply( arguments );
- if ( args.length < 2 ) {
- return true; // end transition
- }
-
- return ( (function( a, b ) {
- if ( a === b ) {
- return true; // catch the most you can
- } else if ( a === null || b === null || typeof a === "undefined" ||
- typeof b === "undefined" ||
- QUnit.objectType( a ) !== QUnit.objectType( b ) ) {
-
- // don't lose time with error prone cases
- return false;
- } else {
- return bindCallbacks( a, callbacks, [ b, a ] );
- }
-
- // apply transition with (1..n) arguments
- }( args[ 0 ], args[ 1 ] ) ) &&
- innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) );
- };
-
- return innerEquiv;
-}());
-
-// Based on jsDump by Ariel Flesler
-// http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html
-QUnit.dump = (function() {
- function quote( str ) {
- return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\"";
- }
- function literal( o ) {
- return o + "";
- }
- function join( pre, arr, post ) {
- var s = dump.separator(),
- base = dump.indent(),
- inner = dump.indent( 1 );
- if ( arr.join ) {
- arr = arr.join( "," + s + inner );
- }
- if ( !arr ) {
- return pre + post;
- }
- return [ pre, inner + arr, base + post ].join( s );
- }
- function array( arr, stack ) {
- var i = arr.length,
- ret = new Array( i );
-
- if ( dump.maxDepth && dump.depth > dump.maxDepth ) {
- return "[object Array]";
- }
-
- this.up();
- while ( i-- ) {
- ret[ i ] = this.parse( arr[ i ], undefined, stack );
- }
- this.down();
- return join( "[", ret, "]" );
- }
-
- var reName = /^function (\w+)/,
- dump = {
-
- // objType is used mostly internally, you can fix a (custom) type in advance
- parse: function( obj, objType, stack ) {
- stack = stack || [];
- var res, parser, parserType,
- inStack = inArray( obj, stack );
-
- if ( inStack !== -1 ) {
- return "recursion(" + ( inStack - stack.length ) + ")";
- }
-
- objType = objType || this.typeOf( obj );
- parser = this.parsers[ objType ];
- parserType = typeof parser;
-
- if ( parserType === "function" ) {
- stack.push( obj );
- res = parser.call( this, obj, stack );
- stack.pop();
- return res;
- }
- return ( parserType === "string" ) ? parser : this.parsers.error;
- },
- typeOf: function( obj ) {
- var type;
- if ( obj === null ) {
- type = "null";
- } else if ( typeof obj === "undefined" ) {
- type = "undefined";
- } else if ( QUnit.is( "regexp", obj ) ) {
- type = "regexp";
- } else if ( QUnit.is( "date", obj ) ) {
- type = "date";
- } else if ( QUnit.is( "function", obj ) ) {
- type = "function";
- } else if ( obj.setInterval !== undefined &&
- obj.document !== undefined &&
- obj.nodeType === undefined ) {
- type = "window";
- } else if ( obj.nodeType === 9 ) {
- type = "document";
- } else if ( obj.nodeType ) {
- type = "node";
- } else if (
-
- // native arrays
- toString.call( obj ) === "[object Array]" ||
-
- // NodeList objects
- ( typeof obj.length === "number" && obj.item !== undefined &&
- ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null &&
- obj[ 0 ] === undefined ) ) )
- ) {
- type = "array";
- } else if ( obj.constructor === Error.prototype.constructor ) {
- type = "error";
- } else {
- type = typeof obj;
- }
- return type;
- },
- separator: function() {
- return this.multiline ? this.HTML ? " " : "\n" : this.HTML ? " " : " ";
- },
- // extra can be a number, shortcut for increasing-calling-decreasing
- indent: function( extra ) {
- if ( !this.multiline ) {
- return "";
- }
- var chr = this.indentChar;
- if ( this.HTML ) {
- chr = chr.replace( /\t/g, " " ).replace( / /g, " " );
- }
- return new Array( this.depth + ( extra || 0 ) ).join( chr );
- },
- up: function( a ) {
- this.depth += a || 1;
- },
- down: function( a ) {
- this.depth -= a || 1;
- },
- setParser: function( name, parser ) {
- this.parsers[ name ] = parser;
- },
- // The next 3 are exposed so you can use them
- quote: quote,
- literal: literal,
- join: join,
- //
- depth: 1,
- maxDepth: 5,
-
- // This is the list of parsers, to modify them, use dump.setParser
- parsers: {
- window: "[Window]",
- document: "[Document]",
- error: function( error ) {
- return "Error(\"" + error.message + "\")";
- },
- unknown: "[Unknown]",
- "null": "null",
- "undefined": "undefined",
- "function": function( fn ) {
- var ret = "function",
-
- // functions never have name in IE
- name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ];
-
- if ( name ) {
- ret += " " + name;
- }
- ret += "( ";
-
- ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" );
- return join( ret, dump.parse( fn, "functionCode" ), "}" );
- },
- array: array,
- nodelist: array,
- "arguments": array,
- object: function( map, stack ) {
- var keys, key, val, i, nonEnumerableProperties,
- ret = [];
-
- if ( dump.maxDepth && dump.depth > dump.maxDepth ) {
- return "[object Object]";
- }
-
- dump.up();
- keys = [];
- for ( key in map ) {
- keys.push( key );
- }
-
- // Some properties are not always enumerable on Error objects.
- nonEnumerableProperties = [ "message", "name" ];
- for ( i in nonEnumerableProperties ) {
- key = nonEnumerableProperties[ i ];
- if ( key in map && !( key in keys ) ) {
- keys.push( key );
- }
- }
- keys.sort();
- for ( i = 0; i < keys.length; i++ ) {
- key = keys[ i ];
- val = map[ key ];
- ret.push( dump.parse( key, "key" ) + ": " +
- dump.parse( val, undefined, stack ) );
- }
- dump.down();
- return join( "{", ret, "}" );
- },
- node: function( node ) {
- var len, i, val,
- open = dump.HTML ? "<" : "<",
- close = dump.HTML ? ">" : ">",
- tag = node.nodeName.toLowerCase(),
- ret = open + tag,
- attrs = node.attributes;
-
- if ( attrs ) {
- for ( i = 0, len = attrs.length; i < len; i++ ) {
- val = attrs[ i ].nodeValue;
-
- // IE6 includes all attributes in .attributes, even ones not explicitly
- // set. Those have values like undefined, null, 0, false, "" or
- // "inherit".
- if ( val && val !== "inherit" ) {
- ret += " " + attrs[ i ].nodeName + "=" +
- dump.parse( val, "attribute" );
- }
- }
- }
- ret += close;
-
- // Show content of TextNode or CDATASection
- if ( node.nodeType === 3 || node.nodeType === 4 ) {
- ret += node.nodeValue;
- }
-
- return ret + open + "/" + tag + close;
- },
-
- // function calls it internally, it's the arguments part of the function
- functionArgs: function( fn ) {
- var args,
- l = fn.length;
-
- if ( !l ) {
- return "";
- }
-
- args = new Array( l );
- while ( l-- ) {
-
- // 97 is 'a'
- args[ l ] = String.fromCharCode( 97 + l );
- }
- return " " + args.join( ", " ) + " ";
- },
- // object calls it internally, the key part of an item in a map
- key: quote,
- // function calls it internally, it's the content of the function
- functionCode: "[code]",
- // node calls it internally, it's an html attribute value
- attribute: quote,
- string: quote,
- date: quote,
- regexp: literal,
- number: literal,
- "boolean": literal
- },
- // if true, entities are escaped ( <, >, \t, space and \n )
- HTML: false,
- // indentation unit
- indentChar: " ",
- // if true, items in a collection, are separated by a \n, else just a space.
- multiline: true
- };
-
- return dump;
-}());
-
-// back compat
-QUnit.jsDump = QUnit.dump;
-
-// For browser, export only select globals
-if ( typeof window !== "undefined" ) {
-
- // Deprecated
- // Extend assert methods to QUnit and Global scope through Backwards compatibility
- (function() {
- var i,
- assertions = Assert.prototype;
-
- function applyCurrent( current ) {
- return function() {
- var assert = new Assert( QUnit.config.current );
- current.apply( assert, arguments );
- };
- }
-
- for ( i in assertions ) {
- QUnit[ i ] = applyCurrent( assertions[ i ] );
- }
- })();
-
- (function() {
- var i, l,
- keys = [
- "test",
- "module",
- "expect",
- "asyncTest",
- "start",
- "stop",
- "ok",
- "equal",
- "notEqual",
- "propEqual",
- "notPropEqual",
- "deepEqual",
- "notDeepEqual",
- "strictEqual",
- "notStrictEqual",
- "throws"
- ];
-
- for ( i = 0, l = keys.length; i < l; i++ ) {
- window[ keys[ i ] ] = QUnit[ keys[ i ] ];
- }
- })();
-
- window.QUnit = QUnit;
-}
-
-// For nodejs
-if ( typeof module !== "undefined" && module && module.exports ) {
- module.exports = QUnit;
-
- // For consistency with CommonJS environments' exports
- module.exports.QUnit = QUnit;
-}
-
-// For CommonJS with exports, but without module.exports, like Rhino
-if ( typeof exports !== "undefined" && exports ) {
- exports.QUnit = QUnit;
-}
-
-// Get a reference to the global object, like window in browsers
-}( (function() {
- return this;
-})() ));
-
-/*istanbul ignore next */
-// jscs:disable maximumLineLength
-/*
- * Javascript Diff Algorithm
- * By John Resig (http://ejohn.org/)
- * Modified by Chu Alan "sprite"
- *
- * Released under the MIT license.
- *
- * More Info:
- * http://ejohn.org/projects/javascript-diff-algorithm/
- *
- * Usage: QUnit.diff(expected, actual)
- *
- * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over"
- */
-QUnit.diff = (function() {
- var hasOwn = Object.prototype.hasOwnProperty;
-
- /*jshint eqeqeq:false, eqnull:true */
- function diff( o, n ) {
- var i,
- ns = {},
- os = {};
-
- for ( i = 0; i < n.length; i++ ) {
- if ( !hasOwn.call( ns, n[ i ] ) ) {
- ns[ n[ i ] ] = {
- rows: [],
- o: null
- };
- }
- ns[ n[ i ] ].rows.push( i );
- }
-
- for ( i = 0; i < o.length; i++ ) {
- if ( !hasOwn.call( os, o[ i ] ) ) {
- os[ o[ i ] ] = {
- rows: [],
- n: null
- };
- }
- os[ o[ i ] ].rows.push( i );
- }
-
- for ( i in ns ) {
- if ( hasOwn.call( ns, i ) ) {
- if ( ns[ i ].rows.length === 1 && hasOwn.call( os, i ) && os[ i ].rows.length === 1 ) {
- n[ ns[ i ].rows[ 0 ] ] = {
- text: n[ ns[ i ].rows[ 0 ] ],
- row: os[ i ].rows[ 0 ]
- };
- o[ os[ i ].rows[ 0 ] ] = {
- text: o[ os[ i ].rows[ 0 ] ],
- row: ns[ i ].rows[ 0 ]
- };
- }
- }
- }
-
- for ( i = 0; i < n.length - 1; i++ ) {
- if ( n[ i ].text != null && n[ i + 1 ].text == null && n[ i ].row + 1 < o.length && o[ n[ i ].row + 1 ].text == null &&
- n[ i + 1 ] == o[ n[ i ].row + 1 ] ) {
-
- n[ i + 1 ] = {
- text: n[ i + 1 ],
- row: n[ i ].row + 1
- };
- o[ n[ i ].row + 1 ] = {
- text: o[ n[ i ].row + 1 ],
- row: i + 1
- };
- }
- }
-
- for ( i = n.length - 1; i > 0; i-- ) {
- if ( n[ i ].text != null && n[ i - 1 ].text == null && n[ i ].row > 0 && o[ n[ i ].row - 1 ].text == null &&
- n[ i - 1 ] == o[ n[ i ].row - 1 ] ) {
-
- n[ i - 1 ] = {
- text: n[ i - 1 ],
- row: n[ i ].row - 1
- };
- o[ n[ i ].row - 1 ] = {
- text: o[ n[ i ].row - 1 ],
- row: i - 1
- };
- }
- }
-
- return {
- o: o,
- n: n
- };
- }
-
- return function( o, n ) {
- o = o.replace( /\s+$/, "" );
- n = n.replace( /\s+$/, "" );
-
- var i, pre,
- str = "",
- out = diff( o === "" ? [] : o.split( /\s+/ ), n === "" ? [] : n.split( /\s+/ ) ),
- oSpace = o.match( /\s+/g ),
- nSpace = n.match( /\s+/g );
-
- if ( oSpace == null ) {
- oSpace = [ " " ];
- } else {
- oSpace.push( " " );
- }
-
- if ( nSpace == null ) {
- nSpace = [ " " ];
- } else {
- nSpace.push( " " );
- }
-
- if ( out.n.length === 0 ) {
- for ( i = 0; i < out.o.length; i++ ) {
- str += "" + out.o[ i ] + oSpace[ i ] + "";
- }
- } else {
- if ( out.n[ 0 ].text == null ) {
- for ( n = 0; n < out.o.length && out.o[ n ].text == null; n++ ) {
- str += "" + out.o[ n ] + oSpace[ n ] + "";
- }
- }
-
- for ( i = 0; i < out.n.length; i++ ) {
- if ( out.n[ i ].text == null ) {
- str += "" + out.n[ i ] + nSpace[ i ] + "";
- } else {
-
- // `pre` initialized at top of scope
- pre = "";
-
- for ( n = out.n[ i ].row + 1; n < out.o.length && out.o[ n ].text == null; n++ ) {
- pre += "" + out.o[ n ] + oSpace[ n ] + "";
- }
- str += " " + out.n[ i ].text + nSpace[ i ] + pre;
- }
- }
- }
-
- return str;
- };
-}());
-// jscs:enable
-
-(function() {
-
-// Deprecated QUnit.init - Ref #530
-// Re-initialize the configuration options
-QUnit.init = function() {
- var tests, banner, result, qunit,
- config = QUnit.config;
-
- config.stats = { all: 0, bad: 0 };
- config.moduleStats = { all: 0, bad: 0 };
- config.started = 0;
- config.updateRate = 1000;
- config.blocking = false;
- config.autostart = true;
- config.autorun = false;
- config.filter = "";
- config.queue = [];
-
- // Return on non-browser environments
- // This is necessary to not break on node tests
- if ( typeof window === "undefined" ) {
- return;
- }
-
- qunit = id( "qunit" );
- if ( qunit ) {
- qunit.innerHTML =
- "
" + escapeText( document.title ) + "
" +
- "" +
- "" +
- "" +
- "";
- }
-
- tests = id( "qunit-tests" );
- banner = id( "qunit-banner" );
- result = id( "qunit-testresult" );
-
- if ( tests ) {
- tests.innerHTML = "";
- }
-
- if ( banner ) {
- banner.className = "";
- }
-
- if ( result ) {
- result.parentNode.removeChild( result );
- }
-
- if ( tests ) {
- result = document.createElement( "p" );
- result.id = "qunit-testresult";
- result.className = "result";
- tests.parentNode.insertBefore( result, tests );
- result.innerHTML = "Running... ";
- }
-};
-
-// Don't load the HTML Reporter on non-Browser environments
-if ( typeof window === "undefined" ) {
- return;
-}
-
-var config = QUnit.config,
- hasOwn = Object.prototype.hasOwnProperty,
- defined = {
- document: window.document !== undefined,
- sessionStorage: (function() {
- var x = "qunit-test-string";
- try {
- sessionStorage.setItem( x, x );
- sessionStorage.removeItem( x );
- return true;
- } catch ( e ) {
- return false;
- }
- }())
- },
- modulesList = [];
-
-/**
-* Escape text for attribute or text content.
-*/
-function escapeText( s ) {
- if ( !s ) {
- return "";
- }
- s = s + "";
-
- // Both single quotes and double quotes (for attributes)
- return s.replace( /['"<>&]/g, function( s ) {
- switch ( s ) {
- case "'":
- return "'";
- case "\"":
- return """;
- case "<":
- return "<";
- case ">":
- return ">";
- case "&":
- return "&";
- }
- });
-}
-
-/**
- * @param {HTMLElement} elem
- * @param {string} type
- * @param {Function} fn
- */
-function addEvent( elem, type, fn ) {
- if ( elem.addEventListener ) {
-
- // Standards-based browsers
- elem.addEventListener( type, fn, false );
- } else if ( elem.attachEvent ) {
-
- // support: IE <9
- elem.attachEvent( "on" + type, fn );
- }
-}
-
-/**
- * @param {Array|NodeList} elems
- * @param {string} type
- * @param {Function} fn
- */
-function addEvents( elems, type, fn ) {
- var i = elems.length;
- while ( i-- ) {
- addEvent( elems[ i ], type, fn );
- }
-}
-
-function hasClass( elem, name ) {
- return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0;
-}
-
-function addClass( elem, name ) {
- if ( !hasClass( elem, name ) ) {
- elem.className += ( elem.className ? " " : "" ) + name;
- }
-}
-
-function toggleClass( elem, name ) {
- if ( hasClass( elem, name ) ) {
- removeClass( elem, name );
- } else {
- addClass( elem, name );
- }
-}
-
-function removeClass( elem, name ) {
- var set = " " + elem.className + " ";
-
- // Class name may appear multiple times
- while ( set.indexOf( " " + name + " " ) >= 0 ) {
- set = set.replace( " " + name + " ", " " );
- }
-
- // trim for prettiness
- elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" );
-}
-
-function id( name ) {
- return defined.document && document.getElementById && document.getElementById( name );
-}
-
-function getUrlConfigHtml() {
- var i, j, val,
- escaped, escapedTooltip,
- selection = false,
- len = config.urlConfig.length,
- urlConfigHtml = "";
-
- for ( i = 0; i < len; i++ ) {
- val = config.urlConfig[ i ];
- if ( typeof val === "string" ) {
- val = {
- id: val,
- label: val
- };
- }
-
- escaped = escapeText( val.id );
- escapedTooltip = escapeText( val.tooltip );
-
- if ( config[ val.id ] === undefined ) {
- config[ val.id ] = QUnit.urlParams[ val.id ];
- }
-
- if ( !val.value || typeof val.value === "string" ) {
- urlConfigHtml += "";
- } else {
- urlConfigHtml += "";
- }
- }
-
- return urlConfigHtml;
-}
-
-// Handle "click" events on toolbar checkboxes and "change" for select menus.
-// Updates the URL with the new state of `config.urlConfig` values.
-function toolbarChanged() {
- var updatedUrl, value,
- field = this,
- params = {};
-
- // Detect if field is a select menu or a checkbox
- if ( "selectedIndex" in field ) {
- value = field.options[ field.selectedIndex ].value || undefined;
- } else {
- value = field.checked ? ( field.defaultValue || true ) : undefined;
- }
-
- params[ field.name ] = value;
- updatedUrl = setUrl( params );
-
- if ( "hidepassed" === field.name && "replaceState" in window.history ) {
- config[ field.name ] = value || false;
- if ( value ) {
- addClass( id( "qunit-tests" ), "hidepass" );
- } else {
- removeClass( id( "qunit-tests" ), "hidepass" );
- }
-
- // It is not necessary to refresh the whole page
- window.history.replaceState( null, "", updatedUrl );
- } else {
- window.location = updatedUrl;
- }
-}
-
-function setUrl( params ) {
- var key,
- querystring = "?";
-
- params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params );
-
- for ( key in params ) {
- if ( hasOwn.call( params, key ) ) {
- if ( params[ key ] === undefined ) {
- continue;
- }
- querystring += encodeURIComponent( key );
- if ( params[ key ] !== true ) {
- querystring += "=" + encodeURIComponent( params[ key ] );
- }
- querystring += "&";
- }
- }
- return location.protocol + "//" + location.host +
- location.pathname + querystring.slice( 0, -1 );
-}
-
-function applyUrlParams() {
- var selectBox = id( "qunit-modulefilter" ),
- selection = decodeURIComponent( selectBox.options[ selectBox.selectedIndex ].value ),
- filter = id( "qunit-filter-input" ).value;
-
- window.location = setUrl({
- module: ( selection === "" ) ? undefined : selection,
- filter: ( filter === "" ) ? undefined : filter,
-
- // Remove testId filter
- testId: undefined
- });
-}
-
-function toolbarUrlConfigContainer() {
- var urlConfigContainer = document.createElement( "span" );
-
- urlConfigContainer.innerHTML = getUrlConfigHtml();
- addClass( urlConfigContainer, "qunit-url-config" );
-
- // For oldIE support:
- // * Add handlers to the individual elements instead of the container
- // * Use "click" instead of "change" for checkboxes
- addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", toolbarChanged );
- addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged );
-
- return urlConfigContainer;
-}
-
-function toolbarLooseFilter() {
- var filter = document.createElement( "form" ),
- label = document.createElement( "label" ),
- input = document.createElement( "input" ),
- button = document.createElement( "button" );
-
- addClass( filter, "qunit-filter" );
-
- label.innerHTML = "Filter: ";
-
- input.type = "text";
- input.value = config.filter || "";
- input.name = "filter";
- input.id = "qunit-filter-input";
-
- button.innerHTML = "Go";
-
- label.appendChild( input );
-
- filter.appendChild( label );
- filter.appendChild( button );
- addEvent( filter, "submit", function( ev ) {
- applyUrlParams();
-
- if ( ev && ev.preventDefault ) {
- ev.preventDefault();
- }
-
- return false;
- });
-
- return filter;
-}
-
-function toolbarModuleFilterHtml() {
- var i,
- moduleFilterHtml = "";
-
- if ( !modulesList.length ) {
- return false;
- }
-
- modulesList.sort(function( a, b ) {
- return a.localeCompare( b );
- });
-
- moduleFilterHtml += "" +
- "";
-
- return moduleFilterHtml;
-}
-
-function toolbarModuleFilter() {
- var toolbar = id( "qunit-testrunner-toolbar" ),
- moduleFilter = document.createElement( "span" ),
- moduleFilterHtml = toolbarModuleFilterHtml();
-
- if ( !toolbar || !moduleFilterHtml ) {
- return false;
- }
-
- moduleFilter.setAttribute( "id", "qunit-modulefilter-container" );
- moduleFilter.innerHTML = moduleFilterHtml;
-
- addEvent( moduleFilter.lastChild, "change", applyUrlParams );
-
- toolbar.appendChild( moduleFilter );
-}
-
-function appendToolbar() {
- var toolbar = id( "qunit-testrunner-toolbar" );
-
- if ( toolbar ) {
- toolbar.appendChild( toolbarUrlConfigContainer() );
- toolbar.appendChild( toolbarLooseFilter() );
- }
-}
-
-function appendHeader() {
- var header = id( "qunit-header" );
-
- if ( header ) {
- header.innerHTML = "" + header.innerHTML + " ";
- }
-}
-
-function appendBanner() {
- var banner = id( "qunit-banner" );
-
- if ( banner ) {
- banner.className = "";
- }
-}
-
-function appendTestResults() {
- var tests = id( "qunit-tests" ),
- result = id( "qunit-testresult" );
-
- if ( result ) {
- result.parentNode.removeChild( result );
- }
-
- if ( tests ) {
- tests.innerHTML = "";
- result = document.createElement( "p" );
- result.id = "qunit-testresult";
- result.className = "result";
- tests.parentNode.insertBefore( result, tests );
- result.innerHTML = "Running... ";
- }
-}
-
-function storeFixture() {
- var fixture = id( "qunit-fixture" );
- if ( fixture ) {
- config.fixture = fixture.innerHTML;
- }
-}
-
-function appendUserAgent() {
- var userAgent = id( "qunit-userAgent" );
- if ( userAgent ) {
- userAgent.innerHTML = "";
- userAgent.appendChild( document.createTextNode( navigator.userAgent ) );
- }
-}
-
-function appendTestsList( modules ) {
- var i, l, x, z, test, moduleObj;
-
- for ( i = 0, l = modules.length; i < l; i++ ) {
- moduleObj = modules[ i ];
-
- if ( moduleObj.name ) {
- modulesList.push( moduleObj.name );
- }
-
- for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) {
- test = moduleObj.tests[ x ];
-
- appendTest( test.name, test.testId, moduleObj.name );
- }
- }
-}
-
-function appendTest( name, testId, moduleName ) {
- var title, rerunTrigger, testBlock, assertList,
- tests = id( "qunit-tests" );
-
- if ( !tests ) {
- return;
- }
-
- title = document.createElement( "strong" );
- title.innerHTML = getNameHtml( name, moduleName );
-
- rerunTrigger = document.createElement( "a" );
- rerunTrigger.innerHTML = "Rerun";
- rerunTrigger.href = setUrl({ testId: testId });
-
- testBlock = document.createElement( "li" );
- testBlock.appendChild( title );
- testBlock.appendChild( rerunTrigger );
- testBlock.id = "qunit-test-output-" + testId;
-
- assertList = document.createElement( "ol" );
- assertList.className = "qunit-assert-list";
-
- testBlock.appendChild( assertList );
-
- tests.appendChild( testBlock );
-}
-
-// HTML Reporter initialization and load
-QUnit.begin(function( details ) {
- var qunit = id( "qunit" );
-
- // Fixture is the only one necessary to run without the #qunit element
- storeFixture();
-
- if ( qunit ) {
- qunit.innerHTML =
- "
" + escapeText( document.title ) + "
" +
- "" +
- "" +
- "" +
- "";
- }
-
- appendHeader();
- appendBanner();
- appendTestResults();
- appendUserAgent();
- appendToolbar();
- appendTestsList( details.modules );
- toolbarModuleFilter();
-
- if ( qunit && config.hidepassed ) {
- addClass( qunit.lastChild, "hidepass" );
- }
-});
-
-QUnit.done(function( details ) {
- var i, key,
- banner = id( "qunit-banner" ),
- tests = id( "qunit-tests" ),
- html = [
- "Tests completed in ",
- details.runtime,
- " milliseconds. ",
- "",
- details.passed,
- " assertions of ",
- details.total,
- " passed, ",
- details.failed,
- " failed."
- ].join( "" );
-
- if ( banner ) {
- banner.className = details.failed ? "qunit-fail" : "qunit-pass";
- }
-
- if ( tests ) {
- id( "qunit-testresult" ).innerHTML = html;
- }
-
- if ( config.altertitle && defined.document && document.title ) {
-
- // show ✖ for good, ✔ for bad suite result in title
- // use escape sequences in case file gets loaded with non-utf-8-charset
- document.title = [
- ( details.failed ? "\u2716" : "\u2714" ),
- document.title.replace( /^[\u2714\u2716] /i, "" )
- ].join( " " );
- }
-
- // clear own sessionStorage items if all tests passed
- if ( config.reorder && defined.sessionStorage && details.failed === 0 ) {
- for ( i = 0; i < sessionStorage.length; i++ ) {
- key = sessionStorage.key( i++ );
- if ( key.indexOf( "qunit-test-" ) === 0 ) {
- sessionStorage.removeItem( key );
- }
- }
- }
-
- // scroll back to top to show results
- if ( config.scrolltop && window.scrollTo ) {
- window.scrollTo( 0, 0 );
- }
-});
-
-function getNameHtml( name, module ) {
- var nameHtml = "";
-
- if ( module ) {
- nameHtml = "" + escapeText( module ) + ": ";
- }
-
- nameHtml += "" + escapeText( name ) + "";
-
- return nameHtml;
-}
-
-QUnit.testStart(function( details ) {
- var running, testBlock;
-
- testBlock = id( "qunit-test-output-" + details.testId );
- if ( testBlock ) {
- testBlock.className = "running";
- } else {
-
- // Report later registered tests
- appendTest( details.name, details.testId, details.module );
- }
-
- running = id( "qunit-testresult" );
- if ( running ) {
- running.innerHTML = "Running: " + getNameHtml( details.name, details.module );
- }
-
-});
-
-QUnit.log(function( details ) {
- var assertList, assertLi,
- message, expected, actual,
- testItem = id( "qunit-test-output-" + details.testId );
-
- if ( !testItem ) {
- return;
- }
-
- message = escapeText( details.message ) || ( details.result ? "okay" : "failed" );
- message = "" + message + "";
- message += "@ " + details.runtime + " ms";
-
- // pushFailure doesn't provide details.expected
- // when it calls, it's implicit to also not show expected and diff stuff
- // Also, we need to check details.expected existence, as it can exist and be undefined
- if ( !details.result && hasOwn.call( details, "expected" ) ) {
- expected = escapeText( QUnit.dump.parse( details.expected ) );
- actual = escapeText( QUnit.dump.parse( details.actual ) );
- message += "
Expected:
" +
- expected +
- "
";
-
- if ( actual !== expected ) {
- message += "
Result:
" +
- actual + "
" +
- "
Diff:
" +
- QUnit.diff( expected, actual ) + "
";
- }
-
- if ( details.source ) {
- message += "
Source:
" +
- escapeText( details.source ) + "
";
- }
-
- message += "
";
-
- // this occours when pushFailure is set and we have an extracted stack trace
- } else if ( !details.result && details.source ) {
- message += "