diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 00000000..3a80eb85 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,4 @@ +{ + "directory": "src/plugins", + "registry": "http://adapt-bower-repository.herokuapp.com/" +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..4cee5402 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: + - 0.10 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..a8f555c4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. {http://fsf.org/} + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + adapt-hello-world Copyright (C) 2013 Chris Jones + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +{http://www.gnu.org/licenses/}. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +{http://www.gnu.org/philosophy/why-not-lgpl.html}. diff --git a/bower.json b/bower.json new file mode 100644 index 00000000..352429e2 --- /dev/null +++ b/bower.json @@ -0,0 +1,15 @@ +{ + "name": "adapt-contrib-spoor", + "version": "0.0.1", + "homepage": "https://github.com/adaptlearning/adapt-contrib-spoor", + "authors": [ + "Kevin Corry " + ], + "description": "A basic scorm tracking plugin for Adapt", + "main": "/js/adapt-contrib-spoor.js", + "keywords": [ + "adapt-plugin", + "adapt-extension" + ], + "license": "GPLv3" +} diff --git a/js/SCORM_API_wrapper.js b/js/SCORM_API_wrapper.js new file mode 100644 index 00000000..c6733700 --- /dev/null +++ b/js/SCORM_API_wrapper.js @@ -0,0 +1,814 @@ + +/* ===================================================================================== + +SCORM wrapper v1.1.7 by Philip Hutchison, May 2008 (http://pipwerks.com). + +Copyright (c) 2008 Philip Hutchison +MIT-style license. Full license text can be found at +http://www.opensource.org/licenses/mit-license.php + +This wrapper is designed to work with both SCORM 1.2 and SCORM 2004. + +Based on APIWrapper.js, created by the ADL and Concurrent Technologies +Corporation, distributed by the ADL (http://www.adlnet.gov/scorm). + +SCORM.API.find() and SCORM.API.get() functions based on ADL code, +modified by Mike Rustici (http://www.scorm.com/resources/apifinder/SCORMAPIFinder.htm), +further modified by Philip Hutchison + +======================================================================================== */ + + +var pipwerks = {}; //pipwerks 'namespace' helps ensure no conflicts with possible other "SCORM" variables +pipwerks.UTILS = {}; //For holding UTILS functions +pipwerks.debug = { isActive: true }; //Enable (true) or disable (false) for debug mode + +pipwerks.SCORM = { //Define the SCORM object + version: null, //Store SCORM version. + handleCompletionStatus: true, //Whether or not the wrapper should automatically handle the initial completion status + handleExitMode: true, //Whether or not the wrapper should automatically handle the exit mode + API: { handle: null, + isFound: false }, //Create API child object + connection: { isActive: false }, //Create connection child object + data: { completionStatus: null, + exitStatus: null }, //Create data child object + debug: {} //Create debug child object +}; + + + +/* -------------------------------------------------------------------------------- + pipwerks.SCORM.isAvailable + A simple function to allow Flash ExternalInterface to confirm + presence of JS wrapper before attempting any LMS communication. + + Parameters: none + Returns: Boolean (true) +----------------------------------------------------------------------------------- */ + +pipwerks.SCORM.isAvailable = function(){ + return true; +}; + + + +// ------------------------------------------------------------------------- // +// --- SCORM.API functions ------------------------------------------------- // +// ------------------------------------------------------------------------- // + + +/* ------------------------------------------------------------------------- + pipwerks.SCORM.API.find(window) + Looks for an object named API in parent and opener windows + + Parameters: window (the browser window object). + Returns: Object if API is found, null if no API found +---------------------------------------------------------------------------- */ + +pipwerks.SCORM.API.find = function(win){ + + var API = null, + findAttempts = 0, + findAttemptLimit = 500, + traceMsgPrefix = "SCORM.API.find", + trace = pipwerks.UTILS.trace, + scorm = pipwerks.SCORM; + + while ((!win.API && !win.API_1484_11) && + (win.parent) && + (win.parent != win) && + (findAttempts <= findAttemptLimit)){ + + findAttempts++; + win = win.parent; + + } + + if(scorm.version){ //If SCORM version is specified by user, look for specific API + + switch(scorm.version){ + + case "2004" : + + if(win.API_1484_11){ + + API = win.API_1484_11; + + } else { + + trace(traceMsgPrefix +": SCORM version 2004 was specified by user, but API_1484_11 cannot be found."); + + } + + break; + + case "1.2" : + + if(win.API){ + + API = win.API; + + } else { + + trace(traceMsgPrefix +": SCORM version 1.2 was specified by user, but API cannot be found."); + + } + + break; + + } + + } else { //If SCORM version not specified by user, look for APIs + + if(win.API_1484_11) { //SCORM 2004-specific API. + + scorm.version = "2004"; //Set version + API = win.API_1484_11; + + } else if(win.API){ //SCORM 1.2-specific API + + scorm.version = "1.2"; //Set version + API = win.API; + + } + + } + + if(API){ + + trace(traceMsgPrefix +": API found. Version: " +scorm.version); + trace("API: " +API); + + } else { + + trace(traceMsgPrefix +": Error finding API. \nFind attempts: " +findAttempts +". \nFind attempt limit: " +findAttemptLimit); + + } + + return API; + +}; + + +/* ------------------------------------------------------------------------- + pipwerks.SCORM.API.get() + Looks for an object named API, first in the current window's frame + hierarchy and then, if necessary, in the current window's opener window + hierarchy (if there is an opener window). + + Parameters: None. + Returns: Object if API found, null if no API found +---------------------------------------------------------------------------- */ + +pipwerks.SCORM.API.get = function(){ + + var API = null, + win = window, + find = pipwerks.SCORM.API.find, + trace = pipwerks.UTILS.trace; + + if(win.parent && win.parent != win){ + + API = find(win.parent); + + } + + if(!API && win.top.opener){ + + API = find(win.top.opener); + + } + + if(API){ + + pipwerks.SCORM.API.isFound = true; + + } else { + + trace("API.get failed: Can't find the API!"); + + } + + return API; + +}; + + +/* ------------------------------------------------------------------------- + pipwerks.SCORM.API.getHandle() + Returns the handle to API object if it was previously set + + Parameters: None. + Returns: Object (the pipwerks.SCORM.API.handle variable). +---------------------------------------------------------------------------- */ + +pipwerks.SCORM.API.getHandle = function() { + + var API = pipwerks.SCORM.API; + + if(!API.handle && !API.isFound){ + + API.handle = API.get(); + + } + + return API.handle; + +}; + + + +// ------------------------------------------------------------------------- // +// --- pipwerks.SCORM.connection functions --------------------------------- // +// ------------------------------------------------------------------------- // + + +/* ------------------------------------------------------------------------- + pipwerks.SCORM.connection.initialize() + Tells the LMS to initiate the communication session. + + Parameters: None + Returns: Boolean +---------------------------------------------------------------------------- */ + +pipwerks.SCORM.connection.initialize = function(){ + + var success = false, + scorm = pipwerks.SCORM, + completionStatus = pipwerks.SCORM.data.completionStatus, + trace = pipwerks.UTILS.trace, + makeBoolean = pipwerks.UTILS.StringToBoolean, + debug = pipwerks.SCORM.debug, + traceMsgPrefix = "SCORM.connection.initialize "; + + trace("connection.initialize called."); + + if(!scorm.connection.isActive){ + + var API = scorm.API.getHandle(), + errorCode = 0; + + if(API){ + + switch(scorm.version){ + case "1.2" : success = makeBoolean(API.LMSInitialize("")); break; + case "2004": success = makeBoolean(API.Initialize("")); break; + } + + if(success){ + + //Double-check that connection is active and working before returning 'true' boolean + errorCode = debug.getCode(); + + if(errorCode !== null && errorCode === 0){ + + scorm.connection.isActive = true; + + if(scorm.handleCompletionStatus){ + + //Automatically set new launches to incomplete + completionStatus = pipwerks.SCORM.status("get"); + + if(completionStatus){ + + switch(completionStatus){ + + //Both SCORM 1.2 and 2004 + case "not attempted": pipwerks.SCORM.status("set", "incomplete"); break; + + //SCORM 2004 only + case "unknown" : pipwerks.SCORM.status("set", "incomplete"); break; + + //Additional options, presented here in case you'd like to use them + //case "completed" : break; + //case "incomplete" : break; + //case "passed" : break; //SCORM 1.2 only + //case "failed" : break; //SCORM 1.2 only + //case "browsed" : break; //SCORM 1.2 only + + } + + } + + } + + } else { + + success = false; + trace(traceMsgPrefix +"failed. \nError code: " +errorCode +" \nError info: " +debug.getInfo(errorCode)); + + } + + } else { + + errorCode = debug.getCode(); + + if(errorCode !== null && errorCode !== 0){ + + trace(traceMsgPrefix +"failed. \nError code: " +errorCode +" \nError info: " +debug.getInfo(errorCode)); + + } else { + + trace(traceMsgPrefix +"failed: No response from server."); + + } + } + + } else { + + trace(traceMsgPrefix +"failed: API is null."); + + } + + } else { + + trace(traceMsgPrefix +"aborted: Connection already active."); + + } + + return success; + +}; + + +/* ------------------------------------------------------------------------- + pipwerks.SCORM.connection.terminate() + Tells the LMS to terminate the communication session + + Parameters: None + Returns: Boolean +---------------------------------------------------------------------------- */ + +pipwerks.SCORM.connection.terminate = function(){ + + var success = false, + scorm = pipwerks.SCORM, + exitStatus = pipwerks.SCORM.data.exitStatus, + completionStatus = pipwerks.SCORM.data.completionStatus, + trace = pipwerks.UTILS.trace, + makeBoolean = pipwerks.UTILS.StringToBoolean, + debug = pipwerks.SCORM.debug, + traceMsgPrefix = "SCORM.connection.terminate "; + + + if(scorm.connection.isActive){ + + var API = scorm.API.getHandle(), + errorCode = 0; + + if(API){ + + if(scorm.handleExitMode && !exitStatus){ + + if(completionStatus !== "completed" && completionStatus !== "passed"){ + + switch(scorm.version){ + case "1.2" : success = scorm.set("cmi.core.exit", "suspend"); break; + case "2004": success = scorm.set("cmi.exit", "suspend"); break; + } + + } else { + + switch(scorm.version){ + case "1.2" : success = scorm.set("cmi.core.exit", "logout"); break; + case "2004": success = scorm.set("cmi.exit", "normal"); break; + } + + } + + } + + switch(scorm.version){ + case "1.2" : success = makeBoolean(API.LMSFinish("")); break; + case "2004": success = makeBoolean(API.Terminate("")); break; + } + + if(success){ + + scorm.connection.isActive = false; + + } else { + + errorCode = debug.getCode(); + trace(traceMsgPrefix +"failed. \nError code: " +errorCode +" \nError info: " +debug.getInfo(errorCode)); + + } + + } else { + + trace(traceMsgPrefix +"failed: API is null."); + + } + + } else { + + trace(traceMsgPrefix +"aborted: Connection already terminated."); + + } + + return success; + +}; + + + +// ------------------------------------------------------------------------- // +// --- pipwerks.SCORM.data functions --------------------------------------- // +// ------------------------------------------------------------------------- // + + +/* ------------------------------------------------------------------------- + pipwerks.SCORM.data.get(parameter) + Requests information from the LMS. + + Parameter: parameter (string, name of the SCORM data model element) + Returns: string (the value of the specified data model element) +---------------------------------------------------------------------------- */ + +pipwerks.SCORM.data.get = function(parameter){ + + var value = null, + scorm = pipwerks.SCORM, + trace = pipwerks.UTILS.trace, + debug = pipwerks.SCORM.debug, + traceMsgPrefix = "SCORM.data.get(" +parameter +") "; + + if(scorm.connection.isActive){ + + var API = scorm.API.getHandle(), + errorCode = 0; + + if(API){ + + switch(scorm.version){ + case "1.2" : value = API.LMSGetValue(parameter); break; + case "2004": value = API.GetValue(parameter); break; + } + + errorCode = debug.getCode(); + + //GetValue returns an empty string on errors + //Double-check errorCode to make sure empty string + //is really an error and not field value + if(value !== "" && errorCode === 0){ + + switch(parameter){ + + case "cmi.core.lesson_status": + case "cmi.completion_status" : scorm.data.completionStatus = value; break; + + case "cmi.core.exit": + case "cmi.exit" : scorm.data.exitStatus = value; break; + + } + + } else { + + trace(traceMsgPrefix +"failed. \nError code: " +errorCode +"\nError info: " +debug.getInfo(errorCode)); + + } + + } else { + + trace(traceMsgPrefix +"failed: API is null."); + + } + + } else { + + trace(traceMsgPrefix +"failed: API connection is inactive."); + + } + + trace(traceMsgPrefix +" value: " +value); + + return String(value); + +}; + + +/* ------------------------------------------------------------------------- + pipwerks.SCORM.data.set() + Tells the LMS to assign the value to the named data model element. + Also stores the SCO's completion status in a variable named + pipwerks.SCORM.data.completionStatus. This variable is checked whenever + pipwerks.SCORM.connection.terminate() is invoked. + + Parameters: parameter (string). The data model element + value (string). The value for the data model element + Returns: Boolean +---------------------------------------------------------------------------- */ + +pipwerks.SCORM.data.set = function(parameter, value){ + + var success = false, + scorm = pipwerks.SCORM, + trace = pipwerks.UTILS.trace, + makeBoolean = pipwerks.UTILS.StringToBoolean, + debug = pipwerks.SCORM.debug, + traceMsgPrefix = "SCORM.data.set(" +parameter +") "; + + + if(scorm.connection.isActive){ + + var API = scorm.API.getHandle(), + errorCode = 0; + + if(API){ + + switch(scorm.version){ + case "1.2" : success = makeBoolean(API.LMSSetValue(parameter, value)); break; + case "2004": success = makeBoolean(API.SetValue(parameter, value)); break; + } + + if(success){ + + if(parameter === "cmi.core.lesson_status" || parameter === "cmi.completion_status"){ + + scorm.data.completionStatus = value; + + } + + } else { + + trace(traceMsgPrefix +"failed. \nError code: " +errorCode +". \nError info: " +debug.getInfo(errorCode)); + + } + + } else { + + trace(traceMsgPrefix +"failed: API is null."); + + } + + } else { + + trace(traceMsgPrefix +"failed: API connection is inactive."); + + } + + return success; + +}; + + +/* ------------------------------------------------------------------------- + pipwerks.SCORM.data.save() + Instructs the LMS to persist all data to this point in the session + + Parameters: None + Returns: Boolean +---------------------------------------------------------------------------- */ + +pipwerks.SCORM.data.save = function(){ + + var success = false, + scorm = pipwerks.SCORM, + trace = pipwerks.UTILS.trace, + makeBoolean = pipwerks.UTILS.StringToBoolean, + traceMsgPrefix = "SCORM.data.save failed"; + + + if(scorm.connection.isActive){ + + var API = scorm.API.getHandle(); + + if(API){ + + switch(scorm.version){ + case "1.2" : success = makeBoolean(API.LMSCommit("")); break; + case "2004": success = makeBoolean(API.Commit("")); break; + } + + } else { + + trace(traceMsgPrefix +": API is null."); + + } + + } else { + + trace(traceMsgPrefix +": API connection is inactive."); + + } + + return success; + +}; + + +pipwerks.SCORM.status = function (action, status){ + + var success = false, + scorm = pipwerks.SCORM, + trace = pipwerks.UTILS.trace, + traceMsgPrefix = "SCORM.getStatus failed", + cmi = ""; + + if(action !== null){ + + switch(scorm.version){ + case "1.2" : cmi = "cmi.core.lesson_status"; break; + case "2004": cmi = "cmi.completion_status"; break; + } + + switch(action){ + + case "get": success = pipwerks.SCORM.data.get(cmi); break; + + case "set": if(status !== null){ + + success = pipwerks.SCORM.data.set(cmi, status); + + } else { + + success = false; + trace(traceMsgPrefix +": status was not specified."); + + } + + break; + + default : success = false; + trace(traceMsgPrefix +": no valid action was specified."); + + } + + } else { + + trace(traceMsgPrefix +": action was not specified."); + + } + + return success; + +}; + + +// ------------------------------------------------------------------------- // +// --- pipwerks.SCORM.debug functions -------------------------------------- // +// ------------------------------------------------------------------------- // + + +/* ------------------------------------------------------------------------- + pipwerks.SCORM.debug.getCode + Requests the error code for the current error state from the LMS + + Parameters: None + Returns: Integer (the last error code). +---------------------------------------------------------------------------- */ + +pipwerks.SCORM.debug.getCode = function(){ + + var API = pipwerks.SCORM.API.getHandle(), + scorm = pipwerks.SCORM, + trace = pipwerks.UTILS.trace, + code = 0; + + if(API){ + + switch(scorm.version){ + case "1.2" : code = parseInt(API.LMSGetLastError(), 10); break; + case "2004": code = parseInt(API.GetLastError(), 10); break; + } + + } else { + + trace("SCORM.debug.getCode failed: API is null."); + + } + + return code; + +}; + + +/* ------------------------------------------------------------------------- + pipwerks.SCORM.debug.getInfo() + "Used by a SCO to request the textual description for the error code + specified by the value of [errorCode]." + + Parameters: errorCode (integer). + Returns: String. +----------------------------------------------------------------------------- */ + +pipwerks.SCORM.debug.getInfo = function(errorCode){ + + var API = pipwerks.SCORM.API.getHandle(), + scorm = pipwerks.SCORM, + trace = pipwerks.UTILS.trace, + result = ""; + + + if(API){ + + switch(scorm.version){ + case "1.2" : result = API.LMSGetErrorString(errorCode.toString()); break; + case "2004": result = API.GetErrorString(errorCode.toString()); break; + } + + } else { + + trace("SCORM.debug.getInfo failed: API is null."); + + } + + return String(result); + +}; + + +/* ------------------------------------------------------------------------- + pipwerks.SCORM.debug.getDiagnosticInfo + "Exists for LMS specific use. It allows the LMS to define additional + diagnostic information through the API Instance." + + Parameters: errorCode (integer). + Returns: String (Additional diagnostic information about the given error code). +---------------------------------------------------------------------------- */ + +pipwerks.SCORM.debug.getDiagnosticInfo = function(errorCode){ + + var API = pipwerks.SCORM.API.getHandle(), + scorm = pipwerks.SCORM, + trace = pipwerks.UTILS.trace, + result = ""; + + if(API){ + + switch(scorm.version){ + case "1.2" : result = API.LMSGetDiagnostic(errorCode); break; + case "2004": result = API.GetDiagnostic(errorCode); break; + } + + } else { + + trace("SCORM.debug.getDiagnosticInfo failed: API is null."); + + } + + return String(result); + +}; + + +// ------------------------------------------------------------------------- // +// --- Shortcuts! ---------------------------------------------------------- // +// ------------------------------------------------------------------------- // + +// Because nobody likes typing verbose code. + +pipwerks.SCORM.init = pipwerks.SCORM.connection.initialize; +pipwerks.SCORM.get = pipwerks.SCORM.data.get; +pipwerks.SCORM.set = pipwerks.SCORM.data.set; +pipwerks.SCORM.save = pipwerks.SCORM.data.save; +pipwerks.SCORM.quit = pipwerks.SCORM.connection.terminate; + + + +// ------------------------------------------------------------------------- // +// --- pipwerks.UTILS functions -------------------------------------------- // +// ------------------------------------------------------------------------- // + + +/* ------------------------------------------------------------------------- + pipwerks.UTILS.StringToBoolean() + Converts 'boolean strings' into actual valid booleans. + + (Most values returned from the API are the strings "true" and "false".) + + Parameters: String + Returns: Boolean +---------------------------------------------------------------------------- */ + +pipwerks.UTILS.StringToBoolean = function(string){ + switch(string.toLowerCase()) { + case "true": case "yes": case "1": return true; + case "false": case "no": case "0": case null: return false; + default: return Boolean(string); + } +}; + + + +/* ------------------------------------------------------------------------- + pipwerks.UTILS.trace() + Displays error messages when in debug mode. + + Parameters: msg (string) + Return: None +---------------------------------------------------------------------------- */ + +pipwerks.UTILS.trace = function(msg){ + + if(pipwerks.debug.isActive){ + + //Firefox users can use the 'Firebug' extension's console. + if(window.console && window.console.firebug){ + console.log(msg); + } else { + //alert(msg); + } + + } +}; \ No newline at end of file diff --git a/js/adapt-contrib-spoor.js b/js/adapt-contrib-spoor.js new file mode 100644 index 00000000..03a31407 --- /dev/null +++ b/js/adapt-contrib-spoor.js @@ -0,0 +1,321 @@ +/* +* adapt-contrib-spoor +* License - http://github.com/adaptlearning/adapt_framework/LICENSE +* Maintainers - Kevin Corry +*/ +define(function(require) { + + var Adapt = require('coreJS/adapt'); + var sAPI = require('extensions/adapt-contrib-spoor/js/SCORM_API_wrapper'); + var sWrapper = require('extensions/adapt-contrib-spoor/js/scormWrapper'); + var sLog = require('extensions/adapt-contrib-spoor/js/logger'); + + var Spoor = Backbone.Model.extend({ + + defaults: { + initialised: false, + SCOFinishCalled: false, + dataIsFromLMS: true, + testingMode: false + }, + + url: "spoor/scormData.json", + + initialize: function() { + //_.bindAll(this); + this.fetch(); + this.SCOStart(); + }, + + init: function() { + if (Adapt.course !== undefined && Adapt.course.get('dataReady')) { + this.onDataReady(); + } else { + Adapt.on('app:dataReady', this.onDataReady, this); + } + //Adapt.blocks.on('change:_isComplete', this.onItemComplete, this); + Adapt.on('itemComplete', this.onItemComplete, this); + }, + + checkCompletionCriteria: function() { + var needModsComplete = this.get('tracking').requireAllModulesCompleted; + var needQuizPassed = this.get('tracking').requireQuizPassed; + + needModsComplete = this.parseBool(needModsComplete); + needQuizPassed = this.parseBool(needQuizPassed); + + var quizPassed = !!Adapt.course.get('quizPassed'); + var modulesCompleted = !!Adapt.course.get('_isComplete'); + var trackingCriteriaMet = false; + + if (needModsComplete) { + if (modulesCompleted) { + if (needQuizPassed) { + if (quizPassed) { + trackingCriteriaMet = true; + } + } else { + trackingCriteriaMet = true; + } + } + } else { + if (needQuizPassed) { + if (quizPassed) { + trackingCriteriaMet = true; + } + } + } + if (trackingCriteriaMet) { + this.setLessonStatus(this.get('reporting').onTrackingCriteriaMet); + } + }, + + convertCompletionStringToArray: function(string) { + var completionArray = string.split(""); + for (var i = 0; i < completionArray.length; i++) { + if (completionArray[i] === "-") { + completionArray[i] = -1; + } else { + completionArray[i] = parseInt(completionArray[i], 10); + } + } + return completionArray; + }, + + convertCompletionArrayToString: function(array) { + var completionString = array.join(""); + return completionString.replace(/-1/g, "-"); + }, + + createCompletionString: function() { + var blockCompletionArray = this.get('blockCompletionArray'); + + _.each(Adapt.blocks.models, function(blockModel) { + if (blockModel.get('_isComplete')) { + blockCompletionArray[blockModel.get('trackingId')] = 1; + } + }, this); + + this.set('blockCompletionArray', blockCompletionArray); + return this.convertCompletionArrayToString(blockCompletionArray); + }, + + createCompletionObject: function() { + var completionObject = {spoor:{}}; + completionObject.spoor.completion = this.createCompletionString(); + completionObject.spoor.quizPassed = Adapt.course.get('quizPassed') || false; + this.set('completionObject', completionObject); + return completionObject; + }, + + getDataIsAlreadyReported: function(completionObject) { + return JSON.stringify(this.get('suspendData')) === JSON.stringify(completionObject); + }, + + markBlockAsComplete: function(parameters) { + var block = parameters.block; + + if (block.get('_isComplete')) { + return; + } + + _.each(block.getChildren(), function(child) { + child.set('_isComplete', true); + }, this); + }, + + onCriterionMet: function(criterion) { + if (!Adapt.course.get(criterion)) { + Adapt.course.set(criterion, true); + } + this.sendCompletionString(); + this.checkCompletionCriteria(); + }, + + onDataReady: function() { + this.setSessionData(); + this.setupBlockCompletionData(); + this.repopulateCompletionData(); + Adapt.trigger("spoorReady"); + }, + + onItemComplete: function(item) { + console.log('onItemComplete'); + if(!this.get('dataIsFromLMS')) { + switch(item.get('_type')) { + case "course": + this.onCriterionMet("complete"); + break; + case "block": + this.set('lastCompletedBlock', item); + this.sendCompletionString(); + break; + } + } + }, + + onQuizComplete: function(isAssessment, quizPassed) { + console.info("Spoor::onQuizComplete: assessment: " + isAssessment + ", quizPassed: " + quizPassed); + if (isAssessment) { + if (quizPassed) { + this.onCriterionMet("quizPassed"); + } else { + var onQuizFailure = this.get('reporting').onQuizFailure; + if (onQuizFailure !== "" && onQuizFailure !== "incomplete") { + this.setLessonStatus(onQuizFailure); + } + } + } + }, + + repopulateCompletionData: function() { + var suspendData = this.get('suspendData'); + + if (suspendData.spoor.completion !== "") { + this.restoreProgress(suspendData); + } + + this.set('dataIsFromLMS', false); + }, + + restoreProgress: function(suspendData) { + if (this.get('scormWrapper').getStatus() === "courseComplete") { + Adapt.course.setOnChildren('_isComplete', true); + } else { + _.each(this.get('blockCompletionArray'), function(blockCompletion, blockTrackingId) { + if (blockCompletion === 1) { + this.markBlockAsComplete({block: Adapt.blocks.findWhere({trackingId: blockTrackingId}), includeChildren: true}); + } + }, this); + } + Adapt.course.set('quizPassed', suspendData.spoor.quizPassed); + this.set("suspendData", suspendData); + this.sendCompletionString(); + }, + + SCOFinish:function() { + if (!this.get('SCOFinishCalled')) { + this.set('SCOFinishCalled', true); + this.get('scormWrapper').finish(); + } + }, + + SCOStart: function() { + this.set('scormWrapper', this.get('testingMode') ? this.get('testingLMS') : ScormWrapper.getInstance()); + var sw = this.get('scormWrapper'); + sw.setVersion("1.2"); + if (sw.initialize()) { + this.set('initialised', true); + var lessonStatus = sw.getStatus().toLowerCase(); + + if (lessonStatus == "not attempted" || lessonStatus == "unknown") { + sw.setIncomplete(); + } + this.init(); + } + }, + + sendCompletionString: function(){ + console.log('sendCompletionString'); + var suspendData = this.createCompletionObject(); + if (!this.getDataIsAlreadyReported(suspendData)) { + if (this.get('lastCompletedBlock') !== undefined) { + console.info("Spoor: block " + this.get('lastCompletedBlock').get('id') + " is complete."); + } + this.set({ + suspendData: suspendData, + blockCompletionArray: this.convertCompletionStringToArray(suspendData.spoor.completion) + }); + this.get('scormWrapper').setSuspendData(JSON.stringify(suspendData)); + } + }, + + setLessonStatus:function(status){ + switch (status){ + case "incomplete": + this.get('scormWrapper').setIncomplete(); + break; + case "completed": + this.get('scormWrapper').setCompleted(); + break; + case "passed": + this.get('scormWrapper').setPassed(); + break; + case "failed": + this.get('scormWrapper').setFailed(); + break; + default: + console.warn("cmi.core.lesson_status of " + status + " is not supported."); + break; + } + }, + + setScore: function(score){ + this.get('scormWrapper').setScore(score,0,100); + }, + + setSessionData: function() { + var suspendData = this.get('scormWrapper').getSuspendData(); + + if (suspendData === "" || suspendData === " " || suspendData === undefined) { + this.set('suspendData', { + spoor: { + completion: "", + quizPassed: false + } + }); + } else { + this.set('suspendData', JSON.parse(suspendData)); + } + + this.set({ + sessionID: this.generateRandomID(8) + }); + }, + + setTestingMode: function(value) { + this.set('testingLMS', value ? new TestingLMS() : null); + this.set('testingMode', value); + }, + + setupBlockCompletionData: function() { + if (Adapt.course.get('latestTrackingId') === undefined) { + var message = "This course is missing a latestTrackingID.\n\nPlease run the grunt process prior to deploying this module on LMS.\n\nScorm tracking will not work correctly until this is done."; + alert(message); + console.error(message); + } + var blockCompletionArray = new Array(Adapt.course.get('latestTrackingId') + 1); + for (var i = 0; i < blockCompletionArray.length; i++) { + blockCompletionArray[i] = -1; + } + + _.each(Adapt.blocks.models, function(model, index) { + var trackingId = model.get('trackingId'); + var recordedCompletion = parseInt(this.get('suspendData').spoor.completion[trackingId], 10) || 0; + if (trackingId === undefined) { + var message = "Block '" + model.get('id') + "' doesn't have a tracking ID assigned.\n\nPlease run the grunt process prior to deploying this module on LMS.\n\nScorm tracking will not work correctly until this is done."; + alert(message); + console.error(message); + } + blockCompletionArray[trackingId] = recordedCompletion; + }, this); + this.set('blockCompletionArray', blockCompletionArray); + }, + + parseBool: function(str) { + if(_.isString(str)) { + if (typeof str === 'string' && str.toLowerCase() == 'true') + return true; + return (parseInt(str) > 0); + } else if (_.isBoolean(str)) return str; + }, + + generateRandomID: function(numberOfCharacters) { + return Math.random().toString(36).slice(-numberOfCharacters); + } + + }); + + return new Spoor(); + +}); diff --git a/js/logger.js b/js/logger.js new file mode 100644 index 00000000..f08c6474 --- /dev/null +++ b/js/logger.js @@ -0,0 +1,67 @@ +Logger = function() { + this.logArr = new Array(); + this.registeredViews = new Array(); +}; + +// static +Logger.instance = null; +Logger.LOG_TYPE_INFO = 0; +Logger.LOG_TYPE_WARN = 1; +Logger.LOG_TYPE_ERROR = 2; +Logger.LOG_TYPE_DEBUG = 3; + +Logger.getInstance = function() { + if (Logger.instance == null) + Logger.instance = new Logger(); + return Logger.instance; +}; + +Logger.prototype.getEntries = function() { + return this.logArr; +}; + +Logger.prototype.getLastEntry = function() { + return this.logArr[this.logArr.length - 1]; +}; + +Logger.prototype.info = function(str) { + this.logArr[this.logArr.length] = {str:str, type:Logger.LOG_TYPE_INFO}; + this.updateViews(); +}; + +Logger.prototype.warn = function(str) { + this.logArr[this.logArr.length] = {str:str, type:Logger.LOG_TYPE_WARN}; + this.updateViews(); +}; + +Logger.prototype.error = function(str) { + this.logArr[this.logArr.length] = {str:str, type:Logger.LOG_TYPE_ERROR}; + this.updateViews(); +}; + +Logger.prototype.debug = function(str) { + this.logArr[this.logArr.length] = {str:str, type:Logger.LOG_TYPE_DEBUG}; + this.updateViews(); +}; + +//register a view +Logger.prototype.registerView = function(_view) { + this.registeredViews[this.registeredViews.length] = _view; +}; + +//unregister a view +Logger.prototype.unregisterView = function(_view) { + for (var i = 0; i < this.registeredViews.length; i++) + if (this.registeredViews[i] == _view) { + this.registeredViews.splice(i, 1); + i--; + } +}; + +// update all views +Logger.prototype.updateViews = function() { + for (var i = 0; i < this.registeredViews.length; i++) { + if (this.registeredViews[i]) + this.registeredViews[i].update(this); + } +}; \ No newline at end of file diff --git a/js/scormData.json b/js/scormData.json new file mode 100644 index 00000000..569f3cfd --- /dev/null +++ b/js/scormData.json @@ -0,0 +1,11 @@ +{ + "tracking": { + "requireAllModulesCompleted": true, + "requireQuizPassed": false + }, + "reporting": { + "comment": "Your options here are 'completed', 'passed', 'failed', and 'incomplete'", + "onTrackingCriteriaMet": "completed", + "onQuizFailure": "incomplete" + } +} \ No newline at end of file diff --git a/js/scormWrapper.js b/js/scormWrapper.js new file mode 100644 index 00000000..e648886c --- /dev/null +++ b/js/scormWrapper.js @@ -0,0 +1,1102 @@ +/* + IMPORTANT: This wrapper uses the Pipwerks SCORM wrapper and should therefore support both SCORM 1.2 and 2004. Ensure any changes support both versions. +*/ + +ScormWrapper = function() +{ + /* configuration */ + this.setCompletedWhenFailed = true;// this only applies to SCORM 2004 + /** + * whether to commit each time there's a change to lesson_status or not + */ + this.commitOnStatusChange = true; + /** + * how frequently (in minutes) to commit automatically. set to 0 to disable. + */ + this.timedCommitFrequency = 10; + /** + * how many times to retry if a commit fails + */ + this.maxCommitRetries = 5; + /** + * time (in milliseconds) to wait between retries + */ + this.commitRetryDelay = 1000; + + /** + * prevents commit from being called if there's already a 'commit retry' pending. + */ + this.commitRetryPending = false; + /** + * how many times we've done a 'commit retry' + */ + this.commitRetries = 0; + /** + * not currently used - but you could include in an error message to show when data was last saved + */ + this.lastCommitSuccessTime = null; + + this.timedCommitIntervalID = null; + this.retryCommitTimeoutID = null; + this.logOutputWin = null; + this.startTime = null; + this.endTime = null; + + this.lmsConnected = false; + this.finishCalled = false; + + this.logger = Logger.getInstance(); + this.scorm = pipwerks.SCORM; + + this.registeredViews = []; + + if (window.__debug) + this.showDebugWindow(); + + /* + stop pipwerks from auto-setting the SCO to incomplete, otherwise we can never let the course know when it is being run for the first time.... + */ + this.scorm.handleCompletionStatus = false; + /** + * and also stop it from setting cmi.core.exit to suspend/logout. there doesn't seem to be any tangible benefit to doing this... + * it can actually cause problems with some LMSes (e.g. setting 'logout' apparently causes Plateau to log out completely!) + * you can always switch it back on for an individual course if you think it's necessary. + */ + this.scorm.handleExitMode = false; +}; + +// static +ScormWrapper.instance = null; + +/******************************* public methods *******************************/ + +// static +ScormWrapper.getInstance = function() +{ + if (ScormWrapper.instance == null) + ScormWrapper.instance = new ScormWrapper(); + return ScormWrapper.instance; +}; + +ScormWrapper.prototype.getVersion = function() +{ + return this.scorm.version; +}; + +ScormWrapper.prototype.setVersion = function(value) +{ + this.scorm.version = value; +}; + +ScormWrapper.prototype.registerView = function(_view) +{ + this.registeredViews[this.registeredViews.length] = _view; +}; + +ScormWrapper.prototype.updateViews = function() +{ + for (var i = 0; i < this.registeredViews.length; i++) { + this.registeredViews[i].update(this); + } +}; + +ScormWrapper.prototype.initialize = function() +{ + this.lmsConnected = this.scorm.init(); + + if (this.lmsConnected) + { + this.startTime = new Date(); + + this.initTimedCommit(); + } + else + { + this.handleError("Course could not connect to the LMS"); + } + + return this.lmsConnected; +}; + +ScormWrapper.prototype.setIncomplete = function() +{ + this.setValue(this.isSCORM2004() ? "cmi.completion_status" : "cmi.core.lesson_status", "incomplete"); + + if(this.commitOnStatusChange) this.commit(); +}; + +ScormWrapper.prototype.setCompleted = function() +{ + this.setValue(this.isSCORM2004() ? "cmi.completion_status" : "cmi.core.lesson_status", "completed"); + + if(this.commitOnStatusChange) this.commit(); +}; + +ScormWrapper.prototype.setPassed = function() +{ + if (this.isSCORM2004()) + { + this.setValue("cmi.completion_status", "completed"); + this.setValue("cmi.success_status", "passed"); + } + else + { + this.setValue("cmi.core.lesson_status", "passed"); + } + + if(this.commitOnStatusChange) this.commit(); +}; + +ScormWrapper.prototype.setFailed = function() +{ + if (this.isSCORM2004()) + { + this.setValue("cmi.success_status", "failed"); + + if(this.setCompletedWhenFailed) + this.setValue("cmi.completion_status", "completed"); + } + else + { + this.setValue("cmi.core.lesson_status", "failed"); + } + + if(this.commitOnStatusChange) this.commit(); +}; + +ScormWrapper.prototype.getStatus = function() +{ + var status = this.getValue(this.isSCORM2004() ? "cmi.completion_status" : "cmi.core.lesson_status"); + + switch(status.toLowerCase())// workaround for some LMSes (e.g. Arena) not adhering to the all-lowercase rule + { + case "passed": + case "completed": + case "incomplete": + case "failed": + case "browsed": + case "not attempted": + case "not_attempted":// mentioned in SCORM 2004 docs but not sure it ever gets used + case "unknown": //the SCORM 2004 version if not attempted + return status; + break; + default: + this.handleError("ScormWrapper::getStatus: invalid lesson status '" + status + "' received from LMS"); + return null; + } +}; + +ScormWrapper.prototype.getScore = function() +{ + return this.getValue(this.isSCORM2004() ? "cmi.score.raw" : "cmi.core.score.raw"); +}; + +ScormWrapper.prototype.setScore = function(_score, _minScore, _maxScore) +{ + if (this.isSCORM2004()) + { + this.setValue("cmi.score.raw", _score) && this.setValue("cmi.score.min", _minScore) && this.setValue("cmi.score.max", _maxScore) && this.setValue("cmi.score.scaled", _score / 100); + } + else + { + this.setValue("cmi.core.score.raw", _score); + + if(this.isSupported("cmi.core.score.min")) this.setValue("cmi.core.score.min", _minScore); + + if(this.isSupported("cmi.core.score.max")) this.setValue("cmi.core.score.max", _maxScore); + } +}; + +ScormWrapper.prototype.getLessonLocation = function() +{ + return this.getValue(this.isSCORM2004() ? "cmi.location" : "cmi.core.lesson_location"); +}; + +ScormWrapper.prototype.setLessonLocation = function(_location) +{ + this.setValue(this.isSCORM2004() ? "cmi.location" : "cmi.core.lesson_location", _location); +}; + +ScormWrapper.prototype.getSuspendData = function() +{ + return this.getValue("cmi.suspend_data"); +}; + +ScormWrapper.prototype.setSuspendData = function(_data) +{ + this.setValue("cmi.suspend_data", _data); +}; + +ScormWrapper.prototype.getStudentName = function() +{ + return this.getValue(this.isSCORM2004() ? "cmi.learner_name" : "cmi.core.student_name"); +}; + +ScormWrapper.prototype.commit = function() +{ + this.logger.debug("ScormWrapper::commit"); + + if (this.lmsConnected) + { + if (this.commitRetryPending) + { + this.logger.debug("ScormWrapper::commit: skipping this commit call as one is already pending.") + } + else + { + if (this.scorm.save()) + { + this.commitRetries = 0; + this.lastCommitSuccessTime = new Date(); + } + else + { + if (this.commitRetries <= this.maxCommitRetries && !this.finishCalled) + { + this.commitRetries++; + this.initRetryCommit(); + } + else + { + var _errorCode = this.scorm.debug.getCode(); + + var _errorMsg = "Course could not commit data to the LMS"; + _errorMsg += "\nError " + _errorCode + ": " + this.scorm.debug.getInfo(_errorCode); + _errorMsg += "\nLMS Error Info: " + this.scorm.debug.getDiagnosticInfo(_errorCode); + + this.handleError(_errorMsg); + } + } + } + } + else + { + this.handleError("Course is not connected to the LMS"); + } +}; + +ScormWrapper.prototype.finish = function() +{ + this.logger.debug("ScormWrapper::finish"); + + if (this.lmsConnected && !this.finishCalled) + { + this.finishCalled = true; + + if(this.timedCommitIntervalID != null) + { + window.clearInterval(this.timedCommitIntervalID); + } + + if(this.commitRetryPending) + { + window.clearTimeout(this.retryCommitTimeoutID); + this.commitRetryPending = false; + } + + if (this.logOutputWin && !this.logOutputWin.closed) + { + this.logOutputWin.close(); + } + + this.endTime = new Date(); + + if (this.isSCORM2004()) + { + this.scorm.set("cmi.session_time", this.convertMilliSecondsToSCORM2004Time(this.endTime.getTime() - this.startTime.getTime())); + this.scorm.set("cmi.exit", "normal"); + } + else + { + this.scorm.set("cmi.core.session_time", this.convertMilliSecondsToSCORMTime(this.endTime.getTime() - this.startTime.getTime())); + this.scorm.set("cmi.core.exit", ""); + } + + this.commit(); + + // api no longer available from this point + this.lmsConnected = false; + + if (!this.scorm.quit()) + { + this.handleError("Course could not finish"); + } + } + else + { + this.handleError("Course is not connected to the LMS"); + } +}; + +ScormWrapper.prototype.recordInteraction = function(strID, strResponse, strCorrect, strLatency, scormInteractionType) +{ + if(this.isSupported("cmi.interactions._count")) + { + if (scormInteractionType == "choice") + { + var responseIdentifiers = new Array(); + var answers = strResponse.split("#"); + + for (var i = 0; i < answers.length; i++) + { + responseIdentifiers.push(new ResponseIdentifier(answers[i], answers[i])); + } + + this.recordMultipleChoiceInteraction(strID, responseIdentifiers, strCorrect, null, null, null, strLatency, null); + } + else if (scormInteractionType == "matching") + { + var matchingResponses = new Array(); + var sourceTargetPairs = strResponse.split("#"); + var sourceTarget = null; + + for (var i = 0; i < sourceTargetPairs.length; i++) + { + sourceTarget = sourceTargetPairs[i].split("."); + matchingResponses.push(new MatchingResponse(sourceTarget[0], sourceTarget[1])); + } + + this.recordMatchingInteraction(strID, matchingResponses, strCorrect, null, null, null, strLatency, null); + } + } + else + { + this.logger.info("ScormWrapper::recordInteraction: cmi.interactions are not supported by this LMS..."); + } +} + +/****************************** private methods ******************************/ +ScormWrapper.prototype.getValue = function(_property) +{ + this.logger.debug("ScormWrapper::getValue: _property=" + _property); + + if(this.finishCalled) + { + this.logger.debug("ScormWrapper::getValue: ignoring request as 'finish' has been called"); + return; + } + + if (this.lmsConnected) + { + var _value = this.scorm.get(_property); + var _errorCode = this.scorm.debug.getCode(); + var _errorMsg = ""; + + if (_errorCode !== 0) + { + if (_errorCode === 403) + { + this.logger.warn("ScormWrapper::getValue: data model element not initialized"); + } + else + { + _errorMsg += "Course could not get " + _property; + _errorMsg += "\nError Info: " + this.scorm.debug.getInfo(_errorCode); + _errorMsg += "\nLMS Error Info: " + this.scorm.debug.getDiagnosticInfo(_errorCode); + + this.handleError(_errorMsg); + } + } + this.logger.debug("ScormWrapper::getValue: returning " + _value); + return _value + ""; + } + else + { + this.handleError("Course is not connected to the LMS"); + } +}; + +ScormWrapper.prototype.setValue = function(_property, _value) +{ + this.logger.debug("ScormWrapper::setValue: _property=" + _property + " _value=" + _value); + + if(this.finishCalled) + { + this.logger.debug("ScormWrapper::setValue: ignoring request as 'finish' has been called"); + return; + } + + if (this.lmsConnected) + { + var _success = this.scorm.set(_property, _value); + var _errorCode = this.scorm.debug.getCode(); + var _errorMsg = ""; + + if (!_success) + { + /* + * Some LMSes have an annoying tendency to return false from a set call even when it actually worked fine... + * So we need to throw an error only if there was a valid error code. Grr. + * http://www.madcrew.se/wordpress/wp-content/uploads/2008/09/stupid_final_01.jpg + */ + if(_errorCode !== 0) + { + _errorMsg += "Course could not set " + _property + " to " + _value; + _errorMsg += "\nError Info: " + this.scorm.debug.getInfo(_errorCode); + _errorMsg += "\nLMS Error Info: " + this.scorm.debug.getDiagnosticInfo(_errorCode); + + this.handleError(_errorMsg); + } + else + { + this.logger.warn("ScormWrapper::setValue: LMS reported that the 'set' call failed but then said there was no error!"); + } + } + + return _success; + } + else + { + this.handleError("Course is not connected to the LMS"); + } +}; + +/** +* used for checking any data field that is not 'LMS Mandatory' to see whether +* the LMS we're running on supports it or not. +* Note that the way this check is being performed means it wouldn't work for any element that is +* 'write only', but so far we've not had a requirement to check for any optional elements that are. +*/ +ScormWrapper.prototype.isSupported = function(_property) +{ + this.logger.debug("ScormWrapper::isSupported: _property=" + _property); + + if(this.finishCalled) + { + this.logger.debug("ScormWrapper::isSupported: ignoring request as 'finish' has been called"); + return; + } + + if (this.lmsConnected) + { + var _value = this.scorm.get(_property); + var _errorCode = this.scorm.debug.getCode(); + + return (_errorCode === 401 ? false : true); + } + else + { + this.handleError("Course is not connected to the LMS"); + return false; + } +}; + +ScormWrapper.prototype.initTimedCommit = function() +{ + this.logger.debug("ScormWrapper::initTimedCommit"); + + if(this.timedCommitFrequency > 0) + { + var delay = this.timedCommitFrequency * (60 * 1000); + this.timedCommitIntervalID = window.setInterval(delegate(this, this.commit), delay); + } +}; + +ScormWrapper.prototype.initRetryCommit = function() +{ + this.logger.debug("ScormWrapper::initRetryCommit"); + + this.commitRetryPending = true;// stop anything else from calling commit until this is done + + this.retryCommitTimeoutID = window.setTimeout(delegate(this, this.doRetryCommit), this.commitRetryDelay); +}; + +ScormWrapper.prototype.doRetryCommit = function() +{ + this.logger.debug("ScormWrapper::doRetryCommit"); + + this.commitRetryPending = false; + + this.commit(); +}; + +ScormWrapper.prototype.handleError = function(_msg) +{ + this.logger.error(_msg); + + if ((!this.logOutputWin || this.logOutputWin.closed) && confirm("An error has occured:\n\n" + _msg + "\n\nPress 'OK' to view debug information to send to technical support.")) + this.showDebugWindow(); +}; + +ScormWrapper.prototype.createValidIdentifier = function(str) +{ + str = this.trim(new String(str)); + + if (_.indexOf(str.toLowerCase(), "urn:") === 0) + { + str = str.substr(4); + } + + // URNs may only contain the following characters: letters, numbers - ( ) + . : = @ ; $ _ ! * ' % + // if anything else is found, replace it with _ + str = str.replace(/[^\w\-\(\)\+\.\:\=\@\;\$\_\!\*\'\%]/g, "_"); + + return str; +}; + +ScormWrapper.prototype.createResponseIdentifier = function(strShort, strLong) +{ + + if (strShort.length != 1 || strShort.search(/\w/) < 0) + { + strShort = ""; + } + else + { + strShort = strShort.toLowerCase(); + } + + strLong = this.createValidIdentifier(strLong); + + return new ResponseIdentifier(strShort, strLong); +}; + +ScormWrapper.prototype.recordInteraction12 = function(strID, strResponse, bCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, scormInteractionType, strAlternateResponse, strAlternateCorrectResponse) +{ + var bResult; + var bTempResult; + var interactionIndex; + var strResult; + + // in SCORM 1.2, add a new interaction rather than updating an old one, because some LMS vendors have misinterpreted the "write only" rule regarding interactions to mean "write once" + interactionIndex = this.getValue("cmi.interactions._count"); + + if (interactionIndex === "") + { + interactionIndex = 0; + } + + if (bCorrect === true || bCorrect == "correct") + { + strResult = "correct"; + } + else if (bCorrect == "false" || bCorrect == "wrong") + { + strResult = "wrong"; + } + else if (bCorrect == "unanticipated") + { + strResult = "unanticipated"; + } + else if (bCorrect == "neutral") + { + strResult = "neutral"; + } + + bResult = this.setValue("cmi.interactions." + interactionIndex + ".id", strID); + bResult = bResult && this.setValue("cmi.interactions." + interactionIndex + ".type", scormInteractionType); + + bTempResult = this.setValue("cmi.interactions." + interactionIndex + ".student_response", strResponse); + + if (bTempResult === false) + { + bTempResult = this.setValue("cmi.interactions." + interactionIndex + ".student_response", strAlternateResponse); + } + + bResult = bResult && bTempResult; + + if (strCorrectResponse !== undefined && strCorrectResponse !== null && strCorrectResponse !== "") + { + bTempResult = this.setValue("cmi.interactions." + interactionIndex + ".correct_responses.0.pattern", strCorrectResponse); + if (bTempResult === false) + { + bTempResult = this.setValue("cmi.interactions." + interactionIndex + ".correct_responses.0.pattern", strAlternateCorrectResponse); + } + + bResult = bResult && bTempResult; + } + + if (strResult !== undefined && strResult !== null && strResult !== "") + { + bResult = bResult && this.setValue("cmi.interactions." + interactionIndex + ".result", strResult); + } + + // ignore the description parameter in SCORM 1.2, there is nothing we can do with it + + if (intWeighting !== undefined && intWeighting !== null && intWeighting !== "") + { + bResult = bResult && this.setValue("cmi.interactions." + interactionIndex + ".weighting", intWeighting); + } + + if (intLatency !== undefined && intLatency !== null && intLatency !== "") + { + bResult = bResult && this.setValue("cmi.interactions." + interactionIndex + ".latency", this.convertMilliSecondsToSCORMTime(intLatency)); + } + + if (strLearningObjectiveID !== undefined && strLearningObjectiveID !== null && strLearningObjectiveID !== "") + { + bResult = bResult && this.setValue("cmi.interactions." + interactionIndex + ".objectives.0.id", strLearningObjectiveID); + } + + bResult = bResult && this.setValue("cmi.interactions." + interactionIndex + ".time", this.convertDateToCMITime(dtmTime)); + + return bResult; +}; + +ScormWrapper.prototype.recordInteraction2004 = function(strID, strResponse, bCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, scormInteractionType) +{ + + var bResult; + var interactionIndex; + var strResult; + + bCorrect = new String(bCorrect); + + interactionIndex = this.getValue("cmi.interactions._count"); + + if (interactionIndex == "") + { + interactionIndex = 0; + } + + if (bCorrect == true || bCorrect == "true" || bCorrect == "correct") + { + strResult = "correct"; + } + else if (bCorrect == "false" || bCorrect == "wrong") + { + strResult = "incorrect"; + } + else if (bCorrect == "unanticipated") + { + strResult = "unanticipated"; + } + else if (bCorrect == "neutral") + { + strResult = "neutral"; + } + else + { + strResult = ""; + } + + strID = this.createValidIdentifier(strID); + + bResult = this.setValue("cmi.interactions." + interactionIndex + ".id", strID); + bResult = bResult && this.setValue("cmi.interactions." + interactionIndex + ".type", scormInteractionType); + bResult = bResult && this.setValue("cmi.interactions." + interactionIndex + ".learner_response", strResponse); + + if (strResult != undefined && strResult != null && strResult != "") + { + bResult = bResult && this.setValue("cmi.interactions." + interactionIndex + ".result", strResult); + } + + if (strCorrectResponse != undefined && strCorrectResponse != null && strCorrectResponse != "") + { + bResult = bResult && this.setValue("cmi.interactions." + interactionIndex + ".correct_responses.0.pattern", strCorrectResponse); + } + + if (strDescription != undefined && strDescription != null && strDescription != "") + { + bResult = bResult && this.setValue("cmi.interactions." + interactionIndex + ".description", strDescription); + } + + // ignore the description parameter in SCORM 1.2, there is nothing we can do with it + + if (intWeighting != undefined && intWeighting != null && intWeighting != "") + { + bResult = bResult && this.setValue("cmi.interactions." + interactionIndex + ".weighting", intWeighting); + } + + if (intLatency != undefined && intLatency != null && intLatency != "") + { + bResult = bResult && this.setValue("cmi.interactions." + interactionIndex + ".latency", this.convertMilliSecondsToSCORM2004Time(intLatency)); + } + + if (strLearningObjectiveID != undefined && strLearningObjectiveID != null && strLearningObjectiveID != "") + { + bResult = bResult && this.setValue("cmi.interactions." + interactionIndex + ".objectives.0.id", strLearningObjectiveID); + } + + bResult = bResult && this.setValue("cmi.interactions." + interactionIndex + ".timestamp", this.convertDateToISO8601Timestamp(dtmTime)); + + return bResult; +}; + +ScormWrapper.prototype.recordMultipleChoiceInteraction = function(strID, response, blnCorrect, correctResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID) +{ + var _responseArray = null; + var _correctResponseArray = null; + + if (response.constructor == String) + { + _responseArray = new Array(this.createResponseIdentifier(response, response)); + } + else if (response.constructor == ResponseIdentifier) + { + _responseArray = new Array(response); + } + else if (response.constructor == Array || response.constructor.toString().search("Array") > 0) + { + _responseArray = response; + } + else if (window.console && response.constructor.toString() == "(Internal Function)" && response.length > 0) + { + _responseArray = response; + } + else + { + this.handleError("ScormWrapper::recordMultipleChoiceInteraction: response is not in the correct format"); + return false; + } + + if (correctResponse != null && correctResponse != undefined && correctResponse != "") + { + if (correctResponse.constructor == String) + { + _correctResponseArray = new Array(this.createResponseIdentifier(correctResponse, correctResponse)); + } + else if (correctResponse.constructor == ResponseIdentifier) + { + _correctResponseArray = new Array(correctResponse); + } + else if (correctResponse.constructor == Array || correctResponse.constructor.toString().search("Array") > 0) + { + _correctResponseArray = correctResponse; + } + else if (window.console && correctResponse.constructor.toString() == "(Internal Function)" && correctResponse.length > 0) + { + _correctResponseArray = correctResponse; + } + else + { + this.handleError("ScormWrapper::recordMultipleChoiceInteraction: correct response is not in the correct format"); + return false; + } + } + else + { + _correctResponseArray = new Array(); + } + + var dtmTime = new Date(); + + var strResponse = ""; + var strResponseLong = ""; + + var strCorrectResponse = ""; + var strCorrectResponseLong = ""; + + for (var i = 0; i < _responseArray.length; i++) + { + if (strResponse.length > 0) {strResponse += this.isSCORM2004() ? "[,]" : ",";} + if (strResponseLong.length > 0) {strResponseLong += ",";} + + strResponse += this.isSCORM2004() ? _responseArray[i].Long : _responseArray[i].Short; + strResponseLong += _responseArray[i].Long; + } + + for (var i = 0; i < _correctResponseArray.length; i++) + { + if (strCorrectResponse.length > 0) {strCorrectResponse += this.isSCORM2004() ? "[,]" : ",";} + if (strCorrectResponseLong.length > 0) {strCorrectResponseLong += ",";} + + strCorrectResponse += this.isSCORM2004() ? _correctResponseArray[i].Long : _correctResponseArray[i].Short; + strCorrectResponseLong += _correctResponseArray[i].Long; + } + + if (this.isSCORM2004()) + return this.recordInteraction2004(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, "choice"); + + return this.recordInteraction12(strID, strResponseLong, blnCorrect, strCorrectResponseLong, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, "choice", strResponse, strCorrectResponse); +}; + +ScormWrapper.prototype.recordMatchingInteraction = function(strID, response, blnCorrect, correctResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID) +{ + var _responseArray = null; + var _correctResponseArray = null; + + if (response.constructor == MatchingResponse) + { + _responseArray = new Array(response); + } + else if (response.constructor == Array || response.constructor.toString().search("Array") > 0) + { + _responseArray = response; + } + else if (window.console && response.constructor.toString() == "(Internal Function)" && response.length > 0) + { + _responseArray = response; + } + else + { + this.handleError("ScormWrapper::recordMatchingInteraction: response is not in the correct format"); + return false; + } + + if (correctResponse != null && correctResponse != undefined) + { + if (correctResponse.constructor == MatchingResponse) + { + _correctResponseArray = new Array(correctResponse); + } + else if (correctResponse.constructor == Array || correctResponse.constructor.toString().search("Array") > 0) + { + _correctResponseArray = correctResponse; + } + else if (window.console && correctResponse.constructor.toString() == "(Internal Function)" && correctResponse.length > 0) + { + _correctResponseArray = correctResponse; + } + else + { + this.handleError("ScormWrapper::recordMatchingInteraction: correct response is not in the correct format"); + return false; + } + } + else + { + _correctResponseArray = new Array(); + } + + var dtmTime = new Date(); + + var strResponse = ""; + var strResponseLong = ""; + + var strCorrectResponse = ""; + var strCorrectResponseLong = ""; + + for (var i = 0; i < _responseArray.length; i++) + { + if (strResponse.length > 0) {strResponse += ",";} + if (strResponseLong.length > 0) {strResponseLong += this.isSCORM2004() ? "[,]" : ",";} + + strResponse += _responseArray[i].Source.Short + "." + _responseArray[i].Target.Short; + strResponseLong += _responseArray[i].Source.Long + (this.isSCORM2004() ? "[.]" : ".") + _responseArray[i].Target.Long; + } + + for (var i = 0; i < _correctResponseArray.length; i++) + { + if (strCorrectResponse.length > 0) {strCorrectResponse += ",";} + if (strCorrectResponseLong.length > 0) {strCorrectResponseLong += this.isSCORM2004() ? "[,]" : ",";} + + strCorrectResponse += _correctResponseArray[i].Source.Short + "." + _correctResponseArray[i].Target.Short; + strCorrectResponseLong += _correctResponseArray[i].Source.Long + (this.isSCORM2004() ? "[.]" : ".") + _correctResponseArray[i].Target.Long; + } + + if (this.isSCORM2004()) + return this.recordInteraction2004(strID, strResponseLong, blnCorrect, strCorrectResponseLong, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, "matching"); + + return this.recordInteraction12(strID, strResponseLong, blnCorrect, strCorrectResponseLong, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, "matching", strResponse, strCorrectResponse); +}; + +ScormWrapper.prototype.showDebugWindow = function() +{ + + if (this.logOutputWin && !this.logOutputWin.closed) { + this.logOutputWin.close(); + } + + this.logOutputWin = window.open("log_output.html", "Log", "width=600,height=300,status=no,scrollbars=yes,resize=yes,menubar=yes,toolbar=yes,location=yes,top=0,left=0"); + + if (this.logOutputWin) + this.logOutputWin.focus(); + + return; +}; + +ScormWrapper.prototype.convertMilliSecondsToSCORMTime = function(value) +{ + var h; + var m; + var s; + var ms; + var cs; + var CMITimeSpan; + + ms = value % 1000; + + s = ((value - ms) / 1000) % 60; + + m = ((value - ms - (s * 1000)) / 60000) % 60; + + h = (value - ms - (s * 1000) - (m * 60000)) / 3600000; + + if (h == 10000) + { + h = 9999; + + m = (value - (h * 3600000)) / 60000; + if (m == 100) + { + m = 99; + } + m = Math.floor(m); + + s = (value - (h * 3600000) - (m * 60000)) / 1000; + if (s == 100) + { + s = 99; + } + s = Math.floor(s); + + ms = (value - (h * 3600000) - (m * 60000) - (s * 1000)); + } + + cs = Math.floor(ms / 10); + + CMITimeSpan = this.zeroPad(h, 4) + ":" + this.zeroPad(m, 2) + ":" + this.zeroPad(s, 2); + CMITimeSpan += "." + cs; + + if (h > 9999) + { + CMITimeSpan = "9999:99:99"; + + CMITimeSpan += ".99"; + } + + return CMITimeSpan; +}; + +ScormWrapper.prototype.convertDateToCMITime = function(_value) +{ + var h; + var m; + var s; + + dtmDate = new Date(_value); + + h = dtmDate.getHours(); + m = dtmDate.getMinutes(); + s = dtmDate.getSeconds(); + + return this.zeroPad(h, 2) + ":" + this.zeroPad(m, 2) + ":" + this.zeroPad(s, 2); +}; + +ScormWrapper.prototype.convertMilliSecondsToSCORM2004Time = function(_value) +{ + var str = ""; + var cs; + var s; + var m; + var h; + var d; + var mo; // assumed to be an "average" month (a leap year every 4 years) = ((365*4) + 1) / 48 = 30.4375 days per month + var y; + + var HUNDREDTHS_PER_SECOND = 100; + var HUNDREDTHS_PER_MINUTE = HUNDREDTHS_PER_SECOND * 60; + var HUNDREDTHS_PER_HOUR = HUNDREDTHS_PER_MINUTE * 60; + var HUNDREDTHS_PER_DAY = HUNDREDTHS_PER_HOUR * 24; + var HUNDREDTHS_PER_MONTH = HUNDREDTHS_PER_DAY * (((365 * 4) + 1) / 48); + var HUNDREDTHS_PER_YEAR = HUNDREDTHS_PER_MONTH * 12; + + cs = Math.floor(_value / 10); + + y = Math.floor(cs / HUNDREDTHS_PER_YEAR); + cs -= (y * HUNDREDTHS_PER_YEAR); + + mo = Math.floor(cs / HUNDREDTHS_PER_MONTH); + cs -= (mo * HUNDREDTHS_PER_MONTH); + + d = Math.floor(cs / HUNDREDTHS_PER_DAY); + cs -= (d * HUNDREDTHS_PER_DAY); + + h = Math.floor(cs / HUNDREDTHS_PER_HOUR); + cs -= (h * HUNDREDTHS_PER_HOUR); + + m = Math.floor(cs / HUNDREDTHS_PER_MINUTE); + cs -= (m * HUNDREDTHS_PER_MINUTE); + + s = Math.floor(cs / HUNDREDTHS_PER_SECOND); + cs -= (s * HUNDREDTHS_PER_SECOND); + + if (y > 0) + str += y + "Y"; + if (mo > 0) + str += mo + "M"; + if (d > 0) + str += d + "D"; + + // check to see if we have any time before adding the "T" + if ((cs + s + m + h) > 0 ) + { + + str += "T"; + + if (h > 0) + str += h + "H"; + + if (m > 0) + str += m + "M"; + + if ((cs + s) > 0) + { + str += s; + + if (cs > 0) + str += "." + cs; + + str += "S"; + } + } + + if (str == "") + str = "0S"; + + str = "P" + str; + + return str; +}; + +ScormWrapper.prototype.convertDateToISO8601Timestamp = function(_value) +{ + var str; + + dtm = new Date(_value); + + var y = dtm.getFullYear(); + var mo = dtm.getMonth() + 1; + var d = dtm.getDate(); + var h = dtm.getHours(); + var m = dtm.getMinutes(); + var s = dtm.getSeconds(); + + mo = this.zeroPad(mo, 2); + d = this.zeroPad(d, 2); + h = this.zeroPad(h, 2); + m = this.zeroPad(m, 2); + s = this.zeroPad(s, 2); + + str = y + "-" + mo + "-" + d + "T" + h + ":" + m + ":" + s; + + return str; +}; + +ScormWrapper.prototype.zeroPad = function(intNum, intNumDigits) +{ + var strTemp; + var intLen; + var i; + + strTemp = new String(intNum); + intLen = strTemp.length; + + if (intLen > intNumDigits) + { + strTemp = strTemp.substr(0, intNumDigits); + } + else + { + for (i = intLen; i < intNumDigits; i++) + strTemp = "0" + strTemp; + } + + return strTemp; +}; + +ScormWrapper.prototype.trim = function(str) +{ + return str.replace(/^\s*|\s*$/g, ""); +}; + +ScormWrapper.prototype.isSCORM2004 = function() +{ + return this.scorm.version == "2004"; +}; + +function delegate(obj, func) +{ + return function() {return func.apply(obj, arguments); }; +} diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 00000000..840324f5 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,29 @@ +module.exports = function(config) { + config.set({ + // The root path location that will be used to resolve all relative paths + // defined in files and exclude + basePath: '', + // List of files or patterns to load in the browser + files: [ + { pattern: 'js/**/*.js', included: false }, + { pattern: 'tests/spec/**/*.js', included: false }, + 'tests/test-main.js' + ], + // List of frameworks you want to use: jasmine, mocha, qunit + frameworks: ['expect', 'mocha', 'requirejs'], + // Enable or disable watching files and executing the tests + // whenever one of these files changes. + autoWatch: false, + // Chrome (comes installed with Karma) + // ChromeCanary (comes installed with Karma) + // PhantomJS (comes installed with Karma) + // Firefox (requires karma-firefox-launcher plugin) + // Opera (requires karma-opera-launcher plugin) + // Internet Explorer (requires karma-ie-launcher plugin) + // Safari (requires karma-safari-launcher plugin) + browsers: ['Chrome', 'PhantomJS', 'Firefox'], + // If singleRun is set to true, Karma will start and capture all + // configured browsers, run tests and then exit with an exit code of 0 or 1. + singleRun: false + }); +}; diff --git a/package.json b/package.json new file mode 100644 index 00000000..e50bfcb1 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "adapt-contrib-spoor", + "version": "0.0.1", + "description": "A basic scorm tracking plugin for Adapt", + "main": "", + "scripts": { + "test": "./node_modules/.bin/karma start --single-run --browsers PhantomJS" + }, + "repository": { + "type": "git", + "url": "git://github.com/adaptlearning/adapt-contrib-spoor.git" + }, + "author": "AdaptLearning", + "license": "GPL v3", + "bugs": { + "url": "https://github.com/adaptlearning/adapt-contrib-spoor/issues" + }, + "devDependencies": { + "karma": "~0.9", + "karma-mocha": "*", + "karma-expect": "*" + } +} diff --git a/tests/SpecRunner.html b/tests/SpecRunner.html new file mode 100644 index 00000000..228dec71 --- /dev/null +++ b/tests/SpecRunner.html @@ -0,0 +1,68 @@ + + + + Adapt Spec Runner + + + + + + + + +
+ + diff --git a/tests/data/articles.json b/tests/data/articles.json new file mode 100644 index 00000000..419ee072 --- /dev/null +++ b/tests/data/articles.json @@ -0,0 +1,26 @@ +[ + { + "_id":"a-05", + "_parentId":"co-05", + "_type":"article", + "title":"Article first title" + }, + { + "_id":"a-10", + "_parentId":"co-05", + "_type":"article", + "title":"Article second title" + }, + { + "_id":"a-15", + "_parentId":"co-15", + "_type":"article", + "title":"Article third title" + }, + { + "_id":"a-20", + "_parentId":"co-20", + "_type":"article", + "title":"Article fourth title" + } +] \ No newline at end of file diff --git a/tests/data/blocks.json b/tests/data/blocks.json new file mode 100644 index 00000000..dbe38744 --- /dev/null +++ b/tests/data/blocks.json @@ -0,0 +1,32 @@ +[ + { + "_id":"b-05", + "_parentId":"a-05", + "_type":"block", + "title":"Title of first block" + }, + { + "_id":"b-10", + "_parentId":"a-05", + "_type":"block", + "title":"Title of second block" + }, + { + "_id":"b-15", + "_parentId":"a-10", + "_type":"block", + "title":"Title of third block" + }, + { + "_id":"b-20", + "_parentId":"a-15", + "_type":"block", + "title":"Title of fourth block" + }, + { + "_id":"b-25", + "_parentId":"a-20", + "_type":"block", + "title":"Title of fifth block" + } +] \ No newline at end of file diff --git a/tests/data/components.json b/tests/data/components.json new file mode 100644 index 00000000..d4ee8501 --- /dev/null +++ b/tests/data/components.json @@ -0,0 +1,47 @@ +[ + { + "_id":"c-05", + "_parentId":"b-05", + "_type":"component", + "_component":"text", + "_classes":"", + "title":"Title of our very first component", + "body":"Whoo - if we get this rendering we've made the big time" + }, + { + "_id":"c-10", + "_parentId":"b-05", + "_type":"component", + "_component":"text", + "_classes":"", + "title":"Title of our very second component", + "body":"Whoo - if we get this rendering we've made the big time" + }, + { + "_id":"c-15", + "_parentId":"b-10", + "_type":"component", + "_component":"text", + "_classes":"", + "title":"Title of our very third component", + "body":"Whoo - if we get this rendering we've made the big time" + }, + { + "_id":"c-20", + "_parentId":"b-10", + "_type":"component", + "_component":"text", + "_classes":"", + "title":"Title of our very fourth component", + "body":"Whoo - if we get this rendering we've made the big time" + }, + { + "_id":"c-25", + "_parentId":"b-15", + "_type":"component", + "_component":"text", + "_classes":"", + "title":"Title of our very fifth component", + "body":"Whoo - if we get this rendering we've made the big time" + } +] \ No newline at end of file diff --git a/tests/data/contentObjects.json b/tests/data/contentObjects.json new file mode 100644 index 00000000..b76aac93 --- /dev/null +++ b/tests/data/contentObjects.json @@ -0,0 +1,26 @@ +[ + { + "_id":"co-05", + "_parentId":"course", + "_type":"page", + "title":"Title of first page model" + }, + { + "_id":"co-10", + "_parentId":"course", + "_type":"menu", + "title":"Title of second page model" + }, + { + "_id":"co-15", + "_parentId":"co-10", + "_type":"page", + "title":"Title of third page model" + }, + { + "_id":"co-20", + "_parentId":"course", + "_type":"page", + "title":"Title of fourth page model" + } +] \ No newline at end of file diff --git a/tests/data/course.json b/tests/data/course.json new file mode 100644 index 00000000..be6a79f7 --- /dev/null +++ b/tests/data/course.json @@ -0,0 +1,6 @@ +{ + "_id":"course", + "_type":"course", + "title":"Adapt", + "body":"This is the first version of the open source re-architecture" +} \ No newline at end of file diff --git a/tests/libraries/expect.js b/tests/libraries/expect.js new file mode 100644 index 00000000..3b9e3ffe --- /dev/null +++ b/tests/libraries/expect.js @@ -0,0 +1,1284 @@ +(function (global, module) { + + var exports = module.exports; + + /** + * Exports. + */ + + module.exports = expect; + expect.Assertion = Assertion; + + /** + * Exports version. + */ + + expect.version = '0.1.2'; + + /** + * Possible assertion flags. + */ + + var flags = { + not: ['to', 'be', 'have', 'include', 'only'] + , to: ['be', 'have', 'include', 'only', 'not'] + , only: ['have'] + , have: ['own'] + , be: ['an'] + }; + + function expect (obj) { + return new Assertion(obj); + } + + /** + * Constructor + * + * @api private + */ + + function Assertion (obj, flag, parent) { + this.obj = obj; + this.flags = {}; + + if (undefined != parent) { + this.flags[flag] = true; + + for (var i in parent.flags) { + if (parent.flags.hasOwnProperty(i)) { + this.flags[i] = true; + } + } + } + + var $flags = flag ? flags[flag] : keys(flags) + , self = this; + + if ($flags) { + for (var i = 0, l = $flags.length; i < l; i++) { + // avoid recursion + if (this.flags[$flags[i]]) continue; + + var name = $flags[i] + , assertion = new Assertion(this.obj, name, this) + + if ('function' == typeof Assertion.prototype[name]) { + // clone the function, make sure we dont touch the prot reference + var old = this[name]; + this[name] = function () { + return old.apply(self, arguments); + }; + + for (var fn in Assertion.prototype) { + if (Assertion.prototype.hasOwnProperty(fn) && fn != name) { + this[name][fn] = bind(assertion[fn], assertion); + } + } + } else { + this[name] = assertion; + } + } + } + } + + /** + * Performs an assertion + * + * @api private + */ + + Assertion.prototype.assert = function (truth, msg, error, expected) { + var msg = this.flags.not ? error : msg + , ok = this.flags.not ? !truth : truth + , err; + + if (!ok) { + err = new Error(msg.call(this)); + if (arguments.length > 3) { + err.actual = this.obj; + err.expected = expected; + err.showDiff = true; + } + throw err; + } + + this.and = new Assertion(this.obj); + }; + + /** + * Check if the value is truthy + * + * @api public + */ + + Assertion.prototype.ok = function () { + this.assert( + !!this.obj + , function(){ return 'expected ' + i(this.obj) + ' to be truthy' } + , function(){ return 'expected ' + i(this.obj) + ' to be falsy' }); + }; + + /** + * Creates an anonymous function which calls fn with arguments. + * + * @api public + */ + + Assertion.prototype.withArgs = function() { + expect(this.obj).to.be.a('function'); + var fn = this.obj; + var args = Array.prototype.slice.call(arguments); + return expect(function() { fn.apply(null, args); }); + }; + + /** + * Assert that the function throws. + * + * @param {Function|RegExp} callback, or regexp to match error string against + * @api public + */ + + Assertion.prototype.throwError = + Assertion.prototype.throwException = function (fn) { + expect(this.obj).to.be.a('function'); + + var thrown = false + , not = this.flags.not; + + try { + this.obj(); + } catch (e) { + if (isRegExp(fn)) { + var subject = 'string' == typeof e ? e : e.message; + if (not) { + expect(subject).to.not.match(fn); + } else { + expect(subject).to.match(fn); + } + } else if ('function' == typeof fn) { + fn(e); + } + thrown = true; + } + + if (isRegExp(fn) && not) { + // in the presence of a matcher, ensure the `not` only applies to + // the matching. + this.flags.not = false; + } + + var name = this.obj.name || 'fn'; + this.assert( + thrown + , function(){ return 'expected ' + name + ' to throw an exception' } + , function(){ return 'expected ' + name + ' not to throw an exception' }); + }; + + /** + * Checks if the array is empty. + * + * @api public + */ + + Assertion.prototype.empty = function () { + var expectation; + + if ('object' == typeof this.obj && null !== this.obj && !isArray(this.obj)) { + if ('number' == typeof this.obj.length) { + expectation = !this.obj.length; + } else { + expectation = !keys(this.obj).length; + } + } else { + if ('string' != typeof this.obj) { + expect(this.obj).to.be.an('object'); + } + + expect(this.obj).to.have.property('length'); + expectation = !this.obj.length; + } + + this.assert( + expectation + , function(){ return 'expected ' + i(this.obj) + ' to be empty' } + , function(){ return 'expected ' + i(this.obj) + ' to not be empty' }); + return this; + }; + + /** + * Checks if the obj exactly equals another. + * + * @api public + */ + + Assertion.prototype.be = + Assertion.prototype.equal = function (obj) { + this.assert( + obj === this.obj + , function(){ return 'expected ' + i(this.obj) + ' to equal ' + i(obj) } + , function(){ return 'expected ' + i(this.obj) + ' to not equal ' + i(obj) }); + return this; + }; + + /** + * Checks if the obj sortof equals another. + * + * @api public + */ + + Assertion.prototype.eql = function (obj) { + this.assert( + expect.eql(this.obj, obj) + , function(){ return 'expected ' + i(this.obj) + ' to sort of equal ' + i(obj) } + , function(){ return 'expected ' + i(this.obj) + ' to sort of not equal ' + i(obj) } + , obj); + return this; + }; + + /** + * Assert within start to finish (inclusive). + * + * @param {Number} start + * @param {Number} finish + * @api public + */ + + Assertion.prototype.within = function (start, finish) { + var range = start + '..' + finish; + this.assert( + this.obj >= start && this.obj <= finish + , function(){ return 'expected ' + i(this.obj) + ' to be within ' + range } + , function(){ return 'expected ' + i(this.obj) + ' to not be within ' + range }); + return this; + }; + + /** + * Assert typeof / instance of + * + * @api public + */ + + Assertion.prototype.a = + Assertion.prototype.an = function (type) { + if ('string' == typeof type) { + // proper english in error msg + var n = /^[aeiou]/.test(type) ? 'n' : ''; + + // typeof with support for 'array' + this.assert( + 'array' == type ? isArray(this.obj) : + 'regexp' == type ? isRegExp(this.obj) : + 'object' == type + ? 'object' == typeof this.obj && null !== this.obj + : type == typeof this.obj + , function(){ return 'expected ' + i(this.obj) + ' to be a' + n + ' ' + type } + , function(){ return 'expected ' + i(this.obj) + ' not to be a' + n + ' ' + type }); + } else { + // instanceof + var name = type.name || 'supplied constructor'; + this.assert( + this.obj instanceof type + , function(){ return 'expected ' + i(this.obj) + ' to be an instance of ' + name } + , function(){ return 'expected ' + i(this.obj) + ' not to be an instance of ' + name }); + } + + return this; + }; + + /** + * Assert numeric value above _n_. + * + * @param {Number} n + * @api public + */ + + Assertion.prototype.greaterThan = + Assertion.prototype.above = function (n) { + this.assert( + this.obj > n + , function(){ return 'expected ' + i(this.obj) + ' to be above ' + n } + , function(){ return 'expected ' + i(this.obj) + ' to be below ' + n }); + return this; + }; + + /** + * Assert numeric value below _n_. + * + * @param {Number} n + * @api public + */ + + Assertion.prototype.lessThan = + Assertion.prototype.below = function (n) { + this.assert( + this.obj < n + , function(){ return 'expected ' + i(this.obj) + ' to be below ' + n } + , function(){ return 'expected ' + i(this.obj) + ' to be above ' + n }); + return this; + }; + + /** + * Assert string value matches _regexp_. + * + * @param {RegExp} regexp + * @api public + */ + + Assertion.prototype.match = function (regexp) { + this.assert( + regexp.exec(this.obj) + , function(){ return 'expected ' + i(this.obj) + ' to match ' + regexp } + , function(){ return 'expected ' + i(this.obj) + ' not to match ' + regexp }); + return this; + }; + + /** + * Assert property "length" exists and has value of _n_. + * + * @param {Number} n + * @api public + */ + + Assertion.prototype.length = function (n) { + expect(this.obj).to.have.property('length'); + var len = this.obj.length; + this.assert( + n == len + , function(){ return 'expected ' + i(this.obj) + ' to have a length of ' + n + ' but got ' + len } + , function(){ return 'expected ' + i(this.obj) + ' to not have a length of ' + len }); + return this; + }; + + /** + * Assert property _name_ exists, with optional _val_. + * + * @param {String} name + * @param {Mixed} val + * @api public + */ + + Assertion.prototype.property = function (name, val) { + if (this.flags.own) { + this.assert( + Object.prototype.hasOwnProperty.call(this.obj, name) + , function(){ return 'expected ' + i(this.obj) + ' to have own property ' + i(name) } + , function(){ return 'expected ' + i(this.obj) + ' to not have own property ' + i(name) }); + return this; + } + + if (this.flags.not && undefined !== val) { + if (undefined === this.obj[name]) { + throw new Error(i(this.obj) + ' has no property ' + i(name)); + } + } else { + var hasProp; + try { + hasProp = name in this.obj + } catch (e) { + hasProp = undefined !== this.obj[name] + } + + this.assert( + hasProp + , function(){ return 'expected ' + i(this.obj) + ' to have a property ' + i(name) } + , function(){ return 'expected ' + i(this.obj) + ' to not have a property ' + i(name) }); + } + + if (undefined !== val) { + this.assert( + val === this.obj[name] + , function(){ return 'expected ' + i(this.obj) + ' to have a property ' + i(name) + + ' of ' + i(val) + ', but got ' + i(this.obj[name]) } + , function(){ return 'expected ' + i(this.obj) + ' to not have a property ' + i(name) + + ' of ' + i(val) }); + } + + this.obj = this.obj[name]; + return this; + }; + + /** + * Assert that the array contains _obj_ or string contains _obj_. + * + * @param {Mixed} obj|string + * @api public + */ + + Assertion.prototype.string = + Assertion.prototype.contain = function (obj) { + if ('string' == typeof this.obj) { + this.assert( + ~this.obj.indexOf(obj) + , function(){ return 'expected ' + i(this.obj) + ' to contain ' + i(obj) } + , function(){ return 'expected ' + i(this.obj) + ' to not contain ' + i(obj) }); + } else { + this.assert( + ~indexOf(this.obj, obj) + , function(){ return 'expected ' + i(this.obj) + ' to contain ' + i(obj) } + , function(){ return 'expected ' + i(this.obj) + ' to not contain ' + i(obj) }); + } + return this; + }; + + /** + * Assert exact keys or inclusion of keys by using + * the `.own` modifier. + * + * @param {Array|String ...} keys + * @api public + */ + + Assertion.prototype.key = + Assertion.prototype.keys = function ($keys) { + var str + , ok = true; + + $keys = isArray($keys) + ? $keys + : Array.prototype.slice.call(arguments); + + if (!$keys.length) throw new Error('keys required'); + + var actual = keys(this.obj) + , len = $keys.length; + + // Inclusion + ok = every($keys, function (key) { + return ~indexOf(actual, key); + }); + + // Strict + if (!this.flags.not && this.flags.only) { + ok = ok && $keys.length == actual.length; + } + + // Key string + if (len > 1) { + $keys = map($keys, function (key) { + return i(key); + }); + var last = $keys.pop(); + str = $keys.join(', ') + ', and ' + last; + } else { + str = i($keys[0]); + } + + // Form + str = (len > 1 ? 'keys ' : 'key ') + str; + + // Have / include + str = (!this.flags.only ? 'include ' : 'only have ') + str; + + // Assertion + this.assert( + ok + , function(){ return 'expected ' + i(this.obj) + ' to ' + str } + , function(){ return 'expected ' + i(this.obj) + ' to not ' + str }); + + return this; + }; + + /** + * Assert a failure. + * + * @param {String ...} custom message + * @api public + */ + Assertion.prototype.fail = function (msg) { + var error = function() { return msg || "explicit failure"; } + this.assert(false, error, error); + return this; + }; + + /** + * Function bind implementation. + */ + + function bind (fn, scope) { + return function () { + return fn.apply(scope, arguments); + } + } + + /** + * Array every compatibility + * + * @see bit.ly/5Fq1N2 + * @api public + */ + + function every (arr, fn, thisObj) { + var scope = thisObj || global; + for (var i = 0, j = arr.length; i < j; ++i) { + if (!fn.call(scope, arr[i], i, arr)) { + return false; + } + } + return true; + } + + /** + * Array indexOf compatibility. + * + * @see bit.ly/a5Dxa2 + * @api public + */ + + function indexOf (arr, o, i) { + if (Array.prototype.indexOf) { + return Array.prototype.indexOf.call(arr, o, i); + } + + if (arr.length === undefined) { + return -1; + } + + for (var j = arr.length, i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0 + ; i < j && arr[i] !== o; i++); + + return j <= i ? -1 : i; + } + + // https://gist.github.com/1044128/ + var getOuterHTML = function(element) { + if ('outerHTML' in element) return element.outerHTML; + var ns = "http://www.w3.org/1999/xhtml"; + var container = document.createElementNS(ns, '_'); + var xmlSerializer = new XMLSerializer(); + var html; + if (document.xmlVersion) { + return xmlSerializer.serializeToString(element); + } else { + container.appendChild(element.cloneNode(false)); + html = container.innerHTML.replace('><', '>' + element.innerHTML + '<'); + container.innerHTML = ''; + return html; + } + }; + + // Returns true if object is a DOM element. + var isDOMElement = function (object) { + if (typeof HTMLElement === 'object') { + return object instanceof HTMLElement; + } else { + return object && + typeof object === 'object' && + object.nodeType === 1 && + typeof object.nodeName === 'string'; + } + }; + + /** + * Inspects an object. + * + * @see taken from node.js `util` module (copyright Joyent, MIT license) + * @api private + */ + + function i (obj, showHidden, depth) { + var seen = []; + + function stylize (str) { + return str; + } + + function format (value, recurseTimes) { + // Provide a hook for user-specified inspect functions. + // Check that value is an object with an inspect function on it + if (value && typeof value.inspect === 'function' && + // Filter out the util module, it's inspect function is special + value !== exports && + // Also filter out any prototype objects using the circular check. + !(value.constructor && value.constructor.prototype === value)) { + return value.inspect(recurseTimes); + } + + // Primitive types cannot have properties + switch (typeof value) { + case 'undefined': + return stylize('undefined', 'undefined'); + + case 'string': + var simple = '\'' + json.stringify(value).replace(/^"|"$/g, '') + .replace(/'/g, "\\'") + .replace(/\\"/g, '"') + '\''; + return stylize(simple, 'string'); + + case 'number': + return stylize('' + value, 'number'); + + case 'boolean': + return stylize('' + value, 'boolean'); + } + // For some reason typeof null is "object", so special case here. + if (value === null) { + return stylize('null', 'null'); + } + + if (isDOMElement(value)) { + return getOuterHTML(value); + } + + // Look up the keys of the object. + var visible_keys = keys(value); + var $keys = showHidden ? Object.getOwnPropertyNames(value) : visible_keys; + + // Functions without properties can be shortcutted. + if (typeof value === 'function' && $keys.length === 0) { + if (isRegExp(value)) { + return stylize('' + value, 'regexp'); + } else { + var name = value.name ? ': ' + value.name : ''; + return stylize('[Function' + name + ']', 'special'); + } + } + + // Dates without properties can be shortcutted + if (isDate(value) && $keys.length === 0) { + return stylize(value.toUTCString(), 'date'); + } + + // Error objects can be shortcutted + if (value instanceof Error) { + return stylize("["+value.toString()+"]", 'Error'); + } + + var base, type, braces; + // Determine the object type + if (isArray(value)) { + type = 'Array'; + braces = ['[', ']']; + } else { + type = 'Object'; + braces = ['{', '}']; + } + + // Make functions say that they are functions + if (typeof value === 'function') { + var n = value.name ? ': ' + value.name : ''; + base = (isRegExp(value)) ? ' ' + value : ' [Function' + n + ']'; + } else { + base = ''; + } + + // Make dates with properties first say the date + if (isDate(value)) { + base = ' ' + value.toUTCString(); + } + + if ($keys.length === 0) { + return braces[0] + base + braces[1]; + } + + if (recurseTimes < 0) { + if (isRegExp(value)) { + return stylize('' + value, 'regexp'); + } else { + return stylize('[Object]', 'special'); + } + } + + seen.push(value); + + var output = map($keys, function (key) { + var name, str; + if (value.__lookupGetter__) { + if (value.__lookupGetter__(key)) { + if (value.__lookupSetter__(key)) { + str = stylize('[Getter/Setter]', 'special'); + } else { + str = stylize('[Getter]', 'special'); + } + } else { + if (value.__lookupSetter__(key)) { + str = stylize('[Setter]', 'special'); + } + } + } + if (indexOf(visible_keys, key) < 0) { + name = '[' + key + ']'; + } + if (!str) { + if (indexOf(seen, value[key]) < 0) { + if (recurseTimes === null) { + str = format(value[key]); + } else { + str = format(value[key], recurseTimes - 1); + } + if (str.indexOf('\n') > -1) { + if (isArray(value)) { + str = map(str.split('\n'), function (line) { + return ' ' + line; + }).join('\n').substr(2); + } else { + str = '\n' + map(str.split('\n'), function (line) { + return ' ' + line; + }).join('\n'); + } + } + } else { + str = stylize('[Circular]', 'special'); + } + } + if (typeof name === 'undefined') { + if (type === 'Array' && key.match(/^\d+$/)) { + return str; + } + name = json.stringify('' + key); + if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { + name = name.substr(1, name.length - 2); + name = stylize(name, 'name'); + } else { + name = name.replace(/'/g, "\\'") + .replace(/\\"/g, '"') + .replace(/(^"|"$)/g, "'"); + name = stylize(name, 'string'); + } + } + + return name + ': ' + str; + }); + + seen.pop(); + + var numLinesEst = 0; + var length = reduce(output, function (prev, cur) { + numLinesEst++; + if (indexOf(cur, '\n') >= 0) numLinesEst++; + return prev + cur.length + 1; + }, 0); + + if (length > 50) { + output = braces[0] + + (base === '' ? '' : base + '\n ') + + ' ' + + output.join(',\n ') + + ' ' + + braces[1]; + + } else { + output = braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; + } + + return output; + } + return format(obj, (typeof depth === 'undefined' ? 2 : depth)); + } + + expect.stringify = i; + + function isArray (ar) { + return Object.prototype.toString.call(ar) === '[object Array]'; + } + + function isRegExp(re) { + var s; + try { + s = '' + re; + } catch (e) { + return false; + } + + return re instanceof RegExp || // easy case + // duck-type for context-switching evalcx case + typeof(re) === 'function' && + re.constructor.name === 'RegExp' && + re.compile && + re.test && + re.exec && + s.match(/^\/.*\/[gim]{0,3}$/); + } + + function isDate(d) { + return d instanceof Date; + } + + function keys (obj) { + if (Object.keys) { + return Object.keys(obj); + } + + var keys = []; + + for (var i in obj) { + if (Object.prototype.hasOwnProperty.call(obj, i)) { + keys.push(i); + } + } + + return keys; + } + + function map (arr, mapper, that) { + if (Array.prototype.map) { + return Array.prototype.map.call(arr, mapper, that); + } + + var other= new Array(arr.length); + + for (var i= 0, n = arr.length; i= 2) { + var rv = arguments[1]; + } else { + do { + if (i in this) { + rv = this[i++]; + break; + } + + // if array contains no values, no initial value to return + if (++i >= len) + throw new TypeError(); + } while (true); + } + + for (; i < len; i++) { + if (i in this) + rv = fun.call(null, rv, this[i], i, this); + } + + return rv; + } + + /** + * Asserts deep equality + * + * @see taken from node.js `assert` module (copyright Joyent, MIT license) + * @api private + */ + + expect.eql = function eql(actual, expected) { + // 7.1. All identical values are equivalent, as determined by ===. + if (actual === expected) { + return true; + } else if ('undefined' != typeof Buffer + && Buffer.isBuffer(actual) && Buffer.isBuffer(expected)) { + if (actual.length != expected.length) return false; + + for (var i = 0; i < actual.length; i++) { + if (actual[i] !== expected[i]) return false; + } + + return true; + + // 7.2. If the expected value is a Date object, the actual value is + // equivalent if it is also a Date object that refers to the same time. + } else if (actual instanceof Date && expected instanceof Date) { + return actual.getTime() === expected.getTime(); + + // 7.3. Other pairs that do not both pass typeof value == "object", + // equivalence is determined by ==. + } else if (typeof actual != 'object' && typeof expected != 'object') { + return actual == expected; + // If both are regular expression use the special `regExpEquiv` method + // to determine equivalence. + } else if (isRegExp(actual) && isRegExp(expected)) { + return regExpEquiv(actual, expected); + // 7.4. For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical "prototype" property. Note: this + // accounts for both named and indexed properties on Arrays. + } else { + return objEquiv(actual, expected); + } + }; + + function isUndefinedOrNull (value) { + return value === null || value === undefined; + } + + function isArguments (object) { + return Object.prototype.toString.call(object) == '[object Arguments]'; + } + + function regExpEquiv (a, b) { + return a.source === b.source && a.global === b.global && + a.ignoreCase === b.ignoreCase && a.multiline === b.multiline; + } + + function objEquiv (a, b) { + if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) + return false; + // an identical "prototype" property. + if (a.prototype !== b.prototype) return false; + //~~~I've managed to break Object.keys through screwy arguments passing. + // Converting to array solves the problem. + if (isArguments(a)) { + if (!isArguments(b)) { + return false; + } + a = pSlice.call(a); + b = pSlice.call(b); + return expect.eql(a, b); + } + try{ + var ka = keys(a), + kb = keys(b), + key, i; + } catch (e) {//happens when one is a string literal and the other isn't + return false; + } + // having the same number of owned properties (keys incorporates hasOwnProperty) + if (ka.length != kb.length) + return false; + //the same set of keys (although not necessarily the same order), + ka.sort(); + kb.sort(); + //~~~cheap key test + for (i = ka.length - 1; i >= 0; i--) { + if (ka[i] != kb[i]) + return false; + } + //equivalent values for every corresponding key, and + //~~~possibly expensive deep test + for (i = ka.length - 1; i >= 0; i--) { + key = ka[i]; + if (!expect.eql(a[key], b[key])) + return false; + } + return true; + } + + var json = (function () { + "use strict"; + + if ('object' == typeof JSON && JSON.parse && JSON.stringify) { + return { + parse: nativeJSON.parse + , stringify: nativeJSON.stringify + } + } + + var JSON = {}; + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + function date(d, key) { + return isFinite(d.valueOf()) ? + d.getUTCFullYear() + '-' + + f(d.getUTCMonth() + 1) + '-' + + f(d.getUTCDate()) + 'T' + + f(d.getUTCHours()) + ':' + + f(d.getUTCMinutes()) + ':' + + f(d.getUTCSeconds()) + 'Z' : null; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + + // If the string contains no control characters, no quote characters, and no + // backslash characters, then we can safely slap some quotes around it. + // Otherwise we must also replace the offending characters with safe escape + // sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + string + '"'; + } + + + function str(key, holder) { + + // Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + + // If the value has a toJSON method, call it to obtain a replacement value. + + if (value instanceof Date) { + value = date(key); + } + + // If we were called with a replacer function, then call the replacer to + // obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + + // What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + + // JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + + // If the value is a boolean or null, convert it to a string. Note: + // typeof null does not produce 'null'. The case is included here in + // the remote chance that this gets fixed someday. + + return String(value); + + // If the type is 'object', we might be dealing with an object or an array or + // null. + + case 'object': + + // Due to a specification blunder in ECMAScript, typeof null is 'object', + // so watch out for that case. + + if (!value) { + return 'null'; + } + + // Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + + // Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + + // The value is an array. Stringify every element. Use null as a placeholder + // for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + + // Join all of the elements together, separated with commas, and wrap them in + // brackets. + + v = partial.length === 0 ? '[]' : gap ? + '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + + // If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + + // Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + + // Join all of the member texts together, separated with commas, + // and wrap them in braces. + + v = partial.length === 0 ? '{}' : gap ? + '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : + '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + + // If the JSON object does not yet have a stringify method, give it one. + + JSON.stringify = function (value, replacer, space) { + + // The stringify method takes a value and an optional replacer, and an optional + // space parameter, and returns a JSON text. The replacer can be a function + // that can replace values, or an array of strings that will select the keys. + // A default replacer method can be provided. Use of the space parameter can + // produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + + // If the space parameter is a number, make an indent string containing that + // many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + + // If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + + // If there is a replacer, it must be a function or an array. + // Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + + // Make a fake root object containing our value under the key of ''. + // Return the result of stringifying the value. + + return str('', {'': value}); + }; + + // If the JSON object does not yet have a parse method, give it one. + + JSON.parse = function (text, reviver) { + // The parse method takes a text and an optional reviver function, and returns + // a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + + // The walk method is used to recursively walk the resulting structure so + // that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + + // Parsing happens in four stages. In the first stage, we replace certain + // Unicode characters with escape sequences. JavaScript handles many characters + // incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + + // In the second stage, we run the text against regular expressions that look + // for non-JSON patterns. We are especially concerned with '()' and 'new' + // because they can cause invocation, and '=' because it can cause mutation. + // But just to be safe, we want to reject all unexpected forms. + + // We split the second stage into 4 regexp operations in order to work around + // crippling inefficiencies in IE's and Safari's regexp engines. First we + // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we + // replace all simple value tokens with ']' characters. Third, we delete all + // open brackets that follow a colon or comma or that begin the text. Finally, + // we look to see that the remaining characters are only whitespace or ']' or + // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/ + .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + + // In the third stage we use the eval function to compile the text into a + // JavaScript structure. The '{' operator is subject to a syntactic ambiguity + // in JavaScript: it can begin a block or an object literal. We wrap the text + // in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + + // In the optional fourth stage, we recursively walk the new structure, passing + // each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + + // If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + + return JSON; + })(); + + if ('undefined' != typeof window) { + window.expect = module.exports; + } + +})( + this + , 'undefined' != typeof module ? module : {exports: {}} +); \ No newline at end of file diff --git a/tests/libraries/mocha.css b/tests/libraries/mocha.css new file mode 100644 index 00000000..008c96b3 --- /dev/null +++ b/tests/libraries/mocha.css @@ -0,0 +1,259 @@ +@charset "utf-8"; + +body { + margin:0; +} + +#mocha { + font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: 60px 50px; +} + +#mocha ul, #mocha li { + margin: 0; + padding: 0; +} + +#mocha ul { + list-style: none; +} + +#mocha h1, #mocha h2 { + margin: 0; +} + +#mocha h1 { + margin-top: 15px; + font-size: 1em; + font-weight: 200; +} + +#mocha h1 a { + text-decoration: none; + color: inherit; +} + +#mocha h1 a:hover { + text-decoration: underline; +} + +#mocha .suite .suite h1 { + margin-top: 0; + font-size: .8em; +} + +#mocha .hidden { + display: none; +} + +#mocha h2 { + font-size: 12px; + font-weight: normal; + cursor: pointer; +} + +#mocha .suite { + margin-left: 15px; +} + +#mocha .test { + margin-left: 15px; + overflow: hidden; +} + +#mocha .test.pending:hover h2::after { + content: '(pending)'; + font-family: arial, sans-serif; +} + +#mocha .test.pass.medium .duration { + background: #C09853; +} + +#mocha .test.pass.slow .duration { + background: #B94A48; +} + +#mocha .test.pass::before { + content: '✓'; + font-size: 12px; + display: block; + float: left; + margin-right: 5px; + color: #00d6b2; +} + +#mocha .test.pass .duration { + font-size: 9px; + margin-left: 5px; + padding: 2px 5px; + color: white; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); + -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); + box-shadow: inset 0 1px 1px rgba(0,0,0,.2); + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + -ms-border-radius: 5px; + -o-border-radius: 5px; + border-radius: 5px; +} + +#mocha .test.pass.fast .duration { + display: none; +} + +#mocha .test.pending { + color: #0b97c4; +} + +#mocha .test.pending::before { + content: '◦'; + color: #0b97c4; +} + +#mocha .test.fail { + color: #c00; +} + +#mocha .test.fail pre { + color: black; +} + +#mocha .test.fail::before { + content: '✖'; + font-size: 12px; + display: block; + float: left; + margin-right: 5px; + color: #c00; +} + +#mocha .test pre.error { + color: #c00; + max-height: 300px; + overflow: auto; +} + +#mocha .test pre { + display: block; + float: left; + clear: left; + font: 12px/1.5 monaco, monospace; + margin: 5px; + padding: 15px; + border: 1px solid #eee; + border-bottom-color: #ddd; + -webkit-border-radius: 3px; + -webkit-box-shadow: 0 1px 3px #eee; + -moz-border-radius: 3px; + -moz-box-shadow: 0 1px 3px #eee; +} + +#mocha .test h2 { + position: relative; +} + +#mocha .test a.replay { + position: absolute; + top: 3px; + right: 0; + text-decoration: none; + vertical-align: middle; + display: block; + width: 15px; + height: 15px; + line-height: 15px; + text-align: center; + background: #eee; + font-size: 15px; + -moz-border-radius: 15px; + border-radius: 15px; + -webkit-transition: opacity 200ms; + -moz-transition: opacity 200ms; + transition: opacity 200ms; + opacity: 0.3; + color: #888; +} + +#mocha .test:hover a.replay { + opacity: 1; +} + +#mocha-report.pass .test.fail { + display: none; +} + +#mocha-report.fail .test.pass { + display: none; +} + +#mocha-report.pending .test.pass, +#mocha-report.pending .test.fail { + display: none; +} +#mocha-report.pending .test.pass.pending { + display: block; +} + +#mocha-error { + color: #c00; + font-size: 1.5em; + font-weight: 100; + letter-spacing: 1px; +} + +#mocha-stats { + position: fixed; + top: 15px; + right: 10px; + font-size: 12px; + margin: 0; + color: #888; + z-index: 1; +} + +#mocha-stats .progress { + float: right; + padding-top: 0; +} + +#mocha-stats em { + color: black; +} + +#mocha-stats a { + text-decoration: none; + color: inherit; +} + +#mocha-stats a:hover { + border-bottom: 1px solid #eee; +} + +#mocha-stats li { + display: inline-block; + margin: 0 5px; + list-style: none; + padding-top: 11px; +} + +#mocha-stats canvas { + width: 40px; + height: 40px; +} + +#mocha code .comment { color: #ddd } +#mocha code .init { color: #2F6FAD } +#mocha code .string { color: #5890AD } +#mocha code .keyword { color: #8A6343 } +#mocha code .number { color: #2F6FAD } + +@media screen and (max-device-width: 480px) { + #mocha { + margin: 60px 0px; + } + + #mocha #stats { + position: absolute; + } +} \ No newline at end of file diff --git a/tests/libraries/mocha.js b/tests/libraries/mocha.js new file mode 100644 index 00000000..1ca585d6 --- /dev/null +++ b/tests/libraries/mocha.js @@ -0,0 +1,5471 @@ +;(function(){ + +// CommonJS require() + +function require(p){ + var path = require.resolve(p) + , mod = require.modules[path]; + if (!mod) throw new Error('failed to require "' + p + '"'); + if (!mod.exports) { + mod.exports = {}; + mod.call(mod.exports, mod, mod.exports, require.relative(path)); + } + return mod.exports; + } + +require.modules = {}; + +require.resolve = function (path){ + var orig = path + , reg = path + '.js' + , index = path + '/index.js'; + return require.modules[reg] && reg + || require.modules[index] && index + || orig; + }; + +require.register = function (path, fn){ + require.modules[path] = fn; + }; + +require.relative = function (parent) { + return function(p){ + if ('.' != p.charAt(0)) return require(p); + + var path = parent.split('/') + , segs = p.split('/'); + path.pop(); + + for (var i = 0; i < segs.length; i++) { + var seg = segs[i]; + if ('..' == seg) path.pop(); + else if ('.' != seg) path.push(seg); + } + + return require(path.join('/')); + }; + }; + + +require.register("browser/debug.js", function(module, exports, require){ + +module.exports = function(type){ + return function(){ + } +}; + +}); // module: browser/debug.js + +require.register("browser/diff.js", function(module, exports, require){ +/* See license.txt for terms of usage */ + +/* + * Text diff implementation. + * + * This library supports the following APIS: + * JsDiff.diffChars: Character by character diff + * JsDiff.diffWords: Word (as defined by \b regex) diff which ignores whitespace + * JsDiff.diffLines: Line based diff + * + * JsDiff.diffCss: Diff targeted at CSS content + * + * These methods are based on the implementation proposed in + * "An O(ND) Difference Algorithm and its Variations" (Myers, 1986). + * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.4.6927 + */ +var JsDiff = (function() { + function clonePath(path) { + return { newPos: path.newPos, components: path.components.slice(0) }; + } + function removeEmpty(array) { + var ret = []; + for (var i = 0; i < array.length; i++) { + if (array[i]) { + ret.push(array[i]); + } + } + return ret; + } + function escapeHTML(s) { + var n = s; + n = n.replace(/&/g, "&"); + n = n.replace(//g, ">"); + n = n.replace(/"/g, """); + + return n; + } + + + var fbDiff = function(ignoreWhitespace) { + this.ignoreWhitespace = ignoreWhitespace; + }; + fbDiff.prototype = { + diff: function(oldString, newString) { + // Handle the identity case (this is due to unrolling editLength == 0 + if (newString == oldString) { + return [{ value: newString }]; + } + if (!newString) { + return [{ value: oldString, removed: true }]; + } + if (!oldString) { + return [{ value: newString, added: true }]; + } + + newString = this.tokenize(newString); + oldString = this.tokenize(oldString); + + var newLen = newString.length, oldLen = oldString.length; + var maxEditLength = newLen + oldLen; + var bestPath = [{ newPos: -1, components: [] }]; + + // Seed editLength = 0 + var oldPos = this.extractCommon(bestPath[0], newString, oldString, 0); + if (bestPath[0].newPos+1 >= newLen && oldPos+1 >= oldLen) { + return bestPath[0].components; + } + + for (var editLength = 1; editLength <= maxEditLength; editLength++) { + for (var diagonalPath = -1*editLength; diagonalPath <= editLength; diagonalPath+=2) { + var basePath; + var addPath = bestPath[diagonalPath-1], + removePath = bestPath[diagonalPath+1]; + oldPos = (removePath ? removePath.newPos : 0) - diagonalPath; + if (addPath) { + // No one else is going to attempt to use this value, clear it + bestPath[diagonalPath-1] = undefined; + } + + var canAdd = addPath && addPath.newPos+1 < newLen; + var canRemove = removePath && 0 <= oldPos && oldPos < oldLen; + if (!canAdd && !canRemove) { + bestPath[diagonalPath] = undefined; + continue; + } + + // Select the diagonal that we want to branch from. We select the prior + // path whose position in the new string is the farthest from the origin + // and does not pass the bounds of the diff graph + if (!canAdd || (canRemove && addPath.newPos < removePath.newPos)) { + basePath = clonePath(removePath); + this.pushComponent(basePath.components, oldString[oldPos], undefined, true); + } else { + basePath = clonePath(addPath); + basePath.newPos++; + this.pushComponent(basePath.components, newString[basePath.newPos], true, undefined); + } + + var oldPos = this.extractCommon(basePath, newString, oldString, diagonalPath); + + if (basePath.newPos+1 >= newLen && oldPos+1 >= oldLen) { + return basePath.components; + } else { + bestPath[diagonalPath] = basePath; + } + } + } + }, + + pushComponent: function(components, value, added, removed) { + var last = components[components.length-1]; + if (last && last.added === added && last.removed === removed) { + // We need to clone here as the component clone operation is just + // as shallow array clone + components[components.length-1] = + {value: this.join(last.value, value), added: added, removed: removed }; + } else { + components.push({value: value, added: added, removed: removed }); + } + }, + extractCommon: function(basePath, newString, oldString, diagonalPath) { + var newLen = newString.length, + oldLen = oldString.length, + newPos = basePath.newPos, + oldPos = newPos - diagonalPath; + while (newPos+1 < newLen && oldPos+1 < oldLen && this.equals(newString[newPos+1], oldString[oldPos+1])) { + newPos++; + oldPos++; + + this.pushComponent(basePath.components, newString[newPos], undefined, undefined); + } + basePath.newPos = newPos; + return oldPos; + }, + + equals: function(left, right) { + var reWhitespace = /\S/; + if (this.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right)) { + return true; + } else { + return left == right; + } + }, + join: function(left, right) { + return left + right; + }, + tokenize: function(value) { + return value; + } + }; + + var CharDiff = new fbDiff(); + + var WordDiff = new fbDiff(true); + WordDiff.tokenize = function(value) { + return removeEmpty(value.split(/(\s+|\b)/)); + }; + + var CssDiff = new fbDiff(true); + CssDiff.tokenize = function(value) { + return removeEmpty(value.split(/([{}:;,]|\s+)/)); + }; + + var LineDiff = new fbDiff(); + LineDiff.tokenize = function(value) { + return value.split(/^/m); + }; + + return { + diffChars: function(oldStr, newStr) { return CharDiff.diff(oldStr, newStr); }, + diffWords: function(oldStr, newStr) { return WordDiff.diff(oldStr, newStr); }, + diffLines: function(oldStr, newStr) { return LineDiff.diff(oldStr, newStr); }, + + diffCss: function(oldStr, newStr) { return CssDiff.diff(oldStr, newStr); }, + + createPatch: function(fileName, oldStr, newStr, oldHeader, newHeader) { + var ret = []; + + ret.push("Index: " + fileName); + ret.push("==================================================================="); + ret.push("--- " + fileName + (typeof oldHeader === "undefined" ? "" : "\t" + oldHeader)); + ret.push("+++ " + fileName + (typeof newHeader === "undefined" ? "" : "\t" + newHeader)); + + var diff = LineDiff.diff(oldStr, newStr); + if (!diff[diff.length-1].value) { + diff.pop(); // Remove trailing newline add + } + diff.push({value: "", lines: []}); // Append an empty value to make cleanup easier + + function contextLines(lines) { + return lines.map(function(entry) { return ' ' + entry; }); + } + function eofNL(curRange, i, current) { + var last = diff[diff.length-2], + isLast = i === diff.length-2, + isLastOfType = i === diff.length-3 && (current.added === !last.added || current.removed === !last.removed); + + // Figure out if this is the last line for the given file and missing NL + if (!/\n$/.test(current.value) && (isLast || isLastOfType)) { + curRange.push('\\ No newline at end of file'); + } + } + + var oldRangeStart = 0, newRangeStart = 0, curRange = [], + oldLine = 1, newLine = 1; + for (var i = 0; i < diff.length; i++) { + var current = diff[i], + lines = current.lines || current.value.replace(/\n$/, "").split("\n"); + current.lines = lines; + + if (current.added || current.removed) { + if (!oldRangeStart) { + var prev = diff[i-1]; + oldRangeStart = oldLine; + newRangeStart = newLine; + + if (prev) { + curRange = contextLines(prev.lines.slice(-4)); + oldRangeStart -= curRange.length; + newRangeStart -= curRange.length; + } + } + curRange.push.apply(curRange, lines.map(function(entry) { return (current.added?"+":"-") + entry; })); + eofNL(curRange, i, current); + + if (current.added) { + newLine += lines.length; + } else { + oldLine += lines.length; + } + } else { + if (oldRangeStart) { + // Close out any changes that have been output (or join overlapping) + if (lines.length <= 8 && i < diff.length-2) { + // Overlapping + curRange.push.apply(curRange, contextLines(lines)); + } else { + // end the range and output + var contextSize = Math.min(lines.length, 4); + ret.push( + "@@ -" + oldRangeStart + "," + (oldLine-oldRangeStart+contextSize) + + " +" + newRangeStart + "," + (newLine-newRangeStart+contextSize) + + " @@"); + ret.push.apply(ret, curRange); + ret.push.apply(ret, contextLines(lines.slice(0, contextSize))); + if (lines.length <= 4) { + eofNL(ret, i, current); + } + + oldRangeStart = 0; newRangeStart = 0; curRange = []; + } + } + oldLine += lines.length; + newLine += lines.length; + } + } + + return ret.join('\n') + '\n'; + }, + + convertChangesToXML: function(changes){ + var ret = []; + for ( var i = 0; i < changes.length; i++) { + var change = changes[i]; + if (change.added) { + ret.push(""); + } else if (change.removed) { + ret.push(""); + } + + ret.push(escapeHTML(change.value)); + + if (change.added) { + ret.push(""); + } else if (change.removed) { + ret.push(""); + } + } + return ret.join(""); + } + }; +})(); + +if (typeof module !== "undefined") { + module.exports = JsDiff; +} + +}); // module: browser/diff.js + +require.register("browser/events.js", function(module, exports, require){ + +/** + * Module exports. + */ + +exports.EventEmitter = EventEmitter; + +/** + * Check if `obj` is an array. + */ + +function isArray(obj) { + return '[object Array]' == {}.toString.call(obj); +} + +/** + * Event emitter constructor. + * + * @api public + */ + +function EventEmitter(){}; + +/** + * Adds a listener. + * + * @api public + */ + +EventEmitter.prototype.on = function (name, fn) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = fn; + } else if (isArray(this.$events[name])) { + this.$events[name].push(fn); + } else { + this.$events[name] = [this.$events[name], fn]; + } + + return this; +}; + +EventEmitter.prototype.addListener = EventEmitter.prototype.on; + +/** + * Adds a volatile listener. + * + * @api public + */ + +EventEmitter.prototype.once = function (name, fn) { + var self = this; + + function on () { + self.removeListener(name, on); + fn.apply(this, arguments); + }; + + on.listener = fn; + this.on(name, on); + + return this; +}; + +/** + * Removes a listener. + * + * @api public + */ + +EventEmitter.prototype.removeListener = function (name, fn) { + if (this.$events && this.$events[name]) { + var list = this.$events[name]; + + if (isArray(list)) { + var pos = -1; + + for (var i = 0, l = list.length; i < l; i++) { + if (list[i] === fn || (list[i].listener && list[i].listener === fn)) { + pos = i; + break; + } + } + + if (pos < 0) { + return this; + } + + list.splice(pos, 1); + + if (!list.length) { + delete this.$events[name]; + } + } else if (list === fn || (list.listener && list.listener === fn)) { + delete this.$events[name]; + } + } + + return this; +}; + +/** + * Removes all listeners for an event. + * + * @api public + */ + +EventEmitter.prototype.removeAllListeners = function (name) { + if (name === undefined) { + this.$events = {}; + return this; + } + + if (this.$events && this.$events[name]) { + this.$events[name] = null; + } + + return this; +}; + +/** + * Gets all listeners for a certain event. + * + * @api public + */ + +EventEmitter.prototype.listeners = function (name) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = []; + } + + if (!isArray(this.$events[name])) { + this.$events[name] = [this.$events[name]]; + } + + return this.$events[name]; +}; + +/** + * Emits an event. + * + * @api public + */ + +EventEmitter.prototype.emit = function (name) { + if (!this.$events) { + return false; + } + + var handler = this.$events[name]; + + if (!handler) { + return false; + } + + var args = [].slice.call(arguments, 1); + + if ('function' == typeof handler) { + handler.apply(this, args); + } else if (isArray(handler)) { + var listeners = handler.slice(); + + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); + } + } else { + return false; + } + + return true; +}; +}); // module: browser/events.js + +require.register("browser/fs.js", function(module, exports, require){ + +}); // module: browser/fs.js + +require.register("browser/path.js", function(module, exports, require){ + +}); // module: browser/path.js + +require.register("browser/progress.js", function(module, exports, require){ + +/** + * Expose `Progress`. + */ + +module.exports = Progress; + +/** + * Initialize a new `Progress` indicator. + */ + +function Progress() { + this.percent = 0; + this.size(0); + this.fontSize(11); + this.font('helvetica, arial, sans-serif'); +} + +/** + * Set progress size to `n`. + * + * @param {Number} n + * @return {Progress} for chaining + * @api public + */ + +Progress.prototype.size = function(n){ + this._size = n; + return this; +}; + +/** + * Set text to `str`. + * + * @param {String} str + * @return {Progress} for chaining + * @api public + */ + +Progress.prototype.text = function(str){ + this._text = str; + return this; +}; + +/** + * Set font size to `n`. + * + * @param {Number} n + * @return {Progress} for chaining + * @api public + */ + +Progress.prototype.fontSize = function(n){ + this._fontSize = n; + return this; +}; + +/** + * Set font `family`. + * + * @param {String} family + * @return {Progress} for chaining + */ + +Progress.prototype.font = function(family){ + this._font = family; + return this; +}; + +/** + * Update percentage to `n`. + * + * @param {Number} n + * @return {Progress} for chaining + */ + +Progress.prototype.update = function(n){ + this.percent = n; + return this; +}; + +/** + * Draw on `ctx`. + * + * @param {CanvasRenderingContext2d} ctx + * @return {Progress} for chaining + */ + +Progress.prototype.draw = function(ctx){ + var percent = Math.min(this.percent, 100) + , size = this._size + , half = size / 2 + , x = half + , y = half + , rad = half - 1 + , fontSize = this._fontSize; + + ctx.font = fontSize + 'px ' + this._font; + + var angle = Math.PI * 2 * (percent / 100); + ctx.clearRect(0, 0, size, size); + + // outer circle + ctx.strokeStyle = '#9f9f9f'; + ctx.beginPath(); + ctx.arc(x, y, rad, 0, angle, false); + ctx.stroke(); + + // inner circle + ctx.strokeStyle = '#eee'; + ctx.beginPath(); + ctx.arc(x, y, rad - 1, 0, angle, true); + ctx.stroke(); + + // text + var text = this._text || (percent | 0) + '%' + , w = ctx.measureText(text).width; + + ctx.fillText( + text + , x - w / 2 + 1 + , y + fontSize / 2 - 1); + + return this; +}; + +}); // module: browser/progress.js + +require.register("browser/tty.js", function(module, exports, require){ + +exports.isatty = function(){ + return true; +}; + +exports.getWindowSize = function(){ + if ('innerHeight' in global) { + return [global.innerHeight, global.innerWidth]; + } else { + // In a Web Worker, the DOM Window is not available. + return [640, 480]; + } +}; + +}); // module: browser/tty.js + +require.register("context.js", function(module, exports, require){ + +/** + * Expose `Context`. + */ + +module.exports = Context; + +/** + * Initialize a new `Context`. + * + * @api private + */ + +function Context(){} + +/** + * Set or get the context `Runnable` to `runnable`. + * + * @param {Runnable} runnable + * @return {Context} + * @api private + */ + +Context.prototype.runnable = function(runnable){ + if (0 == arguments.length) return this._runnable; + this.test = this._runnable = runnable; + return this; +}; + +/** + * Set test timeout `ms`. + * + * @param {Number} ms + * @return {Context} self + * @api private + */ + +Context.prototype.timeout = function(ms){ + this.runnable().timeout(ms); + return this; +}; + +/** + * Set test slowness threshold `ms`. + * + * @param {Number} ms + * @return {Context} self + * @api private + */ + +Context.prototype.slow = function(ms){ + this.runnable().slow(ms); + return this; +}; + +/** + * Inspect the context void of `._runnable`. + * + * @return {String} + * @api private + */ + +Context.prototype.inspect = function(){ + return JSON.stringify(this, function(key, val){ + if ('_runnable' == key) return; + if ('test' == key) return; + return val; + }, 2); +}; + +}); // module: context.js + +require.register("hook.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Runnable = require('./runnable'); + +/** + * Expose `Hook`. + */ + +module.exports = Hook; + +/** + * Initialize a new `Hook` with the given `title` and callback `fn`. + * + * @param {String} title + * @param {Function} fn + * @api private + */ + +function Hook(title, fn) { + Runnable.call(this, title, fn); + this.type = 'hook'; +} + +/** + * Inherit from `Runnable.prototype`. + */ + +function F(){}; +F.prototype = Runnable.prototype; +Hook.prototype = new F; +Hook.prototype.constructor = Hook; + + +/** + * Get or set the test `err`. + * + * @param {Error} err + * @return {Error} + * @api public + */ + +Hook.prototype.error = function(err){ + if (0 == arguments.length) { + var err = this._error; + this._error = null; + return err; + } + + this._error = err; +}; + +}); // module: hook.js + +require.register("interfaces/bdd.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Suite = require('../suite') + , Test = require('../test') + , utils = require('../utils'); + +/** + * BDD-style interface: + * + * describe('Array', function(){ + * describe('#indexOf()', function(){ + * it('should return -1 when not present', function(){ + * + * }); + * + * it('should return the index when present', function(){ + * + * }); + * }); + * }); + * + */ + +module.exports = function(suite){ + var suites = [suite]; + + suite.on('pre-require', function(context, file, mocha){ + + /** + * Execute before running tests. + */ + + context.before = function(fn){ + suites[0].beforeAll(fn); + }; + + /** + * Execute after running tests. + */ + + context.after = function(fn){ + suites[0].afterAll(fn); + }; + + /** + * Execute before each test case. + */ + + context.beforeEach = function(fn){ + suites[0].beforeEach(fn); + }; + + /** + * Execute after each test case. + */ + + context.afterEach = function(fn){ + suites[0].afterEach(fn); + }; + + /** + * Describe a "suite" with the given `title` + * and callback `fn` containing nested suites + * and/or tests. + */ + + context.describe = context.context = function(title, fn){ + var suite = Suite.create(suites[0], title); + suites.unshift(suite); + fn.call(suite); + suites.shift(); + return suite; + }; + + /** + * Pending describe. + */ + + context.xdescribe = + context.xcontext = + context.describe.skip = function(title, fn){ + var suite = Suite.create(suites[0], title); + suite.pending = true; + suites.unshift(suite); + fn.call(suite); + suites.shift(); + }; + + /** + * Exclusive suite. + */ + + context.describe.only = function(title, fn){ + var suite = context.describe(title, fn); + mocha.grep(suite.fullTitle()); + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + */ + + context.it = context.specify = function(title, fn){ + var suite = suites[0]; + if (suite.pending) var fn = null; + var test = new Test(title, fn); + suite.addTest(test); + return test; + }; + + /** + * Exclusive test-case. + */ + + context.it.only = function(title, fn){ + var test = context.it(title, fn); + var reString = '^' + utils.escapeRegexp(test.fullTitle()) + '$'; + mocha.grep(new RegExp(reString)); + }; + + /** + * Pending test case. + */ + + context.xit = + context.xspecify = + context.it.skip = function(title){ + context.it(title); + }; + }); +}; + +}); // module: interfaces/bdd.js + +require.register("interfaces/exports.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Suite = require('../suite') + , Test = require('../test'); + +/** + * TDD-style interface: + * + * exports.Array = { + * '#indexOf()': { + * 'should return -1 when the value is not present': function(){ + * + * }, + * + * 'should return the correct index when the value is present': function(){ + * + * } + * } + * }; + * + */ + +module.exports = function(suite){ + var suites = [suite]; + + suite.on('require', visit); + + function visit(obj) { + var suite; + for (var key in obj) { + if ('function' == typeof obj[key]) { + var fn = obj[key]; + switch (key) { + case 'before': + suites[0].beforeAll(fn); + break; + case 'after': + suites[0].afterAll(fn); + break; + case 'beforeEach': + suites[0].beforeEach(fn); + break; + case 'afterEach': + suites[0].afterEach(fn); + break; + default: + suites[0].addTest(new Test(key, fn)); + } + } else { + var suite = Suite.create(suites[0], key); + suites.unshift(suite); + visit(obj[key]); + suites.shift(); + } + } + } +}; + +}); // module: interfaces/exports.js + +require.register("interfaces/index.js", function(module, exports, require){ + +exports.bdd = require('./bdd'); +exports.tdd = require('./tdd'); +exports.qunit = require('./qunit'); +exports.exports = require('./exports'); + +}); // module: interfaces/index.js + +require.register("interfaces/qunit.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Suite = require('../suite') + , Test = require('../test') + , utils = require('../utils'); + +/** + * QUnit-style interface: + * + * suite('Array'); + * + * test('#length', function(){ + * var arr = [1,2,3]; + * ok(arr.length == 3); + * }); + * + * test('#indexOf()', function(){ + * var arr = [1,2,3]; + * ok(arr.indexOf(1) == 0); + * ok(arr.indexOf(2) == 1); + * ok(arr.indexOf(3) == 2); + * }); + * + * suite('String'); + * + * test('#length', function(){ + * ok('foo'.length == 3); + * }); + * + */ + +module.exports = function(suite){ + var suites = [suite]; + + suite.on('pre-require', function(context, file, mocha){ + + /** + * Execute before running tests. + */ + + context.before = function(fn){ + suites[0].beforeAll(fn); + }; + + /** + * Execute after running tests. + */ + + context.after = function(fn){ + suites[0].afterAll(fn); + }; + + /** + * Execute before each test case. + */ + + context.beforeEach = function(fn){ + suites[0].beforeEach(fn); + }; + + /** + * Execute after each test case. + */ + + context.afterEach = function(fn){ + suites[0].afterEach(fn); + }; + + /** + * Describe a "suite" with the given `title`. + */ + + context.suite = function(title){ + if (suites.length > 1) suites.shift(); + var suite = Suite.create(suites[0], title); + suites.unshift(suite); + return suite; + }; + + /** + * Exclusive test-case. + */ + + context.suite.only = function(title, fn){ + var suite = context.suite(title, fn); + mocha.grep(suite.fullTitle()); + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + */ + + context.test = function(title, fn){ + var test = new Test(title, fn); + suites[0].addTest(test); + return test; + }; + + /** + * Exclusive test-case. + */ + + context.test.only = function(title, fn){ + var test = context.test(title, fn); + var reString = '^' + utils.escapeRegexp(test.fullTitle()) + '$'; + mocha.grep(new RegExp(reString)); + }; + + /** + * Pending test case. + */ + + context.test.skip = function(title){ + context.test(title); + }; + }); +}; + +}); // module: interfaces/qunit.js + +require.register("interfaces/tdd.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Suite = require('../suite') + , Test = require('../test') + , utils = require('../utils');; + +/** + * TDD-style interface: + * + * suite('Array', function(){ + * suite('#indexOf()', function(){ + * suiteSetup(function(){ + * + * }); + * + * test('should return -1 when not present', function(){ + * + * }); + * + * test('should return the index when present', function(){ + * + * }); + * + * suiteTeardown(function(){ + * + * }); + * }); + * }); + * + */ + +module.exports = function(suite){ + var suites = [suite]; + + suite.on('pre-require', function(context, file, mocha){ + + /** + * Execute before each test case. + */ + + context.setup = function(fn){ + suites[0].beforeEach(fn); + }; + + /** + * Execute after each test case. + */ + + context.teardown = function(fn){ + suites[0].afterEach(fn); + }; + + /** + * Execute before the suite. + */ + + context.suiteSetup = function(fn){ + suites[0].beforeAll(fn); + }; + + /** + * Execute after the suite. + */ + + context.suiteTeardown = function(fn){ + suites[0].afterAll(fn); + }; + + /** + * Describe a "suite" with the given `title` + * and callback `fn` containing nested suites + * and/or tests. + */ + + context.suite = function(title, fn){ + var suite = Suite.create(suites[0], title); + suites.unshift(suite); + fn.call(suite); + suites.shift(); + return suite; + }; + + /** + * Pending suite. + */ + context.suite.skip = function(title, fn) { + var suite = Suite.create(suites[0], title); + suite.pending = true; + suites.unshift(suite); + fn.call(suite); + suites.shift(); + }; + + /** + * Exclusive test-case. + */ + + context.suite.only = function(title, fn){ + var suite = context.suite(title, fn); + mocha.grep(suite.fullTitle()); + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + */ + + context.test = function(title, fn){ + var suite = suites[0]; + if (suite.pending) var fn = null; + var test = new Test(title, fn); + suite.addTest(test); + return test; + }; + + /** + * Exclusive test-case. + */ + + context.test.only = function(title, fn){ + var test = context.test(title, fn); + var reString = '^' + utils.escapeRegexp(test.fullTitle()) + '$'; + mocha.grep(new RegExp(reString)); + }; + + /** + * Pending test case. + */ + + context.test.skip = function(title){ + context.test(title); + }; + }); +}; + +}); // module: interfaces/tdd.js + +require.register("mocha.js", function(module, exports, require){ +/*! + * mocha + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var path = require('browser/path') + , utils = require('./utils'); + +/** + * Expose `Mocha`. + */ + +exports = module.exports = Mocha; + +/** + * Expose internals. + */ + +exports.utils = utils; +exports.interfaces = require('./interfaces'); +exports.reporters = require('./reporters'); +exports.Runnable = require('./runnable'); +exports.Context = require('./context'); +exports.Runner = require('./runner'); +exports.Suite = require('./suite'); +exports.Hook = require('./hook'); +exports.Test = require('./test'); + +/** + * Return image `name` path. + * + * @param {String} name + * @return {String} + * @api private + */ + +function image(name) { + return __dirname + '/../images/' + name + '.png'; +} + +/** + * Setup mocha with `options`. + * + * Options: + * + * - `ui` name "bdd", "tdd", "exports" etc + * - `reporter` reporter instance, defaults to `mocha.reporters.Dot` + * - `globals` array of accepted globals + * - `timeout` timeout in milliseconds + * - `bail` bail on the first test failure + * - `slow` milliseconds to wait before considering a test slow + * - `ignoreLeaks` ignore global leaks + * - `grep` string or regexp to filter tests with + * + * @param {Object} options + * @api public + */ + +function Mocha(options) { + options = options || {}; + this.files = []; + this.options = options; + this.grep(options.grep); + this.suite = new exports.Suite('', new exports.Context); + this.ui(options.ui); + this.bail(options.bail); + this.reporter(options.reporter); + if (typeof options.timeout === 'number') this.timeout(options.timeout); + if (options.slow) this.slow(options.slow); +} + +/** + * Enable or disable bailing on the first failure. + * + * @param {Boolean} [bail] + * @api public + */ + +Mocha.prototype.bail = function(bail){ + if (0 == arguments.length) bail = true; + this.suite.bail(bail); + return this; +}; + +/** + * Add test `file`. + * + * @param {String} file + * @api public + */ + +Mocha.prototype.addFile = function(file){ + this.files.push(file); + return this; +}; + +/** + * Set reporter to `reporter`, defaults to "dot". + * + * @param {String|Function} reporter name or constructor + * @api public + */ + +Mocha.prototype.reporter = function(reporter){ + if ('function' == typeof reporter) { + this._reporter = reporter; + } else { + reporter = reporter || 'dot'; + try { + this._reporter = require('./reporters/' + reporter); + } catch (err) { + this._reporter = require(reporter); + } + if (!this._reporter) throw new Error('invalid reporter "' + reporter + '"'); + } + return this; +}; + +/** + * Set test UI `name`, defaults to "bdd". + * + * @param {String} bdd + * @api public + */ + +Mocha.prototype.ui = function(name){ + name = name || 'bdd'; + this._ui = exports.interfaces[name]; + if (!this._ui) throw new Error('invalid interface "' + name + '"'); + this._ui = this._ui(this.suite); + return this; +}; + +/** + * Load registered files. + * + * @api private + */ + +Mocha.prototype.loadFiles = function(fn){ + var self = this; + var suite = this.suite; + var pending = this.files.length; + this.files.forEach(function(file){ + file = path.resolve(file); + suite.emit('pre-require', global, file, self); + suite.emit('require', require(file), file, self); + suite.emit('post-require', global, file, self); + --pending || (fn && fn()); + }); +}; + +/** + * Enable growl support. + * + * @api private + */ + +Mocha.prototype._growl = function(runner, reporter) { + var notify = require('growl'); + + runner.on('end', function(){ + var stats = reporter.stats; + if (stats.failures) { + var msg = stats.failures + ' of ' + runner.total + ' tests failed'; + notify(msg, { name: 'mocha', title: 'Failed', image: image('error') }); + } else { + notify(stats.passes + ' tests passed in ' + stats.duration + 'ms', { + name: 'mocha' + , title: 'Passed' + , image: image('ok') + }); + } + }); +}; + +/** + * Add regexp to grep, if `re` is a string it is escaped. + * + * @param {RegExp|String} re + * @return {Mocha} + * @api public + */ + +Mocha.prototype.grep = function(re){ + this.options.grep = 'string' == typeof re + ? new RegExp(utils.escapeRegexp(re)) + : re; + return this; +}; + +/** + * Invert `.grep()` matches. + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.invert = function(){ + this.options.invert = true; + return this; +}; + +/** + * Ignore global leaks. + * + * @param {Boolean} ignore + * @return {Mocha} + * @api public + */ + +Mocha.prototype.ignoreLeaks = function(ignore){ + this.options.ignoreLeaks = !!ignore; + return this; +}; + +/** + * Enable global leak checking. + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.checkLeaks = function(){ + this.options.ignoreLeaks = false; + return this; +}; + +/** + * Enable growl support. + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.growl = function(){ + this.options.growl = true; + return this; +}; + +/** + * Ignore `globals` array or string. + * + * @param {Array|String} globals + * @return {Mocha} + * @api public + */ + +Mocha.prototype.globals = function(globals){ + this.options.globals = (this.options.globals || []).concat(globals); + return this; +}; + +/** + * Set the timeout in milliseconds. + * + * @param {Number} timeout + * @return {Mocha} + * @api public + */ + +Mocha.prototype.timeout = function(timeout){ + this.suite.timeout(timeout); + return this; +}; + +/** + * Set slowness threshold in milliseconds. + * + * @param {Number} slow + * @return {Mocha} + * @api public + */ + +Mocha.prototype.slow = function(slow){ + this.suite.slow(slow); + return this; +}; + +/** + * Makes all tests async (accepting a callback) + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.asyncOnly = function(){ + this.options.asyncOnly = true; + return this; +}; + +/** + * Run tests and invoke `fn()` when complete. + * + * @param {Function} fn + * @return {Runner} + * @api public + */ + +Mocha.prototype.run = function(fn){ + if (this.files.length) this.loadFiles(); + var suite = this.suite; + var options = this.options; + var runner = new exports.Runner(suite); + var reporter = new this._reporter(runner); + runner.ignoreLeaks = false !== options.ignoreLeaks; + runner.asyncOnly = options.asyncOnly; + if (options.grep) runner.grep(options.grep, options.invert); + if (options.globals) runner.globals(options.globals); + if (options.growl) this._growl(runner, reporter); + return runner.run(fn); +}; + +}); // module: mocha.js + +require.register("ms.js", function(module, exports, require){ +/** + * Helpers. + */ + +var s = 1000; +var m = s * 60; +var h = m * 60; +var d = h * 24; +var y = d * 365.25; + +/** + * Parse or format the given `val`. + * + * Options: + * + * - `long` verbose formatting [false] + * + * @param {String|Number} val + * @param {Object} options + * @return {String|Number} + * @api public + */ + +module.exports = function(val, options){ + options = options || {}; + if ('string' == typeof val) return parse(val); + return options.long + ? long(val) + : short(val); +}; + +/** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ + +function parse(str) { + var match = /^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec(str); + if (!match) return; + var n = parseFloat(match[1]); + var type = (match[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'y': + return n * y; + case 'days': + case 'day': + case 'd': + return n * d; + case 'hours': + case 'hour': + case 'h': + return n * h; + case 'minutes': + case 'minute': + case 'm': + return n * m; + case 'seconds': + case 'second': + case 's': + return n * s; + case 'ms': + return n; + } +} + +/** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function short(ms) { + if (ms >= d) return Math.round(ms / d) + 'd'; + if (ms >= h) return Math.round(ms / h) + 'h'; + if (ms >= m) return Math.round(ms / m) + 'm'; + if (ms >= s) return Math.round(ms / s) + 's'; + return ms + 'ms'; +} + +/** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function long(ms) { + return plural(ms, d, 'day') + || plural(ms, h, 'hour') + || plural(ms, m, 'minute') + || plural(ms, s, 'second') + || ms + ' ms'; +} + +/** + * Pluralization helper. + */ + +function plural(ms, n, name) { + if (ms < n) return; + if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name; + return Math.ceil(ms / n) + ' ' + name + 's'; +} + +}); // module: ms.js + +require.register("reporters/base.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var tty = require('browser/tty') + , diff = require('browser/diff') + , ms = require('../ms'); + +/** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + +var Date = global.Date + , setTimeout = global.setTimeout + , setInterval = global.setInterval + , clearTimeout = global.clearTimeout + , clearInterval = global.clearInterval; + +/** + * Check if both stdio streams are associated with a tty. + */ + +var isatty = tty.isatty(1) && tty.isatty(2); + +/** + * Expose `Base`. + */ + +exports = module.exports = Base; + +/** + * Enable coloring by default. + */ + +exports.useColors = isatty; + +/** + * Default color map. + */ + +exports.colors = { + 'pass': 90 + , 'fail': 31 + , 'bright pass': 92 + , 'bright fail': 91 + , 'bright yellow': 93 + , 'pending': 36 + , 'suite': 0 + , 'error title': 0 + , 'error message': 31 + , 'error stack': 90 + , 'checkmark': 32 + , 'fast': 90 + , 'medium': 33 + , 'slow': 31 + , 'green': 32 + , 'light': 90 + , 'diff gutter': 90 + , 'diff added': 42 + , 'diff removed': 41 +}; + +/** + * Default symbol map. + */ + +exports.symbols = { + ok: '✓', + err: '✖', + dot: '․' +}; + +// With node.js on Windows: use symbols available in terminal default fonts +if ('win32' == process.platform) { + exports.symbols.ok = '\u221A'; + exports.symbols.err = '\u00D7'; + exports.symbols.dot = '.'; +} + +/** + * Color `str` with the given `type`, + * allowing colors to be disabled, + * as well as user-defined color + * schemes. + * + * @param {String} type + * @param {String} str + * @return {String} + * @api private + */ + +var color = exports.color = function(type, str) { + if (!exports.useColors) return str; + return '\u001b[' + exports.colors[type] + 'm' + str + '\u001b[0m'; +}; + +/** + * Expose term window size, with some + * defaults for when stderr is not a tty. + */ + +exports.window = { + width: isatty + ? process.stdout.getWindowSize + ? process.stdout.getWindowSize(1)[0] + : tty.getWindowSize()[1] + : 75 +}; + +/** + * Expose some basic cursor interactions + * that are common among reporters. + */ + +exports.cursor = { + hide: function(){ + process.stdout.write('\u001b[?25l'); + }, + + show: function(){ + process.stdout.write('\u001b[?25h'); + }, + + deleteLine: function(){ + process.stdout.write('\u001b[2K'); + }, + + beginningOfLine: function(){ + process.stdout.write('\u001b[0G'); + }, + + CR: function(){ + exports.cursor.deleteLine(); + exports.cursor.beginningOfLine(); + } +}; + +/** + * Outut the given `failures` as a list. + * + * @param {Array} failures + * @api public + */ + +exports.list = function(failures){ + console.error(); + failures.forEach(function(test, i){ + // format + var fmt = color('error title', ' %s) %s:\n') + + color('error message', ' %s') + + color('error stack', '\n%s\n'); + + // msg + var err = test.err + , message = err.message || '' + , stack = err.stack || message + , index = stack.indexOf(message) + message.length + , msg = stack.slice(0, index) + , actual = err.actual + , expected = err.expected + , escape = true; + + // uncaught + if (err.uncaught) { + msg = 'Uncaught ' + msg; + } + + // explicitly show diff + if (err.showDiff && sameType(actual, expected)) { + escape = false; + err.actual = actual = stringify(actual); + err.expected = expected = stringify(expected); + } + + // actual / expected diff + if ('string' == typeof actual && 'string' == typeof expected) { + msg = errorDiff(err, 'Words', escape); + + // linenos + var lines = msg.split('\n'); + if (lines.length > 4) { + var width = String(lines.length).length; + msg = lines.map(function(str, i){ + return pad(++i, width) + ' |' + ' ' + str; + }).join('\n'); + } + + // legend + msg = '\n' + + color('diff removed', 'actual') + + ' ' + + color('diff added', 'expected') + + '\n\n' + + msg + + '\n'; + + // indent + msg = msg.replace(/^/gm, ' '); + + fmt = color('error title', ' %s) %s:\n%s') + + color('error stack', '\n%s\n'); + } + + // indent stack trace without msg + stack = stack.slice(index ? index + 1 : index) + .replace(/^/gm, ' '); + + console.error(fmt, (i + 1), test.fullTitle(), msg, stack); + }); +}; + +/** + * Initialize a new `Base` reporter. + * + * All other reporters generally + * inherit from this reporter, providing + * stats such as test duration, number + * of tests passed / failed etc. + * + * @param {Runner} runner + * @api public + */ + +function Base(runner) { + var self = this + , stats = this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0 } + , failures = this.failures = []; + + if (!runner) return; + this.runner = runner; + + runner.stats = stats; + + runner.on('start', function(){ + stats.start = new Date; + }); + + runner.on('suite', function(suite){ + stats.suites = stats.suites || 0; + suite.root || stats.suites++; + }); + + runner.on('test end', function(test){ + stats.tests = stats.tests || 0; + stats.tests++; + }); + + runner.on('pass', function(test){ + stats.passes = stats.passes || 0; + + var medium = test.slow() / 2; + test.speed = test.duration > test.slow() + ? 'slow' + : test.duration > medium + ? 'medium' + : 'fast'; + + stats.passes++; + }); + + runner.on('fail', function(test, err){ + stats.failures = stats.failures || 0; + stats.failures++; + test.err = err; + failures.push(test); + }); + + runner.on('end', function(){ + stats.end = new Date; + stats.duration = new Date - stats.start; + }); + + runner.on('pending', function(){ + stats.pending++; + }); +} + +/** + * Output common epilogue used by many of + * the bundled reporters. + * + * @api public + */ + +Base.prototype.epilogue = function(){ + var stats = this.stats; + var tests; + var fmt; + + console.log(); + + // passes + fmt = color('bright pass', ' ') + + color('green', ' %d passing') + + color('light', ' (%s)'); + + console.log(fmt, + stats.passes || 0, + ms(stats.duration)); + + // pending + if (stats.pending) { + fmt = color('pending', ' ') + + color('pending', ' %d pending'); + + console.log(fmt, stats.pending); + } + + // failures + if (stats.failures) { + fmt = color('fail', ' %d failing'); + + console.error(fmt, + stats.failures); + + Base.list(this.failures); + console.error(); + } + + console.log(); +}; + +/** + * Pad the given `str` to `len`. + * + * @param {String} str + * @param {String} len + * @return {String} + * @api private + */ + +function pad(str, len) { + str = String(str); + return Array(len - str.length + 1).join(' ') + str; +} + +/** + * Return a character diff for `err`. + * + * @param {Error} err + * @return {String} + * @api private + */ + +function errorDiff(err, type, escape) { + return diff['diff' + type](err.actual, err.expected).map(function(str){ + if (escape) { + str.value = str.value + .replace(/\t/g, '') + .replace(/\r/g, '') + .replace(/\n/g, '\n'); + } + if (str.added) return colorLines('diff added', str.value); + if (str.removed) return colorLines('diff removed', str.value); + return str.value; + }).join(''); +} + +/** + * Color lines for `str`, using the color `name`. + * + * @param {String} name + * @param {String} str + * @return {String} + * @api private + */ + +function colorLines(name, str) { + return str.split('\n').map(function(str){ + return color(name, str); + }).join('\n'); +} + +/** + * Stringify `obj`. + * + * @param {Mixed} obj + * @return {String} + * @api private + */ + +function stringify(obj) { + if (obj instanceof RegExp) return obj.toString(); + return JSON.stringify(obj, null, 2); +} + +/** + * Check that a / b have the same type. + * + * @param {Object} a + * @param {Object} b + * @return {Boolean} + * @api private + */ + +function sameType(a, b) { + a = Object.prototype.toString.call(a); + b = Object.prototype.toString.call(b); + return a == b; +} + +}); // module: reporters/base.js + +require.register("reporters/doc.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , utils = require('../utils'); + +/** + * Expose `Doc`. + */ + +exports = module.exports = Doc; + +/** + * Initialize a new `Doc` reporter. + * + * @param {Runner} runner + * @api public + */ + +function Doc(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , total = runner.total + , indents = 2; + + function indent() { + return Array(indents).join(' '); + } + + runner.on('suite', function(suite){ + if (suite.root) return; + ++indents; + console.log('%s
', indent()); + ++indents; + console.log('%s

%s

', indent(), utils.escape(suite.title)); + console.log('%s
', indent()); + }); + + runner.on('suite end', function(suite){ + if (suite.root) return; + console.log('%s
', indent()); + --indents; + console.log('%s
', indent()); + --indents; + }); + + runner.on('pass', function(test){ + console.log('%s
%s
', indent(), utils.escape(test.title)); + var code = utils.escape(utils.clean(test.fn.toString())); + console.log('%s
%s
', indent(), code); + }); +} + +}); // module: reporters/doc.js + +require.register("reporters/dot.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , color = Base.color; + +/** + * Expose `Dot`. + */ + +exports = module.exports = Dot; + +/** + * Initialize a new `Dot` matrix test reporter. + * + * @param {Runner} runner + * @api public + */ + +function Dot(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , width = Base.window.width * .75 | 0 + , n = 0; + + runner.on('start', function(){ + process.stdout.write('\n '); + }); + + runner.on('pending', function(test){ + process.stdout.write(color('pending', Base.symbols.dot)); + }); + + runner.on('pass', function(test){ + if (++n % width == 0) process.stdout.write('\n '); + if ('slow' == test.speed) { + process.stdout.write(color('bright yellow', Base.symbols.dot)); + } else { + process.stdout.write(color(test.speed, Base.symbols.dot)); + } + }); + + runner.on('fail', function(test, err){ + if (++n % width == 0) process.stdout.write('\n '); + process.stdout.write(color('fail', Base.symbols.dot)); + }); + + runner.on('end', function(){ + console.log(); + self.epilogue(); + }); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +Dot.prototype = new F; +Dot.prototype.constructor = Dot; + +}); // module: reporters/dot.js + +require.register("reporters/html-cov.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var JSONCov = require('./json-cov') + , fs = require('browser/fs'); + +/** + * Expose `HTMLCov`. + */ + +exports = module.exports = HTMLCov; + +/** + * Initialize a new `JsCoverage` reporter. + * + * @param {Runner} runner + * @api public + */ + +function HTMLCov(runner) { + var jade = require('jade') + , file = __dirname + '/templates/coverage.jade' + , str = fs.readFileSync(file, 'utf8') + , fn = jade.compile(str, { filename: file }) + , self = this; + + JSONCov.call(this, runner, false); + + runner.on('end', function(){ + process.stdout.write(fn({ + cov: self.cov + , coverageClass: coverageClass + })); + }); +} + +/** + * Return coverage class for `n`. + * + * @return {String} + * @api private + */ + +function coverageClass(n) { + if (n >= 75) return 'high'; + if (n >= 50) return 'medium'; + if (n >= 25) return 'low'; + return 'terrible'; +} +}); // module: reporters/html-cov.js + +require.register("reporters/html.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , utils = require('../utils') + , Progress = require('../browser/progress') + , escape = utils.escape; + +/** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + +var Date = global.Date + , setTimeout = global.setTimeout + , setInterval = global.setInterval + , clearTimeout = global.clearTimeout + , clearInterval = global.clearInterval; + +/** + * Expose `Doc`. + */ + +exports = module.exports = HTML; + +/** + * Stats template. + */ + +var statsTemplate = ''; + +/** + * Initialize a new `Doc` reporter. + * + * @param {Runner} runner + * @api public + */ + +function HTML(runner, root) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , total = runner.total + , stat = fragment(statsTemplate) + , items = stat.getElementsByTagName('li') + , passes = items[1].getElementsByTagName('em')[0] + , passesLink = items[1].getElementsByTagName('a')[0] + , failures = items[2].getElementsByTagName('em')[0] + , failuresLink = items[2].getElementsByTagName('a')[0] + , duration = items[3].getElementsByTagName('em')[0] + , canvas = stat.getElementsByTagName('canvas')[0] + , report = fragment('
    ') + , stack = [report] + , progress + , ctx + + root = root || document.getElementById('mocha'); + + if (canvas.getContext) { + var ratio = window.devicePixelRatio || 1; + canvas.style.width = canvas.width; + canvas.style.height = canvas.height; + canvas.width *= ratio; + canvas.height *= ratio; + ctx = canvas.getContext('2d'); + ctx.scale(ratio, ratio); + progress = new Progress; + } + + if (!root) return error('#mocha div missing, add it to your document'); + + // pass toggle + on(passesLink, 'click', function(){ + unhide(); + var name = /pass/.test(report.className) ? '' : ' pass'; + report.className = report.className.replace(/fail|pass/g, '') + name; + if (report.className.trim()) hideSuitesWithout('test pass'); + }); + + // failure toggle + on(failuresLink, 'click', function(){ + unhide(); + var name = /fail/.test(report.className) ? '' : ' fail'; + report.className = report.className.replace(/fail|pass/g, '') + name; + if (report.className.trim()) hideSuitesWithout('test fail'); + }); + + root.appendChild(stat); + root.appendChild(report); + + if (progress) progress.size(40); + + runner.on('suite', function(suite){ + if (suite.root) return; + + // suite + var url = '?grep=' + encodeURIComponent(suite.fullTitle()); + var el = fragment('
  • %s

  • ', url, escape(suite.title)); + + // container + stack[0].appendChild(el); + stack.unshift(document.createElement('ul')); + el.appendChild(stack[0]); + }); + + runner.on('suite end', function(suite){ + if (suite.root) return; + stack.shift(); + }); + + runner.on('fail', function(test, err){ + if ('hook' == test.type) runner.emit('test end', test); + }); + + runner.on('test end', function(test){ + // TODO: add to stats + var percent = stats.tests / this.total * 100 | 0; + if (progress) progress.update(percent).draw(ctx); + + // update stats + var ms = new Date - stats.start; + text(passes, stats.passes); + text(failures, stats.failures); + text(duration, (ms / 1000).toFixed(2)); + + // test + if ('passed' == test.state) { + var el = fragment('
  • %e%ems

  • ', test.speed, test.title, test.duration, encodeURIComponent(test.fullTitle())); + } else if (test.pending) { + var el = fragment('
  • %e

  • ', test.title); + } else { + var el = fragment('
  • %e

  • ', test.title, encodeURIComponent(test.fullTitle())); + var str = test.err.stack || test.err.toString(); + + // FF / Opera do not add the message + if (!~str.indexOf(test.err.message)) { + str = test.err.message + '\n' + str; + } + + // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we + // check for the result of the stringifying. + if ('[object Error]' == str) str = test.err.message; + + // Safari doesn't give you a stack. Let's at least provide a source line. + if (!test.err.stack && test.err.sourceURL && test.err.line !== undefined) { + str += "\n(" + test.err.sourceURL + ":" + test.err.line + ")"; + } + + el.appendChild(fragment('
    %e
    ', str)); + } + + // toggle code + // TODO: defer + if (!test.pending) { + var h2 = el.getElementsByTagName('h2')[0]; + + on(h2, 'click', function(){ + pre.style.display = 'none' == pre.style.display + ? 'block' + : 'none'; + }); + + var pre = fragment('
    %e
    ', utils.clean(test.fn.toString())); + el.appendChild(pre); + pre.style.display = 'none'; + } + + // Don't call .appendChild if #mocha-report was already .shift()'ed off the stack. + if (stack[0]) stack[0].appendChild(el); + }); +} + +/** + * Display error `msg`. + */ + +function error(msg) { + document.body.appendChild(fragment('
    %s
    ', msg)); +} + +/** + * Return a DOM fragment from `html`. + */ + +function fragment(html) { + var args = arguments + , div = document.createElement('div') + , i = 1; + + div.innerHTML = html.replace(/%([se])/g, function(_, type){ + switch (type) { + case 's': return String(args[i++]); + case 'e': return escape(args[i++]); + } + }); + + return div.firstChild; +} + +/** + * Check for suites that do not have elements + * with `classname`, and hide them. + */ + +function hideSuitesWithout(classname) { + var suites = document.getElementsByClassName('suite'); + for (var i = 0; i < suites.length; i++) { + var els = suites[i].getElementsByClassName(classname); + if (0 == els.length) suites[i].className += ' hidden'; + } +} + +/** + * Unhide .hidden suites. + */ + +function unhide() { + var els = document.getElementsByClassName('suite hidden'); + for (var i = 0; i < els.length; ++i) { + els[i].className = els[i].className.replace('suite hidden', 'suite'); + } +} + +/** + * Set `el` text to `str`. + */ + +function text(el, str) { + if (el.textContent) { + el.textContent = str; + } else { + el.innerText = str; + } +} + +/** + * Listen on `event` with callback `fn`. + */ + +function on(el, event, fn) { + if (el.addEventListener) { + el.addEventListener(event, fn, false); + } else { + el.attachEvent('on' + event, fn); + } +} + +}); // module: reporters/html.js + +require.register("reporters/index.js", function(module, exports, require){ + +exports.Base = require('./base'); +exports.Dot = require('./dot'); +exports.Doc = require('./doc'); +exports.TAP = require('./tap'); +exports.JSON = require('./json'); +exports.HTML = require('./html'); +exports.List = require('./list'); +exports.Min = require('./min'); +exports.Spec = require('./spec'); +exports.Nyan = require('./nyan'); +exports.XUnit = require('./xunit'); +exports.Markdown = require('./markdown'); +exports.Progress = require('./progress'); +exports.Landing = require('./landing'); +exports.JSONCov = require('./json-cov'); +exports.HTMLCov = require('./html-cov'); +exports.JSONStream = require('./json-stream'); +exports.Teamcity = require('./teamcity'); + +}); // module: reporters/index.js + +require.register("reporters/json-cov.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base'); + +/** + * Expose `JSONCov`. + */ + +exports = module.exports = JSONCov; + +/** + * Initialize a new `JsCoverage` reporter. + * + * @param {Runner} runner + * @param {Boolean} output + * @api public + */ + +function JSONCov(runner, output) { + var self = this + , output = 1 == arguments.length ? true : output; + + Base.call(this, runner); + + var tests = [] + , failures = [] + , passes = []; + + runner.on('test end', function(test){ + tests.push(test); + }); + + runner.on('pass', function(test){ + passes.push(test); + }); + + runner.on('fail', function(test){ + failures.push(test); + }); + + runner.on('end', function(){ + var cov = global._$jscoverage || {}; + var result = self.cov = map(cov); + result.stats = self.stats; + result.tests = tests.map(clean); + result.failures = failures.map(clean); + result.passes = passes.map(clean); + if (!output) return; + process.stdout.write(JSON.stringify(result, null, 2 )); + }); +} + +/** + * Map jscoverage data to a JSON structure + * suitable for reporting. + * + * @param {Object} cov + * @return {Object} + * @api private + */ + +function map(cov) { + var ret = { + instrumentation: 'node-jscoverage' + , sloc: 0 + , hits: 0 + , misses: 0 + , coverage: 0 + , files: [] + }; + + for (var filename in cov) { + var data = coverage(filename, cov[filename]); + ret.files.push(data); + ret.hits += data.hits; + ret.misses += data.misses; + ret.sloc += data.sloc; + } + + ret.files.sort(function(a, b) { + return a.filename.localeCompare(b.filename); + }); + + if (ret.sloc > 0) { + ret.coverage = (ret.hits / ret.sloc) * 100; + } + + return ret; +}; + +/** + * Map jscoverage data for a single source file + * to a JSON structure suitable for reporting. + * + * @param {String} filename name of the source file + * @param {Object} data jscoverage coverage data + * @return {Object} + * @api private + */ + +function coverage(filename, data) { + var ret = { + filename: filename, + coverage: 0, + hits: 0, + misses: 0, + sloc: 0, + source: {} + }; + + data.source.forEach(function(line, num){ + num++; + + if (data[num] === 0) { + ret.misses++; + ret.sloc++; + } else if (data[num] !== undefined) { + ret.hits++; + ret.sloc++; + } + + ret.source[num] = { + source: line + , coverage: data[num] === undefined + ? '' + : data[num] + }; + }); + + ret.coverage = ret.hits / ret.sloc * 100; + + return ret; +} + +/** + * Return a plain-object representation of `test` + * free of cyclic properties etc. + * + * @param {Object} test + * @return {Object} + * @api private + */ + +function clean(test) { + return { + title: test.title + , fullTitle: test.fullTitle() + , duration: test.duration + } +} + +}); // module: reporters/json-cov.js + +require.register("reporters/json-stream.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , color = Base.color; + +/** + * Expose `List`. + */ + +exports = module.exports = List; + +/** + * Initialize a new `List` test reporter. + * + * @param {Runner} runner + * @api public + */ + +function List(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , total = runner.total; + + runner.on('start', function(){ + console.log(JSON.stringify(['start', { total: total }])); + }); + + runner.on('pass', function(test){ + console.log(JSON.stringify(['pass', clean(test)])); + }); + + runner.on('fail', function(test, err){ + console.log(JSON.stringify(['fail', clean(test)])); + }); + + runner.on('end', function(){ + process.stdout.write(JSON.stringify(['end', self.stats])); + }); +} + +/** + * Return a plain-object representation of `test` + * free of cyclic properties etc. + * + * @param {Object} test + * @return {Object} + * @api private + */ + +function clean(test) { + return { + title: test.title + , fullTitle: test.fullTitle() + , duration: test.duration + } +} +}); // module: reporters/json-stream.js + +require.register("reporters/json.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `JSON`. + */ + +exports = module.exports = JSONReporter; + +/** + * Initialize a new `JSON` reporter. + * + * @param {Runner} runner + * @api public + */ + +function JSONReporter(runner) { + var self = this; + Base.call(this, runner); + + var tests = [] + , failures = [] + , passes = []; + + runner.on('test end', function(test){ + tests.push(test); + }); + + runner.on('pass', function(test){ + passes.push(test); + }); + + runner.on('fail', function(test){ + failures.push(test); + }); + + runner.on('end', function(){ + var obj = { + stats: self.stats + , tests: tests.map(clean) + , failures: failures.map(clean) + , passes: passes.map(clean) + }; + + process.stdout.write(JSON.stringify(obj, null, 2)); + }); +} + +/** + * Return a plain-object representation of `test` + * free of cyclic properties etc. + * + * @param {Object} test + * @return {Object} + * @api private + */ + +function clean(test) { + return { + title: test.title + , fullTitle: test.fullTitle() + , duration: test.duration + } +} +}); // module: reporters/json.js + +require.register("reporters/landing.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `Landing`. + */ + +exports = module.exports = Landing; + +/** + * Airplane color. + */ + +Base.colors.plane = 0; + +/** + * Airplane crash color. + */ + +Base.colors['plane crash'] = 31; + +/** + * Runway color. + */ + +Base.colors.runway = 90; + +/** + * Initialize a new `Landing` reporter. + * + * @param {Runner} runner + * @api public + */ + +function Landing(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , width = Base.window.width * .75 | 0 + , total = runner.total + , stream = process.stdout + , plane = color('plane', '✈') + , crashed = -1 + , n = 0; + + function runway() { + var buf = Array(width).join('-'); + return ' ' + color('runway', buf); + } + + runner.on('start', function(){ + stream.write('\n '); + cursor.hide(); + }); + + runner.on('test end', function(test){ + // check if the plane crashed + var col = -1 == crashed + ? width * ++n / total | 0 + : crashed; + + // show the crash + if ('failed' == test.state) { + plane = color('plane crash', '✈'); + crashed = col; + } + + // render landing strip + stream.write('\u001b[4F\n\n'); + stream.write(runway()); + stream.write('\n '); + stream.write(color('runway', Array(col).join('⋅'))); + stream.write(plane) + stream.write(color('runway', Array(width - col).join('⋅') + '\n')); + stream.write(runway()); + stream.write('\u001b[0m'); + }); + + runner.on('end', function(){ + cursor.show(); + console.log(); + self.epilogue(); + }); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +Landing.prototype = new F; +Landing.prototype.constructor = Landing; + +}); // module: reporters/landing.js + +require.register("reporters/list.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `List`. + */ + +exports = module.exports = List; + +/** + * Initialize a new `List` test reporter. + * + * @param {Runner} runner + * @api public + */ + +function List(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , n = 0; + + runner.on('start', function(){ + console.log(); + }); + + runner.on('test', function(test){ + process.stdout.write(color('pass', ' ' + test.fullTitle() + ': ')); + }); + + runner.on('pending', function(test){ + var fmt = color('checkmark', ' -') + + color('pending', ' %s'); + console.log(fmt, test.fullTitle()); + }); + + runner.on('pass', function(test){ + var fmt = color('checkmark', ' '+Base.symbols.dot) + + color('pass', ' %s: ') + + color(test.speed, '%dms'); + cursor.CR(); + console.log(fmt, test.fullTitle(), test.duration); + }); + + runner.on('fail', function(test, err){ + cursor.CR(); + console.log(color('fail', ' %d) %s'), ++n, test.fullTitle()); + }); + + runner.on('end', self.epilogue.bind(self)); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +List.prototype = new F; +List.prototype.constructor = List; + + +}); // module: reporters/list.js + +require.register("reporters/markdown.js", function(module, exports, require){ +/** + * Module dependencies. + */ + +var Base = require('./base') + , utils = require('../utils'); + +/** + * Expose `Markdown`. + */ + +exports = module.exports = Markdown; + +/** + * Initialize a new `Markdown` reporter. + * + * @param {Runner} runner + * @api public + */ + +function Markdown(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , level = 0 + , buf = ''; + + function title(str) { + return Array(level).join('#') + ' ' + str; + } + + function indent() { + return Array(level).join(' '); + } + + function mapTOC(suite, obj) { + var ret = obj; + obj = obj[suite.title] = obj[suite.title] || { suite: suite }; + suite.suites.forEach(function(suite){ + mapTOC(suite, obj); + }); + return ret; + } + + function stringifyTOC(obj, level) { + ++level; + var buf = ''; + var link; + for (var key in obj) { + if ('suite' == key) continue; + if (key) link = ' - [' + key + '](#' + utils.slug(obj[key].suite.fullTitle()) + ')\n'; + if (key) buf += Array(level).join(' ') + link; + buf += stringifyTOC(obj[key], level); + } + --level; + return buf; + } + + function generateTOC(suite) { + var obj = mapTOC(suite, {}); + return stringifyTOC(obj, 0); + } + + generateTOC(runner.suite); + + runner.on('suite', function(suite){ + ++level; + var slug = utils.slug(suite.fullTitle()); + buf += '' + '\n'; + buf += title(suite.title) + '\n'; + }); + + runner.on('suite end', function(suite){ + --level; + }); + + runner.on('pass', function(test){ + var code = utils.clean(test.fn.toString()); + buf += test.title + '.\n'; + buf += '\n```js\n'; + buf += code + '\n'; + buf += '```\n\n'; + }); + + runner.on('end', function(){ + process.stdout.write('# TOC\n'); + process.stdout.write(generateTOC(runner.suite)); + process.stdout.write(buf); + }); +} +}); // module: reporters/markdown.js + +require.register("reporters/min.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base'); + +/** + * Expose `Min`. + */ + +exports = module.exports = Min; + +/** + * Initialize a new `Min` minimal test reporter (best used with --watch). + * + * @param {Runner} runner + * @api public + */ + +function Min(runner) { + Base.call(this, runner); + + runner.on('start', function(){ + // clear screen + process.stdout.write('\u001b[2J'); + // set cursor position + process.stdout.write('\u001b[1;3H'); + }); + + runner.on('end', this.epilogue.bind(this)); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +Min.prototype = new F; +Min.prototype.constructor = Min; + + +}); // module: reporters/min.js + +require.register("reporters/nyan.js", function(module, exports, require){ +/** + * Module dependencies. + */ + +var Base = require('./base') + , color = Base.color; + +/** + * Expose `Dot`. + */ + +exports = module.exports = NyanCat; + +/** + * Initialize a new `Dot` matrix test reporter. + * + * @param {Runner} runner + * @api public + */ + +function NyanCat(runner) { + Base.call(this, runner); + var self = this + , stats = this.stats + , width = Base.window.width * .75 | 0 + , rainbowColors = this.rainbowColors = self.generateColors() + , colorIndex = this.colorIndex = 0 + , numerOfLines = this.numberOfLines = 4 + , trajectories = this.trajectories = [[], [], [], []] + , nyanCatWidth = this.nyanCatWidth = 11 + , trajectoryWidthMax = this.trajectoryWidthMax = (width - nyanCatWidth) + , scoreboardWidth = this.scoreboardWidth = 5 + , tick = this.tick = 0 + , n = 0; + + runner.on('start', function(){ + Base.cursor.hide(); + self.draw(); + }); + + runner.on('pending', function(test){ + self.draw(); + }); + + runner.on('pass', function(test){ + self.draw(); + }); + + runner.on('fail', function(test, err){ + self.draw(); + }); + + runner.on('end', function(){ + Base.cursor.show(); + for (var i = 0; i < self.numberOfLines; i++) write('\n'); + self.epilogue(); + }); +} + +/** + * Draw the nyan cat + * + * @api private + */ + +NyanCat.prototype.draw = function(){ + this.appendRainbow(); + this.drawScoreboard(); + this.drawRainbow(); + this.drawNyanCat(); + this.tick = !this.tick; +}; + +/** + * Draw the "scoreboard" showing the number + * of passes, failures and pending tests. + * + * @api private + */ + +NyanCat.prototype.drawScoreboard = function(){ + var stats = this.stats; + var colors = Base.colors; + + function draw(color, n) { + write(' '); + write('\u001b[' + color + 'm' + n + '\u001b[0m'); + write('\n'); + } + + draw(colors.green, stats.passes); + draw(colors.fail, stats.failures); + draw(colors.pending, stats.pending); + write('\n'); + + this.cursorUp(this.numberOfLines); +}; + +/** + * Append the rainbow. + * + * @api private + */ + +NyanCat.prototype.appendRainbow = function(){ + var segment = this.tick ? '_' : '-'; + var rainbowified = this.rainbowify(segment); + + for (var index = 0; index < this.numberOfLines; index++) { + var trajectory = this.trajectories[index]; + if (trajectory.length >= this.trajectoryWidthMax) trajectory.shift(); + trajectory.push(rainbowified); + } +}; + +/** + * Draw the rainbow. + * + * @api private + */ + +NyanCat.prototype.drawRainbow = function(){ + var self = this; + + this.trajectories.forEach(function(line, index) { + write('\u001b[' + self.scoreboardWidth + 'C'); + write(line.join('')); + write('\n'); + }); + + this.cursorUp(this.numberOfLines); +}; + +/** + * Draw the nyan cat + * + * @api private + */ + +NyanCat.prototype.drawNyanCat = function() { + var self = this; + var startWidth = this.scoreboardWidth + this.trajectories[0].length; + var color = '\u001b[' + startWidth + 'C'; + var padding = ''; + + write(color); + write('_,------,'); + write('\n'); + + write(color); + padding = self.tick ? ' ' : ' '; + write('_|' + padding + '/\\_/\\ '); + write('\n'); + + write(color); + padding = self.tick ? '_' : '__'; + var tail = self.tick ? '~' : '^'; + var face; + write(tail + '|' + padding + this.face() + ' '); + write('\n'); + + write(color); + padding = self.tick ? ' ' : ' '; + write(padding + '"" "" '); + write('\n'); + + this.cursorUp(this.numberOfLines); +}; + +/** + * Draw nyan cat face. + * + * @return {String} + * @api private + */ + +NyanCat.prototype.face = function() { + var stats = this.stats; + if (stats.failures) { + return '( x .x)'; + } else if (stats.pending) { + return '( o .o)'; + } else if(stats.passes) { + return '( ^ .^)'; + } else { + return '( - .-)'; + } +} + +/** + * Move cursor up `n`. + * + * @param {Number} n + * @api private + */ + +NyanCat.prototype.cursorUp = function(n) { + write('\u001b[' + n + 'A'); +}; + +/** + * Move cursor down `n`. + * + * @param {Number} n + * @api private + */ + +NyanCat.prototype.cursorDown = function(n) { + write('\u001b[' + n + 'B'); +}; + +/** + * Generate rainbow colors. + * + * @return {Array} + * @api private + */ + +NyanCat.prototype.generateColors = function(){ + var colors = []; + + for (var i = 0; i < (6 * 7); i++) { + var pi3 = Math.floor(Math.PI / 3); + var n = (i * (1.0 / 6)); + var r = Math.floor(3 * Math.sin(n) + 3); + var g = Math.floor(3 * Math.sin(n + 2 * pi3) + 3); + var b = Math.floor(3 * Math.sin(n + 4 * pi3) + 3); + colors.push(36 * r + 6 * g + b + 16); + } + + return colors; +}; + +/** + * Apply rainbow to the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + +NyanCat.prototype.rainbowify = function(str){ + var color = this.rainbowColors[this.colorIndex % this.rainbowColors.length]; + this.colorIndex += 1; + return '\u001b[38;5;' + color + 'm' + str + '\u001b[0m'; +}; + +/** + * Stdout helper. + */ + +function write(string) { + process.stdout.write(string); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +NyanCat.prototype = new F; +NyanCat.prototype.constructor = NyanCat; + + +}); // module: reporters/nyan.js + +require.register("reporters/progress.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `Progress`. + */ + +exports = module.exports = Progress; + +/** + * General progress bar color. + */ + +Base.colors.progress = 90; + +/** + * Initialize a new `Progress` bar test reporter. + * + * @param {Runner} runner + * @param {Object} options + * @api public + */ + +function Progress(runner, options) { + Base.call(this, runner); + + var self = this + , options = options || {} + , stats = this.stats + , width = Base.window.width * .50 | 0 + , total = runner.total + , complete = 0 + , max = Math.max; + + // default chars + options.open = options.open || '['; + options.complete = options.complete || '▬'; + options.incomplete = options.incomplete || Base.symbols.dot; + options.close = options.close || ']'; + options.verbose = false; + + // tests started + runner.on('start', function(){ + console.log(); + cursor.hide(); + }); + + // tests complete + runner.on('test end', function(){ + complete++; + var incomplete = total - complete + , percent = complete / total + , n = width * percent | 0 + , i = width - n; + + cursor.CR(); + process.stdout.write('\u001b[J'); + process.stdout.write(color('progress', ' ' + options.open)); + process.stdout.write(Array(n).join(options.complete)); + process.stdout.write(Array(i).join(options.incomplete)); + process.stdout.write(color('progress', options.close)); + if (options.verbose) { + process.stdout.write(color('progress', ' ' + complete + ' of ' + total)); + } + }); + + // tests are complete, output some stats + // and the failures if any + runner.on('end', function(){ + cursor.show(); + console.log(); + self.epilogue(); + }); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +Progress.prototype = new F; +Progress.prototype.constructor = Progress; + + +}); // module: reporters/progress.js + +require.register("reporters/spec.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `Spec`. + */ + +exports = module.exports = Spec; + +/** + * Initialize a new `Spec` test reporter. + * + * @param {Runner} runner + * @api public + */ + +function Spec(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , indents = 0 + , n = 0; + + function indent() { + return Array(indents).join(' ') + } + + runner.on('start', function(){ + console.log(); + }); + + runner.on('suite', function(suite){ + ++indents; + console.log(color('suite', '%s%s'), indent(), suite.title); + }); + + runner.on('suite end', function(suite){ + --indents; + if (1 == indents) console.log(); + }); + + runner.on('test', function(test){ + process.stdout.write(indent() + color('pass', ' ◦ ' + test.title + ': ')); + }); + + runner.on('pending', function(test){ + var fmt = indent() + color('pending', ' - %s'); + console.log(fmt, test.title); + }); + + runner.on('pass', function(test){ + if ('fast' == test.speed) { + var fmt = indent() + + color('checkmark', ' ' + Base.symbols.ok) + + color('pass', ' %s '); + cursor.CR(); + console.log(fmt, test.title); + } else { + var fmt = indent() + + color('checkmark', ' ' + Base.symbols.ok) + + color('pass', ' %s ') + + color(test.speed, '(%dms)'); + cursor.CR(); + console.log(fmt, test.title, test.duration); + } + }); + + runner.on('fail', function(test, err){ + cursor.CR(); + console.log(indent() + color('fail', ' %d) %s'), ++n, test.title); + }); + + runner.on('end', self.epilogue.bind(self)); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +Spec.prototype = new F; +Spec.prototype.constructor = Spec; + + +}); // module: reporters/spec.js + +require.register("reporters/tap.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `TAP`. + */ + +exports = module.exports = TAP; + +/** + * Initialize a new `TAP` reporter. + * + * @param {Runner} runner + * @api public + */ + +function TAP(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , n = 1 + , passes = 0 + , failures = 0; + + runner.on('start', function(){ + var total = runner.grepTotal(runner.suite); + console.log('%d..%d', 1, total); + }); + + runner.on('test end', function(){ + ++n; + }); + + runner.on('pending', function(test){ + console.log('ok %d %s # SKIP -', n, title(test)); + }); + + runner.on('pass', function(test){ + passes++; + console.log('ok %d %s', n, title(test)); + }); + + runner.on('fail', function(test, err){ + failures++; + console.log('not ok %d %s', n, title(test)); + if (err.stack) console.log(err.stack.replace(/^/gm, ' ')); + }); + + runner.on('end', function(){ + console.log('# tests ' + (passes + failures)); + console.log('# pass ' + passes); + console.log('# fail ' + failures); + }); +} + +/** + * Return a TAP-safe title of `test` + * + * @param {Object} test + * @return {String} + * @api private + */ + +function title(test) { + return test.fullTitle().replace(/#/g, ''); +} + +}); // module: reporters/tap.js + +require.register("reporters/teamcity.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base'); + +/** + * Expose `Teamcity`. + */ + +exports = module.exports = Teamcity; + +/** + * Initialize a new `Teamcity` reporter. + * + * @param {Runner} runner + * @api public + */ + +function Teamcity(runner) { + Base.call(this, runner); + var stats = this.stats; + + runner.on('start', function() { + console.log("##teamcity[testSuiteStarted name='mocha.suite']"); + }); + + runner.on('test', function(test) { + console.log("##teamcity[testStarted name='" + escape(test.fullTitle()) + "']"); + }); + + runner.on('fail', function(test, err) { + console.log("##teamcity[testFailed name='" + escape(test.fullTitle()) + "' message='" + escape(err.message) + "']"); + }); + + runner.on('pending', function(test) { + console.log("##teamcity[testIgnored name='" + escape(test.fullTitle()) + "' message='pending']"); + }); + + runner.on('test end', function(test) { + console.log("##teamcity[testFinished name='" + escape(test.fullTitle()) + "' duration='" + test.duration + "']"); + }); + + runner.on('end', function() { + console.log("##teamcity[testSuiteFinished name='mocha.suite' duration='" + stats.duration + "']"); + }); +} + +/** + * Escape the given `str`. + */ + +function escape(str) { + return str + .replace(/\|/g, "||") + .replace(/\n/g, "|n") + .replace(/\r/g, "|r") + .replace(/\[/g, "|[") + .replace(/\]/g, "|]") + .replace(/\u0085/g, "|x") + .replace(/\u2028/g, "|l") + .replace(/\u2029/g, "|p") + .replace(/'/g, "|'"); +} + +}); // module: reporters/teamcity.js + +require.register("reporters/xunit.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , utils = require('../utils') + , escape = utils.escape; + +/** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + +var Date = global.Date + , setTimeout = global.setTimeout + , setInterval = global.setInterval + , clearTimeout = global.clearTimeout + , clearInterval = global.clearInterval; + +/** + * Expose `XUnit`. + */ + +exports = module.exports = XUnit; + +/** + * Initialize a new `XUnit` reporter. + * + * @param {Runner} runner + * @api public + */ + +function XUnit(runner) { + Base.call(this, runner); + var stats = this.stats + , tests = [] + , self = this; + + runner.on('pass', function(test){ + tests.push(test); + }); + + runner.on('fail', function(test){ + tests.push(test); + }); + + runner.on('end', function(){ + console.log(tag('testsuite', { + name: 'Mocha Tests' + , tests: stats.tests + , failures: stats.failures + , errors: stats.failures + , skipped: stats.tests - stats.failures - stats.passes + , timestamp: (new Date).toUTCString() + , time: (stats.duration / 1000) || 0 + }, false)); + + tests.forEach(test); + console.log(''); + }); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +XUnit.prototype = new F; +XUnit.prototype.constructor = XUnit; + + +/** + * Output tag for the given `test.` + */ + +function test(test) { + var attrs = { + classname: test.parent.fullTitle() + , name: test.title + , time: test.duration / 1000 + }; + + if ('failed' == test.state) { + var err = test.err; + attrs.message = escape(err.message); + console.log(tag('testcase', attrs, false, tag('failure', attrs, false, cdata(err.stack)))); + } else if (test.pending) { + console.log(tag('testcase', attrs, false, tag('skipped', {}, true))); + } else { + console.log(tag('testcase', attrs, true) ); + } +} + +/** + * HTML tag helper. + */ + +function tag(name, attrs, close, content) { + var end = close ? '/>' : '>' + , pairs = [] + , tag; + + for (var key in attrs) { + pairs.push(key + '="' + escape(attrs[key]) + '"'); + } + + tag = '<' + name + (pairs.length ? ' ' + pairs.join(' ') : '') + end; + if (content) tag += content + ''; +} + +}); // module: reporters/xunit.js + +require.register("runnable.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var EventEmitter = require('browser/events').EventEmitter + , debug = require('browser/debug')('mocha:runnable') + , milliseconds = require('./ms'); + +/** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + +var Date = global.Date + , setTimeout = global.setTimeout + , setInterval = global.setInterval + , clearTimeout = global.clearTimeout + , clearInterval = global.clearInterval; + +/** + * Object#toString(). + */ + +var toString = Object.prototype.toString; + +/** + * Expose `Runnable`. + */ + +module.exports = Runnable; + +/** + * Initialize a new `Runnable` with the given `title` and callback `fn`. + * + * @param {String} title + * @param {Function} fn + * @api private + */ + +function Runnable(title, fn) { + this.title = title; + this.fn = fn; + this.async = fn && fn.length; + this.sync = ! this.async; + this._timeout = 2000; + this._slow = 75; + this.timedOut = false; +} + +/** + * Inherit from `EventEmitter.prototype`. + */ + +function F(){}; +F.prototype = EventEmitter.prototype; +Runnable.prototype = new F; +Runnable.prototype.constructor = Runnable; + + +/** + * Set & get timeout `ms`. + * + * @param {Number|String} ms + * @return {Runnable|Number} ms or self + * @api private + */ + +Runnable.prototype.timeout = function(ms){ + if (0 == arguments.length) return this._timeout; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('timeout %d', ms); + this._timeout = ms; + if (this.timer) this.resetTimeout(); + return this; +}; + +/** + * Set & get slow `ms`. + * + * @param {Number|String} ms + * @return {Runnable|Number} ms or self + * @api private + */ + +Runnable.prototype.slow = function(ms){ + if (0 === arguments.length) return this._slow; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('timeout %d', ms); + this._slow = ms; + return this; +}; + +/** + * Return the full title generated by recursively + * concatenating the parent's full title. + * + * @return {String} + * @api public + */ + +Runnable.prototype.fullTitle = function(){ + return this.parent.fullTitle() + ' ' + this.title; +}; + +/** + * Clear the timeout. + * + * @api private + */ + +Runnable.prototype.clearTimeout = function(){ + clearTimeout(this.timer); +}; + +/** + * Inspect the runnable void of private properties. + * + * @return {String} + * @api private + */ + +Runnable.prototype.inspect = function(){ + return JSON.stringify(this, function(key, val){ + if ('_' == key[0]) return; + if ('parent' == key) return '#'; + if ('ctx' == key) return '#'; + return val; + }, 2); +}; + +/** + * Reset the timeout. + * + * @api private + */ + +Runnable.prototype.resetTimeout = function(){ + var self = this; + var ms = this.timeout() || 1e9; + + this.clearTimeout(); + this.timer = setTimeout(function(){ + self.callback(new Error('timeout of ' + ms + 'ms exceeded')); + self.timedOut = true; + }, ms); +}; + +/** + * Run the test and invoke `fn(err)`. + * + * @param {Function} fn + * @api private + */ + +Runnable.prototype.run = function(fn){ + var self = this + , ms = this.timeout() + , start = new Date + , ctx = this.ctx + , finished + , emitted; + + if (ctx) ctx.runnable(this); + + // timeout + if (this.async) { + if (ms) { + this.timer = setTimeout(function(){ + done(new Error('timeout of ' + ms + 'ms exceeded')); + self.timedOut = true; + }, ms); + } + } + + // called multiple times + function multiple(err) { + if (emitted) return; + emitted = true; + self.emit('error', err || new Error('done() called multiple times')); + } + + // finished + function done(err) { + if (self.timedOut) return; + if (finished) return multiple(err); + self.clearTimeout(); + self.duration = new Date - start; + finished = true; + fn(err); + } + + // for .resetTimeout() + this.callback = done; + + // async + if (this.async) { + try { + this.fn.call(ctx, function(err){ + if (err instanceof Error || toString.call(err) === "[object Error]") return done(err); + if (null != err) return done(new Error('done() invoked with non-Error: ' + err)); + done(); + }); + } catch (err) { + done(err); + } + return; + } + + if (this.asyncOnly) { + return done(new Error('--async-only option in use without declaring `done()`')); + } + + // sync + try { + if (!this.pending) this.fn.call(ctx); + this.duration = new Date - start; + fn(); + } catch (err) { + fn(err); + } +}; + +}); // module: runnable.js + +require.register("runner.js", function(module, exports, require){ +/** + * Module dependencies. + */ + +var EventEmitter = require('browser/events').EventEmitter + , debug = require('browser/debug')('mocha:runner') + , Test = require('./test') + , utils = require('./utils') + , filter = utils.filter + , keys = utils.keys; + +/** + * Non-enumerable globals. + */ + +var globals = [ + 'setTimeout', + 'clearTimeout', + 'setInterval', + 'clearInterval', + 'XMLHttpRequest', + 'Date' +]; + +/** + * Expose `Runner`. + */ + +module.exports = Runner; + +/** + * Initialize a `Runner` for the given `suite`. + * + * Events: + * + * - `start` execution started + * - `end` execution complete + * - `suite` (suite) test suite execution started + * - `suite end` (suite) all tests (and sub-suites) have finished + * - `test` (test) test execution started + * - `test end` (test) test completed + * - `hook` (hook) hook execution started + * - `hook end` (hook) hook complete + * - `pass` (test) test passed + * - `fail` (test, err) test failed + * - `pending` (test) test pending + * + * @api public + */ + +function Runner(suite) { + var self = this; + this._globals = []; + this.suite = suite; + this.total = suite.total(); + this.failures = 0; + this.on('test end', function(test){ self.checkGlobals(test); }); + this.on('hook end', function(hook){ self.checkGlobals(hook); }); + this.grep(/.*/); + this.globals(this.globalProps().concat(['errno'])); +} + +/** + * Wrapper for setImmediate, process.nextTick, or browser polyfill. + * + * @param {Function} fn + * @api private + */ + +Runner.immediately = global.setImmediate || process.nextTick; + +/** + * Inherit from `EventEmitter.prototype`. + */ + +function F(){}; +F.prototype = EventEmitter.prototype; +Runner.prototype = new F; +Runner.prototype.constructor = Runner; + + +/** + * Run tests with full titles matching `re`. Updates runner.total + * with number of tests matched. + * + * @param {RegExp} re + * @param {Boolean} invert + * @return {Runner} for chaining + * @api public + */ + +Runner.prototype.grep = function(re, invert){ + debug('grep %s', re); + this._grep = re; + this._invert = invert; + this.total = this.grepTotal(this.suite); + return this; +}; + +/** + * Returns the number of tests matching the grep search for the + * given suite. + * + * @param {Suite} suite + * @return {Number} + * @api public + */ + +Runner.prototype.grepTotal = function(suite) { + var self = this; + var total = 0; + + suite.eachTest(function(test){ + var match = self._grep.test(test.fullTitle()); + if (self._invert) match = !match; + if (match) total++; + }); + + return total; +}; + +/** + * Return a list of global properties. + * + * @return {Array} + * @api private + */ + +Runner.prototype.globalProps = function() { + var props = utils.keys(global); + + // non-enumerables + for (var i = 0; i < globals.length; ++i) { + if (~utils.indexOf(props, globals[i])) continue; + props.push(globals[i]); + } + + return props; +}; + +/** + * Allow the given `arr` of globals. + * + * @param {Array} arr + * @return {Runner} for chaining + * @api public + */ + +Runner.prototype.globals = function(arr){ + if (0 == arguments.length) return this._globals; + debug('globals %j', arr); + utils.forEach(arr, function(arr){ + this._globals.push(arr); + }, this); + return this; +}; + +/** + * Check for global variable leaks. + * + * @api private + */ + +Runner.prototype.checkGlobals = function(test){ + if (this.ignoreLeaks) return; + var ok = this._globals; + var globals = this.globalProps(); + var isNode = process.kill; + var leaks; + + // check length - 2 ('errno' and 'location' globals) + if (isNode && 1 == ok.length - globals.length) return + else if (2 == ok.length - globals.length) return; + + leaks = filterLeaks(ok, globals); + this._globals = this._globals.concat(leaks); + + if (leaks.length > 1) { + this.fail(test, new Error('global leaks detected: ' + leaks.join(', ') + '')); + } else if (leaks.length) { + this.fail(test, new Error('global leak detected: ' + leaks[0])); + } +}; + +/** + * Fail the given `test`. + * + * @param {Test} test + * @param {Error} err + * @api private + */ + +Runner.prototype.fail = function(test, err){ + ++this.failures; + test.state = 'failed'; + + if ('string' == typeof err) { + err = new Error('the string "' + err + '" was thrown, throw an Error :)'); + } + + this.emit('fail', test, err); +}; + +/** + * Fail the given `hook` with `err`. + * + * Hook failures (currently) hard-end due + * to that fact that a failing hook will + * surely cause subsequent tests to fail, + * causing jumbled reporting. + * + * @param {Hook} hook + * @param {Error} err + * @api private + */ + +Runner.prototype.failHook = function(hook, err){ + this.fail(hook, err); + this.emit('end'); +}; + +/** + * Run hook `name` callbacks and then invoke `fn()`. + * + * @param {String} name + * @param {Function} function + * @api private + */ + +Runner.prototype.hook = function(name, fn){ + var suite = this.suite + , hooks = suite['_' + name] + , self = this + , timer; + + function next(i) { + var hook = hooks[i]; + if (!hook) return fn(); + if (self.failures && suite.bail()) return fn(); + self.currentRunnable = hook; + + hook.ctx.currentTest = self.test; + + self.emit('hook', hook); + + hook.on('error', function(err){ + self.failHook(hook, err); + }); + + hook.run(function(err){ + hook.removeAllListeners('error'); + var testError = hook.error(); + if (testError) self.fail(self.test, testError); + if (err) return self.failHook(hook, err); + self.emit('hook end', hook); + delete hook.ctx.currentTest; + next(++i); + }); + } + + Runner.immediately(function(){ + next(0); + }); +}; + +/** + * Run hook `name` for the given array of `suites` + * in order, and callback `fn(err)`. + * + * @param {String} name + * @param {Array} suites + * @param {Function} fn + * @api private + */ + +Runner.prototype.hooks = function(name, suites, fn){ + var self = this + , orig = this.suite; + + function next(suite) { + self.suite = suite; + + if (!suite) { + self.suite = orig; + return fn(); + } + + self.hook(name, function(err){ + if (err) { + self.suite = orig; + return fn(err); + } + + next(suites.pop()); + }); + } + + next(suites.pop()); +}; + +/** + * Run hooks from the top level down. + * + * @param {String} name + * @param {Function} fn + * @api private + */ + +Runner.prototype.hookUp = function(name, fn){ + var suites = [this.suite].concat(this.parents()).reverse(); + this.hooks(name, suites, fn); +}; + +/** + * Run hooks from the bottom up. + * + * @param {String} name + * @param {Function} fn + * @api private + */ + +Runner.prototype.hookDown = function(name, fn){ + var suites = [this.suite].concat(this.parents()); + this.hooks(name, suites, fn); +}; + +/** + * Return an array of parent Suites from + * closest to furthest. + * + * @return {Array} + * @api private + */ + +Runner.prototype.parents = function(){ + var suite = this.suite + , suites = []; + while (suite = suite.parent) suites.push(suite); + return suites; +}; + +/** + * Run the current test and callback `fn(err)`. + * + * @param {Function} fn + * @api private + */ + +Runner.prototype.runTest = function(fn){ + var test = this.test + , self = this; + + if (this.asyncOnly) test.asyncOnly = true; + + try { + test.on('error', function(err){ + self.fail(test, err); + }); + test.run(fn); + } catch (err) { + fn(err); + } +}; + +/** + * Run tests in the given `suite` and invoke + * the callback `fn()` when complete. + * + * @param {Suite} suite + * @param {Function} fn + * @api private + */ + +Runner.prototype.runTests = function(suite, fn){ + var self = this + , tests = suite.tests.slice() + , test; + + function next(err) { + // if we bail after first err + if (self.failures && suite._bail) return fn(); + + // next test + test = tests.shift(); + + // all done + if (!test) return fn(); + + // grep + var match = self._grep.test(test.fullTitle()); + if (self._invert) match = !match; + if (!match) return next(); + + // pending + if (test.pending) { + self.emit('pending', test); + self.emit('test end', test); + return next(); + } + + // execute test and hook(s) + self.emit('test', self.test = test); + self.hookDown('beforeEach', function(){ + self.currentRunnable = self.test; + self.runTest(function(err){ + test = self.test; + + if (err) { + self.fail(test, err); + self.emit('test end', test); + return self.hookUp('afterEach', next); + } + + test.state = 'passed'; + self.emit('pass', test); + self.emit('test end', test); + self.hookUp('afterEach', next); + }); + }); + } + + this.next = next; + next(); +}; + +/** + * Run the given `suite` and invoke the + * callback `fn()` when complete. + * + * @param {Suite} suite + * @param {Function} fn + * @api private + */ + +Runner.prototype.runSuite = function(suite, fn){ + var total = this.grepTotal(suite) + , self = this + , i = 0; + + debug('run suite %s', suite.fullTitle()); + + if (!total) return fn(); + + this.emit('suite', this.suite = suite); + + function next() { + var curr = suite.suites[i++]; + if (!curr) return done(); + self.runSuite(curr, next); + } + + function done() { + self.suite = suite; + self.hook('afterAll', function(){ + self.emit('suite end', suite); + fn(); + }); + } + + this.hook('beforeAll', function(){ + self.runTests(suite, next); + }); +}; + +/** + * Handle uncaught exceptions. + * + * @param {Error} err + * @api private + */ + +Runner.prototype.uncaught = function(err){ + debug('uncaught exception %s', err.message); + var runnable = this.currentRunnable; + if (!runnable || 'failed' == runnable.state) return; + runnable.clearTimeout(); + err.uncaught = true; + this.fail(runnable, err); + + // recover from test + if ('test' == runnable.type) { + this.emit('test end', runnable); + this.hookUp('afterEach', this.next); + return; + } + + // bail on hooks + this.emit('end'); +}; + +/** + * Run the root suite and invoke `fn(failures)` + * on completion. + * + * @param {Function} fn + * @return {Runner} for chaining + * @api public + */ + +Runner.prototype.run = function(fn){ + var self = this + , fn = fn || function(){}; + + function uncaught(err){ + self.uncaught(err); + } + + debug('start'); + + // callback + this.on('end', function(){ + debug('end'); + process.removeListener('uncaughtException', uncaught); + fn(self.failures); + }); + + // run suites + this.emit('start'); + this.runSuite(this.suite, function(){ + debug('finished running'); + self.emit('end'); + }); + + // uncaught exception + process.on('uncaughtException', uncaught); + + return this; +}; + +/** + * Filter leaks with the given globals flagged as `ok`. + * + * @param {Array} ok + * @param {Array} globals + * @return {Array} + * @api private + */ + +function filterLeaks(ok, globals) { + return filter(globals, function(key){ + // Firefox and Chrome exposes iframes as index inside the window object + if (/^d+/.test(key)) return false; + var matched = filter(ok, function(ok){ + if (~ok.indexOf('*')) return 0 == key.indexOf(ok.split('*')[0]); + // Opera and IE expose global variables for HTML element IDs (issue #243) + if (/^mocha-/.test(key)) return true; + return key == ok; + }); + return matched.length == 0 && (!global.navigator || 'onerror' !== key); + }); +} + +}); // module: runner.js + +require.register("suite.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var EventEmitter = require('browser/events').EventEmitter + , debug = require('browser/debug')('mocha:suite') + , milliseconds = require('./ms') + , utils = require('./utils') + , Hook = require('./hook'); + +/** + * Expose `Suite`. + */ + +exports = module.exports = Suite; + +/** + * Create a new `Suite` with the given `title` + * and parent `Suite`. When a suite with the + * same title is already present, that suite + * is returned to provide nicer reporter + * and more flexible meta-testing. + * + * @param {Suite} parent + * @param {String} title + * @return {Suite} + * @api public + */ + +exports.create = function(parent, title){ + var suite = new Suite(title, parent.ctx); + suite.parent = parent; + if (parent.pending) suite.pending = true; + title = suite.fullTitle(); + parent.addSuite(suite); + return suite; +}; + +/** + * Initialize a new `Suite` with the given + * `title` and `ctx`. + * + * @param {String} title + * @param {Context} ctx + * @api private + */ + +function Suite(title, ctx) { + this.title = title; + this.ctx = ctx; + this.suites = []; + this.tests = []; + this.pending = false; + this._beforeEach = []; + this._beforeAll = []; + this._afterEach = []; + this._afterAll = []; + this.root = !title; + this._timeout = 2000; + this._slow = 75; + this._bail = false; +} + +/** + * Inherit from `EventEmitter.prototype`. + */ + +function F(){}; +F.prototype = EventEmitter.prototype; +Suite.prototype = new F; +Suite.prototype.constructor = Suite; + + +/** + * Return a clone of this `Suite`. + * + * @return {Suite} + * @api private + */ + +Suite.prototype.clone = function(){ + var suite = new Suite(this.title); + debug('clone'); + suite.ctx = this.ctx; + suite.timeout(this.timeout()); + suite.slow(this.slow()); + suite.bail(this.bail()); + return suite; +}; + +/** + * Set timeout `ms` or short-hand such as "2s". + * + * @param {Number|String} ms + * @return {Suite|Number} for chaining + * @api private + */ + +Suite.prototype.timeout = function(ms){ + if (0 == arguments.length) return this._timeout; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('timeout %d', ms); + this._timeout = parseInt(ms, 10); + return this; +}; + +/** + * Set slow `ms` or short-hand such as "2s". + * + * @param {Number|String} ms + * @return {Suite|Number} for chaining + * @api private + */ + +Suite.prototype.slow = function(ms){ + if (0 === arguments.length) return this._slow; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('slow %d', ms); + this._slow = ms; + return this; +}; + +/** + * Sets whether to bail after first error. + * + * @parma {Boolean} bail + * @return {Suite|Number} for chaining + * @api private + */ + +Suite.prototype.bail = function(bail){ + if (0 == arguments.length) return this._bail; + debug('bail %s', bail); + this._bail = bail; + return this; +}; + +/** + * Run `fn(test[, done])` before running tests. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.beforeAll = function(fn){ + if (this.pending) return this; + var hook = new Hook('"before all" hook', fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._beforeAll.push(hook); + this.emit('beforeAll', hook); + return this; +}; + +/** + * Run `fn(test[, done])` after running tests. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.afterAll = function(fn){ + if (this.pending) return this; + var hook = new Hook('"after all" hook', fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._afterAll.push(hook); + this.emit('afterAll', hook); + return this; +}; + +/** + * Run `fn(test[, done])` before each test case. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.beforeEach = function(fn){ + if (this.pending) return this; + var hook = new Hook('"before each" hook', fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._beforeEach.push(hook); + this.emit('beforeEach', hook); + return this; +}; + +/** + * Run `fn(test[, done])` after each test case. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.afterEach = function(fn){ + if (this.pending) return this; + var hook = new Hook('"after each" hook', fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._afterEach.push(hook); + this.emit('afterEach', hook); + return this; +}; + +/** + * Add a test `suite`. + * + * @param {Suite} suite + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.addSuite = function(suite){ + suite.parent = this; + suite.timeout(this.timeout()); + suite.slow(this.slow()); + suite.bail(this.bail()); + this.suites.push(suite); + this.emit('suite', suite); + return this; +}; + +/** + * Add a `test` to this suite. + * + * @param {Test} test + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.addTest = function(test){ + test.parent = this; + test.timeout(this.timeout()); + test.slow(this.slow()); + test.ctx = this.ctx; + this.tests.push(test); + this.emit('test', test); + return this; +}; + +/** + * Return the full title generated by recursively + * concatenating the parent's full title. + * + * @return {String} + * @api public + */ + +Suite.prototype.fullTitle = function(){ + if (this.parent) { + var full = this.parent.fullTitle(); + if (full) return full + ' ' + this.title; + } + return this.title; +}; + +/** + * Return the total number of tests. + * + * @return {Number} + * @api public + */ + +Suite.prototype.total = function(){ + return utils.reduce(this.suites, function(sum, suite){ + return sum + suite.total(); + }, 0) + this.tests.length; +}; + +/** + * Iterates through each suite recursively to find + * all tests. Applies a function in the format + * `fn(test)`. + * + * @param {Function} fn + * @return {Suite} + * @api private + */ + +Suite.prototype.eachTest = function(fn){ + utils.forEach(this.tests, fn); + utils.forEach(this.suites, function(suite){ + suite.eachTest(fn); + }); + return this; +}; + +}); // module: suite.js + +require.register("test.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Runnable = require('./runnable'); + +/** + * Expose `Test`. + */ + +module.exports = Test; + +/** + * Initialize a new `Test` with the given `title` and callback `fn`. + * + * @param {String} title + * @param {Function} fn + * @api private + */ + +function Test(title, fn) { + Runnable.call(this, title, fn); + this.pending = !fn; + this.type = 'test'; +} + +/** + * Inherit from `Runnable.prototype`. + */ + +function F(){}; +F.prototype = Runnable.prototype; +Test.prototype = new F; +Test.prototype.constructor = Test; + + +}); // module: test.js + +require.register("utils.js", function(module, exports, require){ +/** + * Module dependencies. + */ + +var fs = require('browser/fs') + , path = require('browser/path') + , join = path.join + , debug = require('browser/debug')('mocha:watch'); + +/** + * Ignored directories. + */ + +var ignore = ['node_modules', '.git']; + +/** + * Escape special characters in the given string of html. + * + * @param {String} html + * @return {String} + * @api private + */ + +exports.escape = function(html){ + return String(html) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); +}; + +/** + * Array#forEach (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @param {Object} scope + * @api private + */ + +exports.forEach = function(arr, fn, scope){ + for (var i = 0, l = arr.length; i < l; i++) + fn.call(scope, arr[i], i); +}; + +/** + * Array#indexOf (<=IE8) + * + * @parma {Array} arr + * @param {Object} obj to find index of + * @param {Number} start + * @api private + */ + +exports.indexOf = function(arr, obj, start){ + for (var i = start || 0, l = arr.length; i < l; i++) { + if (arr[i] === obj) + return i; + } + return -1; +}; + +/** + * Array#reduce (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @param {Object} initial value + * @api private + */ + +exports.reduce = function(arr, fn, val){ + var rval = val; + + for (var i = 0, l = arr.length; i < l; i++) { + rval = fn(rval, arr[i], i, arr); + } + + return rval; +}; + +/** + * Array#filter (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @api private + */ + +exports.filter = function(arr, fn){ + var ret = []; + + for (var i = 0, l = arr.length; i < l; i++) { + var val = arr[i]; + if (fn(val, i, arr)) ret.push(val); + } + + return ret; +}; + +/** + * Object.keys (<=IE8) + * + * @param {Object} obj + * @return {Array} keys + * @api private + */ + +exports.keys = Object.keys || function(obj) { + var keys = [] + , has = Object.prototype.hasOwnProperty // for `window` on <=IE8 + + for (var key in obj) { + if (has.call(obj, key)) { + keys.push(key); + } + } + + return keys; +}; + +/** + * Watch the given `files` for changes + * and invoke `fn(file)` on modification. + * + * @param {Array} files + * @param {Function} fn + * @api private + */ + +exports.watch = function(files, fn){ + var options = { interval: 100 }; + files.forEach(function(file){ + debug('file %s', file); + fs.watchFile(file, options, function(curr, prev){ + if (prev.mtime < curr.mtime) fn(file); + }); + }); +}; + +/** + * Ignored files. + */ + +function ignored(path){ + return !~ignore.indexOf(path); +} + +/** + * Lookup files in the given `dir`. + * + * @return {Array} + * @api private + */ + +exports.files = function(dir, ret){ + ret = ret || []; + + fs.readdirSync(dir) + .filter(ignored) + .forEach(function(path){ + path = join(dir, path); + if (fs.statSync(path).isDirectory()) { + exports.files(path, ret); + } else if (path.match(/\.(js|coffee|litcoffee|coffee.md)$/)) { + ret.push(path); + } + }); + + return ret; +}; + +/** + * Compute a slug from the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + +exports.slug = function(str){ + return str + .toLowerCase() + .replace(/ +/g, '-') + .replace(/[^-\w]/g, ''); +}; + +/** + * Strip the function definition from `str`, + * and re-indent for pre whitespace. + */ + +exports.clean = function(str) { + str = str + .replace(/^function *\(.*\) *{/, '') + .replace(/\s+\}$/, ''); + + var whitespace = str.match(/^\n?(\s*)/)[1] + , re = new RegExp('^' + whitespace, 'gm'); + + str = str.replace(re, ''); + + return exports.trim(str); +}; + +/** + * Escape regular expression characters in `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + +exports.escapeRegexp = function(str){ + return str.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"); +}; + +/** + * Trim the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + +exports.trim = function(str){ + return str.replace(/^\s+|\s+$/g, ''); +}; + +/** + * Parse the given `qs`. + * + * @param {String} qs + * @return {Object} + * @api private + */ + +exports.parseQuery = function(qs){ + return exports.reduce(qs.replace('?', '').split('&'), function(obj, pair){ + var i = pair.indexOf('=') + , key = pair.slice(0, i) + , val = pair.slice(++i); + + obj[key] = decodeURIComponent(val); + return obj; + }, {}); +}; + +/** + * Highlight the given string of `js`. + * + * @param {String} js + * @return {String} + * @api private + */ + +function highlight(js) { + return js + .replace(//g, '>') + .replace(/\/\/(.*)/gm, '//$1') + .replace(/('.*?')/gm, '$1') + .replace(/(\d+\.\d+)/gm, '$1') + .replace(/(\d+)/gm, '$1') + .replace(/\bnew *(\w+)/gm, 'new $1') + .replace(/\b(function|new|throw|return|var|if|else)\b/gm, '$1') +} + +/** + * Highlight the contents of tag `name`. + * + * @param {String} name + * @api private + */ + +exports.highlightTags = function(name) { + var code = document.getElementsByTagName(name); + for (var i = 0, len = code.length; i < len; ++i) { + code[i].innerHTML = highlight(code[i].innerHTML); + } +}; + +}); // module: utils.js +// The global object is "self" in Web Workers. +global = (function() { return this; })(); + +/** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + +var Date = global.Date; +var setTimeout = global.setTimeout; +var setInterval = global.setInterval; +var clearTimeout = global.clearTimeout; +var clearInterval = global.clearInterval; + +/** + * Node shims. + * + * These are meant only to allow + * mocha.js to run untouched, not + * to allow running node code in + * the browser. + */ + +var process = {}; +process.exit = function(status){}; +process.stdout = {}; + +/** + * Remove uncaughtException listener. + */ + +process.removeListener = function(e){ + if ('uncaughtException' == e) { + global.onerror = function() {}; + } +}; + +/** + * Implements uncaughtException listener. + */ + +process.on = function(e, fn){ + if ('uncaughtException' == e) { + global.onerror = function(err, url, line){ + fn(new Error(err + ' (' + url + ':' + line + ')')); + }; + } +}; + +/** + * Expose mocha. + */ + +var Mocha = global.Mocha = require('mocha'), + mocha = global.mocha = new Mocha({ reporter: 'html' }); + +var immediateQueue = [] + , immediateTimeout; + +function timeslice() { + var immediateStart = new Date().getTime(); + while (immediateQueue.length && (new Date().getTime() - immediateStart) < 100) { + immediateQueue.shift()(); + } + if (immediateQueue.length) { + immediateTimeout = setTimeout(timeslice, 0); + } else { + immediateTimeout = null; + } +} + +/** + * High-performance override of Runner.immediately. + */ + +Mocha.Runner.immediately = function(callback) { + immediateQueue.push(callback); + if (!immediateTimeout) { + immediateTimeout = setTimeout(timeslice, 0); + } +}; + +/** + * Override ui to ensure that the ui functions are initialized. + * Normally this would happen in Mocha.prototype.loadFiles. + */ + +mocha.ui = function(ui){ + Mocha.prototype.ui.call(this, ui); + this.suite.emit('pre-require', global, null, this); + return this; +}; + +/** + * Setup mocha with the given setting options. + */ + +mocha.setup = function(opts){ + if ('string' == typeof opts) opts = { ui: opts }; + for (var opt in opts) this[opt](opts[opt]); + return this; +}; + +/** + * Run mocha, returning the Runner. + */ + +mocha.run = function(fn){ + var options = mocha.options; + mocha.globals('location'); + + var query = Mocha.utils.parseQuery(global.location.search || ''); + if (query.grep) mocha.grep(query.grep); + if (query.invert) mocha.invert(); + + return Mocha.prototype.run.call(mocha, function(){ + // The DOM Document is not available in Web Workers. + if (global.document) { + Mocha.utils.highlightTags('code'); + } + if (fn) fn(); + }); +}; + +/** + * Expose the process shim. + */ + +Mocha.process = process; +})(); \ No newline at end of file diff --git a/tests/spec/spoor.js b/tests/spec/spoor.js new file mode 100644 index 00000000..0d8327ad --- /dev/null +++ b/tests/spec/spoor.js @@ -0,0 +1,12 @@ +define(function(require) { + + describe('My fake test', function() { + + it('should allow me to lie about a test', function() { + + expect(true).to.be(true); + + }); + }); + +}); diff --git a/tests/test-main.js b/tests/test-main.js new file mode 100644 index 00000000..971d89ff --- /dev/null +++ b/tests/test-main.js @@ -0,0 +1,44 @@ +var tests = []; +for (var file in window.__karma__.files) { + if (/spec\//.test(file)) { + tests.push(file); + } +} + +requirejs.config({ + baseUrl: '/base/src', + paths: { + jquery: 'core/js/libraries/jquery', + underscore: 'core/js/libraries/underscore', + backbone: 'core/js/libraries/backbone', + modernizr: 'core/js/libraries/modernizr', + handlebars: 'core/js/libraries/handlebars', + imageReady: 'core/js/libraries/imageReady', + inview: 'core/js/libraries/inview', + scrollTo: 'core/js/libraries/scrollTo', + coreJS: 'core/js', + coreViews: 'core/js/views', + coreModels: 'core/js/models', + coreCollections: 'core/js/collections', + templates: 'templates/templates', + spec: '../test/spec' + }, + shim: { + jquery: [], + backbone: { + deps: [ + 'underscore', + 'jquery' + ], + exports: 'Backbone' + }, + underscore: { + exports: '_' + }, + handlebars: { + exports: 'Handlebars' + } + }, + deps: tests, + callback: window.__karma__.start +});