Merge pull request #1160 from ether/release/releases-1.2

Release version 1.2
This commit is contained in:
John McLear 2012-11-14 13:29:51 -08:00
commit 5f30ea447e
82 changed files with 11960 additions and 353 deletions

22
.travis.yml Normal file
View file

@ -0,0 +1,22 @@
language: node_js
node_js:
- "0.8"
install:
- "bin/installDeps.sh"
- "export GIT_HASH=$(cat .git/HEAD | head -c 7)"
before_script:
- "tests/frontend/travis/sauce_tunnel.sh"
script:
- "tests/frontend/travis/runner.sh"
env:
global:
- secure: "oKA4KbSvyxMOFCiOa3hWswnaIrCmX60MfhBhD8xu8sodOqbdK5RUrxDJew9p\n1nNSewxoVmKhX0G5GxIABfGtdU1nrEzCEoejTDJIFmzEbcLcHpcyarouWLSY\nOpn11FKS1rnb69aflHM7K8l4dhrCkA2i0Dwwl8LN3HayGzDV2Rg="
- SAUCE_USER=pita
jdk:
- oraclejdk6
notifications:
email:
- petermartischka@googlemail.com
irc:
channels:
- "irc.freenode.org#etherpad-lite-dev"

View file

@ -1,3 +1,16 @@
# v1.2
* Internationalization / Language / Translation support (i18n) with support for German/French
* A frontend/client side testing framework and backend build tests
* Customizable robots.txt
* Customizable app title (finally you can name your epl instance!)
* eejs render arguments are now passed on to eejs hooks through the newly introduced `renderContext` argument.
* Plugin-specific settings in settings.json (finally allowing for things like a google analytics plugin)
* Serve admin dashboard at /admin (still very limited, though)
* Modify your settings.json through the newly created UI at /admin/settings
* Fix: Import <ol>'s as <ol>'s and not as <ul>'s!
* Added solaris compatibility (bin/installDeps.sh was broken on solaris)
* Fix a bug with IE9 and Password Protected Pads using HTTPS
# v1.1.5
* We updated to express v3 (please [make sure](https://github.com/visionmedia/express/wiki/Migrating-from-2.x-to-3.x) your plugin works under express v3)
* `userColor` URL parameter which sets the initial author color

View file

@ -1,49 +1,56 @@
# Developer Guidelines
Please talk to people on the mailing list before you change this page
Mailing list: https://groups.google.com/forum/?fromgroups#!forum/etherpad-lite-dev
IRC channels: [#etherpad](irc://freenode/#etherpad) ([webchat](webchat.freenode.net?channels=etherpad)), [#etherpad-lite-dev](irc://freenode/#etherpad-lite-dev) ([webchat](webchat.freenode.net?channels=etherpad-lite-dev))
(Please talk to people on the mailing list before you change this page, see our section on [how to get in touch](https://github.com/ether/etherpad-lite#get-in-touch))
**Our goal is to iterate in small steps. Release often, release early. Evolution instead of a revolution**
## General goals of Etherpad Lite
* easy to install for admins
* easy to use for people
To make sure everybody is going in the same direction:
* easy to install for admins and easy to use for people
* easy to integrate into other apps, but also usable as standalone
* using less resources on server side
* easy to embed for admins
* also runable as etherpad lite only
* keep it maintainable, we don't wanna end ob as the monster Etherpad was
* extensible, as much functionality should be extendable with plugins so changes don't have to be done in core
Also, keep it maintainable. We don't wanna end ob as the monster Etherpad was!
## How to code:
* **Please write comments**. I don't mean you have to comment every line and every loop. I just mean, if you do anything thats a bit complex or a bit weird, please leave a comment. It's easy to do that if you do while you're writing the code. Keep in mind that you will probably leave the project at some point and that other people will read your code. Undocumented huge amounts of code are worthless
* Never ever use tabs
* Indentation: JS/CSS: 2 spaces; HTML: 4 spaces
* Don't overengineer. Don't try to solve any possible problem in one step. Try to solve problems as easy as possible and improve the solution over time
* Do generalize sooner or later - if an old solution hacked together according to the above point, poses more problems than it solves today, reengineer it, with the lessons learned taken into account.
* Keep it compatible to API-Clients/older DBs/configurations. Don't make incompatible changes the protocol/database format without good reasons
## How to work with git
* Make a new branch for every feature you're working on. Don't work in your master branch. This ensures that you can work you can do lot of small pull requests instead of one big one with complete different features
* Don't use the online edit function of github. This only creates ugly and not working commits
* Test before you push. Sounds easy, it isn't
* Try to make clean commits that are easy readable
* Don't check in stuff that gets generated during build or runtime (like jquery, minified files, dbs etc...)
* Make pull requests from your feature branch to our develop branch once your feature is ready
## How to work with git?
* Don't work in your master branch.
* Make a new branch for every feature you're working on. (This ensures that you can work you can do lots of small, independent pull requests instead of one big one with complete different features)
* Don't use the online edit function of github (this only creates ugly and not working commits!)
* Try to make clean commits that are easy readable (including descriptive commit messages!)
* Test before you push. Sounds easy, it isn't!
* Don't check in stuff that gets generated during build or runtime
* Make small pull requests that are easy to review but make sure they do add value by themselves / individually
## Branching model in Etherpad Lite
## Coding style
* Do write comments. (You don't have to comment every line, but if you come up with something thats a bit complex/weird, just leave a comment. Bear in mind that you will probably leave the project at some point and that other people will read your code. Undocumented huge amounts of code are worthless!)
* Never ever use tabs
* Indentation: JS/CSS: 2 spaces; HTML: 4 spaces
* Don't overengineer. Don't try to solve any possible problem in one step, but try to solve problems as easy as possible and improve the solution over time!
* Do generalize sooner or later! (if an old solution, quickly hacked together, poses more problems than it solves today, refactor it!)
* Keep it compatible. Do not introduce changes to the public API, db schema or configurations too lightly. Don't make incompatible changes without good reasons!
* If you do make changes, document them! (see below)
## Branching model / git workflow
see git flow http://nvie.com/posts/a-successful-git-branching-model/
* master, the stable. This is the branch everyone should use for production stuff
* develop, everything that is READY to go into master at some point in time. This stuff is tested and ready to go out
* release branches, stuff that should go into master very soon, only bugfixes go into these (see http://nvie.com/posts/a-successful-git-branching-model/ for why)
* you can set tags in the master branch, there is no real need for release branches imho
* The latest tag is not what is shown in github by default. Doing a clone of master should give you latest stable, not what is gonna be latest stable in a week, also, we should not be blocking new features to develop, just because we feel that we should be releasing it to master soon. This is the situation that release branches solve/handle.
* hotfix branches, fixes for bugs in master
* feature branches (in your own repos), these are the branches where you develop your features in. If its ready to go out, it will be merged into develop
### `master` branch
* the stable
* This is the branch everyone should use for production stuff
### `develop`branch
* everything that is READY to go into master at some point in time
* This stuff is tested and ready to go out
### release branches
* stuff that should go into master very soon
* only bugfixes go into these (see http://nvie.com/posts/a-successful-git-branching-model/ for why)
* we should not be blocking new features to develop, just because we feel that we should be releasing it to master soon. This is the situation that release branches solve/handle.
### hotfix branches
* fixes for bugs in master
### feature branches (in your own repos)
* these are the branches where you develop your features in
* If its ready to go out, it will be merged into develop
Over the time we pull features from feature branches into the develop branch. Every month we pull from develop into master. Bugs in master get fixed in hotfix branches. These branches will get merged into master AND develop. There should never be commits in master that aren't in develop

View file

@ -1,13 +1,13 @@
doc_sources = $(wildcard doc/*/*.md) $(wildcard doc/*.md)
outdoc_files = $(addprefix out/,$(doc_sources:.md=.html))
docassets = $(addprefix out/,$(wildcard doc/_assets/*))
docassets = $(addprefix out/,$(wildcard doc/assets/*))
VERSION = $(shell node -e "console.log( require('./src/package.json').version )")
docs: $(outdoc_files) $(docassets)
out/doc/_assets/%: doc/_assets/%
out/doc/assets/%: doc/assets/%
mkdir -p $(@D)
cp $< $@

View file

@ -27,13 +27,13 @@ documented codebase makes it easier for developers to improve the code and contr
</table>
Etherpad Lite is designed to be easily embeddable and provides a [HTTP API](https://github.com/Pita/etherpad-lite/wiki/HTTP-API)
that allows your web application to manage pads, users and groups. It is recommended to use the client implementations available for this API, listed on [this wiki page](https://github.com/Pita/etherpad-lite/wiki/HTTP-API-client-libraries).
Etherpad Lite is designed to be easily embeddable and provides a [HTTP API](https://github.com/ether/etherpad-lite/wiki/HTTP-API)
that allows your web application to manage pads, users and groups. It is recommended to use the client implementations available for this API, listed on [this wiki page](https://github.com/ether/etherpad-lite/wiki/HTTP-API-client-libraries).
There is also a [jQuery plugin](https://github.com/johnyma22/etherpad-lite-jquery-plugin) that helps you to embed Pads into your website
**Visit [beta.etherpad.org](http://beta.etherpad.org) to test it live**
Also, check out the **[FAQ](https://github.com/Pita/etherpad-lite/wiki/FAQ)**, really!
Also, check out the **[FAQ](https://github.com/ether/etherpad-lite/wiki/FAQ)**, really!
# Installation
@ -42,7 +42,7 @@ Also, check out the **[FAQ](https://github.com/Pita/etherpad-lite/wiki/FAQ)**, r
### Prebuilt windows package
This package works out of the box on any windows machine, but it's not very useful for developing purposes...
1. Download the windows package <https://github.com/Pita/etherpad-lite/downloads>
1. Download the windows package <https://github.com/ether/etherpad-lite/downloads>
2. Extract the folder
Now, run `start.bat` and open <http://localhost:9001> in your browser. You like it? [Next steps](#next-steps).
@ -51,8 +51,8 @@ Now, run `start.bat` and open <http://localhost:9001> in your browser. You like
You'll need [node.js](http://nodejs.org) and (optionally, though recommended) git.
1. Grab the source, either
- download <https://github.com/Pita/etherpad-lite/zipball/master>
- or `git clone https://github.com/Pita/etherpad-lite.git` (for this you need git, obviously)
- download <https://github.com/ether/etherpad-lite/zipball/master>
- or `git clone https://github.com/ether/etherpad-lite.git` (for this you need git, obviously)
2. start `bin\installOnWindows.bat`
Now, run `start.bat` and open <http://localhost:9001> in your browser.
@ -70,7 +70,7 @@ Additionally, you'll need [node.js](http://nodejs.org).
**As any user (we recommend creating a separate user called etherpad-lite):**
1. Move to a folder where you want to install Etherpad Lite. Clone the git repository `git clone git://github.com/Pita/etherpad-lite.git`
1. Move to a folder where you want to install Etherpad Lite. Clone the git repository `git clone git://github.com/ether/etherpad-lite.git`
2. Change into the new directory containing the cloned source code `cd etherpad-lite`
Now, run `bin\run.sh` and open <http://127.0.0.1:9001> in your browser.
@ -87,7 +87,7 @@ You can modify the settings in `settings.json`. (If you need to handle multiple
You should use a dedicated database such as "mysql", if you are planning on using etherpad-lite in a production environment, since the "dirtyDB" database driver is only for testing and/or development purposes.
## Helpful resources
The [wiki](https://github.com/Pita/etherpad-lite/wiki) is your one-stop resource for Tutorials and How-to's, really check it out! Also, feel free to improve these wiki pages.
The [wiki](https://github.com/ether/etherpad-lite/wiki) is your one-stop resource for Tutorials and How-to's, really check it out! Also, feel free to improve these wiki pages.
Documentation can be found in `docs/`.
@ -100,27 +100,27 @@ If you're new to node.js, start with Ryan Dahl's [Introduction to Node.js](http:
You can debug Etherpad lite using `bin/debugRun.sh`.
If you want to find out how Etherpad's `Easysync` works (the library that makes it really realtime), start with this [PDF](https://github.com/Pita/etherpad-lite/raw/master/doc/easysync/easysync-full-description.pdf) (complex, but worth reading).
If you want to find out how Etherpad's `Easysync` works (the library that makes it really realtime), start with this [PDF](https://github.com/ether/etherpad-lite/raw/master/doc/easysync/easysync-full-description.pdf) (complex, but worth reading).
## Getting started
You know all this and just want to know how you can help?
Look at the [TODO list](https://github.com/Pita/etherpad-lite/wiki/TODO) and our [Issue tracker](https://github.com/Pita/etherpad-lite/issues). (Please consider using [jshint](http://www.jshint.com/about/), if you plan to contribute code.)
Look at the [TODO list](https://github.com/ether/etherpad-lite/wiki/TODO) and our [Issue tracker](https://github.com/ether/etherpad-lite/issues). (Please consider using [jshint](http://www.jshint.com/about/), if you plan to contribute code.)
Also, and most importantly, read our [**Developer Guidelines**](https://github.com/Pita/etherpad-lite/wiki/Developer-Guidelines), really!
Also, and most importantly, read our [**Developer Guidelines**](https://github.com/ether/etherpad-lite/wiki/Developer-Guidelines), really!
# Get in touch
Join the [mailinglist](http://groups.google.com/group/etherpad-lite-dev) and make some noise on our freenode irc channel [#etherpad-lite-dev](http://webchat.freenode.net?channels=#etherpad-lite-dev)!
# Modules created for this project
* [ueberDB](https://github.com/Pita/ueberDB) "transforms every database into a object key value store" - manages all database access
* [channels](https://github.com/Pita/channels) "Event channels in node.js" - ensures that ueberDB operations are atomic and in series for each key
* [async-stacktrace](https://github.com/Pita/async-stacktrace) "Improves node.js stacktraces and makes it easier to handle errors"
* [ueberDB](https://github.com/ether/ueberDB) "transforms every database into a object key value store" - manages all database access
* [channels](https://github.com/ether/channels) "Event channels in node.js" - ensures that ueberDB operations are atomic and in series for each key
* [async-stacktrace](https://github.com/ether/async-stacktrace) "Improves node.js stacktraces and makes it easier to handle errors"
# Donate!
* [Flattr] (http://flattr.com/thing/71378/Etherpad-Foundation)
* Paypal - Press the donate button on [etherpad.org](http://etherpad.org)
# License
[Apache License v2](http://www.apache.org/licenses/LICENSE-2.0.html)
[Apache License v2](http://www.apache.org/licenses/LICENSE-2.0.html)

View file

@ -1,7 +0,0 @@
.git*
docs/
examples/
support/
test/
testing.js
.DS_Store

View file

@ -1,36 +0,0 @@
{
"parts": [
{
"name": "somepart",
"pre": [],
"post": ["ep_onemoreplugin/partone"]
},
{
"name": "partlast",
"pre": ["ep_fintest/otherpart"],
"post": [],
"hooks": {
"somehookname": "ep_fintest/partlast:somehook"
}
},
{
"name": "partfirst",
"pre": [],
"post": ["ep_onemoreplugin/somepart"]
},
{
"name": "otherpart",
"pre": ["ep_fintest/somepart", "ep_otherplugin/main"],
"post": [],
"hooks": {
"somehookname": "ep_fintest/otherpart:somehook",
"morehook": "ep_fintest/otherpart:morehook",
"expressCreateServer": "ep_fintest/otherpart:expressServer",
"eejsBlock_editbarMenuLeft": "ep_fintest/otherpart:eejsBlock_editbarMenuLeft"
},
"client_hooks": {
"somehookname": "ep_fintest/static/js/test:bar"
}
}
]
}

View file

@ -1,25 +0,0 @@
test = require("ep_fintest/static/js/test.js");
console.log("FOOO:", test.foo);
exports.somehook = function (hook_name, args, cb) {
return cb(["otherpart:somehook was here"]);
}
exports.morehook = function (hook_name, args, cb) {
return cb(["otherpart:morehook was here"]);
}
exports.expressServer = function (hook_name, args, cb) {
args.app.get('/otherpart', function(req, res) {
res.send("<em>Abra cadabra</em>");
});
}
exports.eejsBlock_editbarMenuLeft = function (hook_name, args, cb) {
args.content = args.content + '\
<li id="testButton" onClick="window.pad&amp;&amp;pad.editbarClick(\'clearauthorship\');return false;">\
<a class="buttonicon buttonicon-test" title="Test test test"></a>\
</li>\
';
return cb();
}

View file

@ -1,9 +0,0 @@
{
"name": "ep_fintest",
"description": "A test plugin",
"version": "0.0.1",
"author": "RedHog (Egil Moeller) <egil.moller@freecode.no>",
"contributors": [],
"dependencies": {},
"engines": { "node": ">= 0.4.1 < 0.7.0" }
}

View file

@ -1,3 +0,0 @@
exports.somehook = function (hook_name, args, cb) {
return cb(["partlast:somehook was here"]);
}

View file

@ -1,5 +0,0 @@
exports.foo = 42;
exports.bar = function (hook_name, args, cb) {
return cb(["FOOOO"]);
}

View file

@ -1 +0,0 @@
<em>Test bla bla</em>

View file

@ -8,6 +8,14 @@ if [ -d "../bin" ]; then
cd "../"
fi
#Is gnu-grep (ggrep) installed on SunOS (Solaris)
if [ $(uname) = "SunOS" ]; then
hash ggrep > /dev/null 2>&1 || {
echo "Please install ggrep (pkg install gnu-grep)" >&2
exit 1
}
fi
#Is wget installed?
hash curl > /dev/null 2>&1 || {
echo "Please install curl" >&2
@ -52,7 +60,7 @@ done
#Does a $settings exist? if no copy the template
if [ ! -f $settings ]; then
echo "Copy the settings template to $settings..."
cp -v settings.json.template $settings || exit 1
cp settings.json.template $settings || exit 1
fi
echo "Ensure that all dependencies are up to date..."
@ -61,7 +69,7 @@ echo "Ensure that all dependencies are up to date..."
cd node_modules
[ -e ep_etherpad-lite ] || ln -s ../src ep_etherpad-lite
cd ep_etherpad-lite
npm install
npm install -s
) || {
rm -rf node_modules
exit 1
@ -71,8 +79,12 @@ echo "Ensure jQuery is downloaded and up to date..."
DOWNLOAD_JQUERY="true"
NEEDED_VERSION="1.7.1"
if [ -f "src/static/js/jquery.js" ]; then
VERSION=$(cat src/static/js/jquery.js | head -n 3 | grep -o "v[0-9]\.[0-9]\(\.[0-9]\)\?");
if [ $(uname) = "SunOS"]; then
VERSION=$(cat src/static/js/jquery.js | head -n 3 | ggrep -o "v[0-9]\.[0-9]\(\.[0-9]\)\?");
else
VERSION=$(cat src/static/js/jquery.js | head -n 3 | grep -o "v[0-9]\.[0-9]\(\.[0-9]\)\?");
fi
if [ ${VERSION#v} = $NEEDED_VERSION ]; then
DOWNLOAD_JQUERY="false"
fi
@ -91,11 +103,11 @@ echo "ensure custom css/js files are created..."
for f in "index" "pad" "timeslider"
do
if [ ! -f "src/static/custom/$f.js" ]; then
cp -v "src/static/custom/js.template" "src/static/custom/$f.js" || exit 1
cp "src/static/custom/js.template" "src/static/custom/$f.js" || exit 1
fi
if [ ! -f "src/static/custom/$f.css" ]; then
cp -v "src/static/custom/css.template" "src/static/custom/$f.css" || exit 1
cp "src/static/custom/css.template" "src/static/custom/$f.css" || exit 1
fi
done

View file

@ -1,11 +1,17 @@
var dirty = require("../src/node_modules/ueberDB/node_modules/dirty")('var/dirty.db');
var db = require("../src/node/db/DB");
require("ep_etherpad-lite/node_modules/npm").load({}, function(er,npm) {
db.init(function() {
db = db.db;
dirty.on("load", function() {
dirty.forEach(function(key, value) {
db.set(key, value);
process.chdir(npm.root+'/..')
var dirty = require("ep_etherpad-lite/node_modules/ueberDB/node_modules/dirty")('var/dirty.db');
var db = require("ep_etherpad-lite/node/db/DB");
db.init(function() {
db = db.db;
dirty.on("load", function() {
dirty.forEach(function(key, value) {
db.set(key, value);
});
});
});
});

View file

@ -36,6 +36,13 @@ Default: "unnamed"
Example: `userName=Etherpad%20User`
## userColor
* String (css hex color value)
Default: randomly chosen by pad server
Example: `userColor=%23ff9900`
## noColors
* Boolean
@ -45,3 +52,11 @@ Default: false
* Boolean
Default: false
## lang
* String
Default: en
Example: `lang=ar` (translates the interface into Arabic)

11
doc/custom_static.md Normal file
View file

@ -0,0 +1,11 @@
# Custom static files
Etherpad Lite allows you to include your own static files in the browser, by modifying the files in `static/custom`.
* `index.js` Javascript that'll be run in `/`
* `index.css` Stylesheet affecting `/`
* `pad.js` Javascript that'll be run in `/p/:padid`
* `pad.css` Stylesheet affecting `/p/:padid`
* `timeslider.js` Javascript that'll be run in `/p/:padid/timeslider`
* `timeslider.css` Stylesheet affecting `/p/:padid/timeslider`
* `favicon.ico` Overrides the default favicon.
* `robots.txt` Overrides the default `robots.txt`.

View file

@ -1,4 +1,6 @@
@include documentation
@include localization
@include custom_static
@include api/api
@include plugins
@include database

19
doc/localization.md Normal file
View file

@ -0,0 +1,19 @@
# Localization
Etherpad lite provides a multi-language user interface, that's apart from your users' content, so users from different countries can collaborate on a single document, while still having the user interface displayed in their mother tongue.
## Translating
`/src/locales` contains files for all supported languages which contain the translated strings. To add support for a new language, copy the English language file named `en.ini` and translate it.
Translation files are simply `*.ini` files and look like this:
```
pad.modals.connected = Connecté.
pad.modals.uderdup = Ouvrir dans une nouvelle fenêtre.
pad.toolbar.unindent.title = Désindenter
pad.toolbar.undo.title = Annuler (Ctrl-Z)
timeslider.pageTitle = {{appTitle}} Curseur temporel
```
There must be only one translation per line. Each translation consists of a key (the id of the string that is to be translated), an equal sign and the translated string. Anything after the equa sign will be used as the translated string (you may put some spaces after `=` for better readability, though). Terms in curly braces must not be touched but left as they are, since they represent a dynamically changing part of the string like a variable. Imagine a message welcoming a user: `Welcome, {{userName}}!` would be translated as `Ahoy, {{userName}}!` in pirate.
## Under the hood
We use a `language` cookie to save your language settings if you change them. If you don't, we autodetect your locale using information from your browser. Now, that we know your preferred language this information is feeded into a very nice library called [webL10n](https://github.com/fabi1cazenave/webL10n), which loads the appropriate translations and applies them to our templates, providing translation params, pluralization, include rules and even a nice javascript API along the way.

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<title>__SECTION__ - Etherpad Lite v__VERSION__ Manual &amp; Documentation</title>
<link rel="stylesheet" href="_assets/style.css">
<link rel="stylesheet" href="assets/style.css">
</head>
<body class="apidoc" id="api-section-__FILENAME__">
<header id="header">

View file

@ -4,6 +4,13 @@
Please edit settings.json, not settings.json.template
*/
{
// Name your instance!
"title": "Etherpad Lite",
// favicon default name
// alternatively, set up a fully specified Url to your own favicon
"favicon": "favicon.ico",
//Ip and port which etherpad should bind at
"ip": "0.0.0.0",
"port" : 9001,

View file

@ -5,6 +5,7 @@
"restartServer": "ep_etherpad-lite/node/hooks/express:restartServer"
} },
{ "name": "static", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/static:expressCreateServer" } },
{ "name": "i18n", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/i18n:expressCreateServer" } },
{ "name": "specialpages", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages:expressCreateServer" } },
{ "name": "padurlsanitize", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize:expressCreateServer" } },
{ "name": "padreadonly", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padreadonly:expressCreateServer" } },
@ -13,8 +14,15 @@
{ "name": "importexport", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/importexport:expressCreateServer" } },
{ "name": "errorhandling", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling:expressCreateServer" } },
{ "name": "socketio", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/socketio:expressCreateServer" } },
{ "name": "tests", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/tests:expressCreateServer" } },
{ "name": "admin", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/admin:expressCreateServer" } },
{ "name": "adminplugins", "hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminplugins:expressCreateServer",
"socketio": "ep_etherpad-lite/node/hooks/express/adminplugins:socketio" } }
"socketio": "ep_etherpad-lite/node/hooks/express/adminplugins:socketio" }
},
{ "name": "adminsettings", "hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminsettings:expressCreateServer",
"socketio": "ep_etherpad-lite/node/hooks/express/adminsettings:socketio" }
}
]
}

85
src/locales/de.ini Normal file
View file

@ -0,0 +1,85 @@
[de]
index.newPad = Neues Pad
index.createOpenPad = Pad mit folgendem Namen öffnen
pad.toolbar.bold.title = Fett (Strg-B)
pad.toolbar.italic.title = Kursiv (Strg-I)
pad.toolbar.underline.title = Unterstrichen (Strg-U)
pad.toolbar.strikethrough.title = Durchgestrichen
pad.toolbar.ol.title = Nummerierte Liste
pad.toolbar.ul.title = Ungeordnete Liste
pad.toolbar.indent.title = Einrücken
pad.toolbar.unindent.title = Ausrücken
pad.toolbar.undo.title = Rückgängig (Strg-Z)
pad.toolbar.redo.title = Wiederholen (Strg-Y)
pad.toolbar.clearAuthorship.title = Autorenfarben zurücksetzen
pad.toolbar.import_export.title = Import/Export von verschiedenen Dateiformaten
pad.toolbar.timeslider.title = Pad-Geschichte anzeigen
pad.toolbar.savedRevision.title = Diese Revision markieren
pad.toolbar.settings.title = Einstellungen
pad.toolbar.embed.title = Dieses Pad teilen oder einbetten
pad.toolbar.showusers.title = Verbundene Benutzer anzeigen
pad.colorpicker.save = Speichern
pad.colorpicker.cancel = Abbrechen
pad.loading = Laden...
pad.settings.padSettings = Pad Einstellungen
pad.settings.myView = Eigene Ansicht
pad.settings.stickychat = Chat immer anzeigen
pad.settings.colorcheck = Autorenfarben anzeigen
pad.settings.linenocheck = Zeilennummern
pad.settings.fontType = Schriftart:
pad.settings.fontType.normal = Normal
pad.settings.fontType.monospaced = Monospace
pad.settings.language = Sprache:
pad.settings.globalView = Gemeinsame Ansicht
pad.importExport.import_export = Import/Export
pad.importExport.import = Datei oder Dokument hochladen
pad.importExport.successful = Erfolgreich!
pad.importExport.export = Dieses Pad exportieren
pad.importExport.exporthtml = HTML
pad.importExport.exportplain = Reiner Text
pad.importExport.exportword = Microsoft Word
pad.importExport.exportpdf = PDf
pad.importExport.exportopen = ODF (Open Document Format)
pad.importExport.exportdokuwiki = DokuWiki
pad.modals.connected = Verbunden.
pad.modals.reconnecting = Wiederherstellen der Verbindung...
pad.modals.forcereconnect = Erneut Verbinden
pad.modals.uderdup = In einem anderen Fenster geöffnet
pad.modals.userdup.explanation = Dieses Pad scheint in mehr als einem Browser-Fenster auf diesem Computer geöffnet zu sein.
pad.modals.userdup.advice = Um dieses Fenster zu benutzen, verbinden Sie bitte erneut.
pad.modals.unauth = Nicht Authorisiert.
pad.modals.unauth.explanation = Ihre Befugnisse auf dieses Pad zuzugreifen haben sich geädert. Versuchen Sie, erneut zu verbinden.
pad.modals.looping = Verbindung unterbrochen.
pad.modals.looping.explanation = Es gibt Probleme bei der Kommunikation mit dem Synchronisationsserver.
pad.modals.looping.cause = Möglicherweise verläuft Ihre Verbindung durch eine inkompatible Firewall oder einen inkompatiblen Proxy.
pad.modals.initsocketfail = Server nicht erreichbar.
pad.modals.initsocketfail.explanation = Es konnte keine Verbindung zum Synchronisationsserver hergestellt werden.
pad.modals.initsocketfail.cause = Dies könnte an Ihrem Browser oder Ihrer Internet-Verbindung liegen.
pad.modals.slowcommit = Verbindung unterbrochen.
pad.modals.slowcommit.explanation = Der Server reagiert nicht.
pad.modals.slowcommit.cause = Dies könnte an Problemen mit Netzwerk-Konnektivität liegen. Möglicherweise ist der Server aber auch überlastet.
pad.modals.deleted = Entfernt.
pad.modals.deleted.explanation = Dieses Pad wurde entfernt.
pad.modals.disconnected = Verbindung unterbrochen.
pad.modals.disconnected.explanation = Die Verbindung zum Synchronisationsserver wurde unterbrochen.
pad.modals.disconnected.cause = Möglicherweise ist der Server nicht erreichbar. Bitte benachrichtigen Sie uns, falls dies weiterhin passiert.
pad.share = Dieses Pad teilen
pad.share.readonly = Eingeschränkter zugriff (Nur lesen)
pad.share.link = Link
pad.share.emebdcode = In Webseite einbetten
pad.chat = Chat
pad.chat.title = Den Chat für dieses Pad öffnen
timeslider.pageTitle = {{appTitle}} Pad-Geschichte
timeslider.toolbar.returnbutton = Zurück zum Pad
timeslider.toolbar.authors = Autoren:
timeslider.toolbar.authorsList = keine Autoren
timeslider.exportCurrent = Exportiere diese Version als:

77
src/locales/en.ini Normal file
View file

@ -0,0 +1,77 @@
[en]
index.newPad = New Pad
index.createOpenPad = or create/open a Pad with the name:
pad.toolbar.bold.title = Bold (Ctrl-B)
pad.toolbar.italic.title = Italic (Ctrl-I)
pad.toolbar.underline.title = Underline (Ctrl-U)
pad.toolbar.strikethrough.title = Strikethrough
pad.toolbar.ol.title = Ordered list
pad.toolbar.ul.title = UnOrdered List
pad.toolbar.indent.title = Indent
pad.toolbar.unindent.title = Outdent
pad.toolbar.undo.title = Undo (Ctrl-Z)
pad.toolbar.redo.title = Redo (Ctrl-Y)
pad.toolbar.clearAuthorship.title = Clear Authorship Colors
pad.toolbar.import_export.title = Import/Export from/to different file formats
pad.toolbar.timeslider.title = Timeslider
pad.toolbar.savedRevision.title = Saved Revisions
pad.toolbar.settings.title = Settings
pad.toolbar.embed.title = Embed this pad
pad.toolbar.showusers.title = Show the users on this pad
pad.colorpicker.save = Save
pad.colorpicker.cancel = Cancel
pad.loading = Loading...
pad.settings.padSettings = Pad Settings
pad.settings.myView = My View
pad.settings.stickychat = Chat always on screen
pad.settings.colorcheck = Authorship colors
pad.settings.linenocheck = Line numbers
pad.settings.fontType = Font type:
pad.settings.fontType.normal = Normal
pad.settings.fontType.monospaced = Monospace
pad.settings.globalView = Global View
pad.settings.language = Language:
pad.importExport.import_export = Import/Export
pad.importExport.import = Upload any text file or document
pad.importExport.successful = Successful!
pad.importExport.export = Export current pad as
pad.importExport.exporthtml = HTML
pad.importExport.exportplain = Plain text
pad.importExport.exportword = Microsoft Word
pad.importExport.exportpdf = PDf
pad.importExport.exportopen = ODF (Open Document Format)
pad.importExport.exportdokuwiki = DokuWiki
pad.modals.connected = Connected.
pad.modals.reconnecting = Reconnecting to your pad..
pad.modals.forcereconnect = Force reconnect
pad.modals.uderdup = Open in another window
pad.modals.userdup.explanation = This pad seems to be opened in more than one browser window on this computer.
pad.modals.userdup.advice = Reconnect to use this windows instead.
pad.modals.unauth = Not authorized
pad.modals.unauth.explanation = Your permissions have changes while viewing this page. Try to reconnect.
pad.modals.looping = Disconnected.
pad.modals.looping.explanation = We're having problem communicating to the synchronization server.
pad.modals.looping.cause = Perhaps their connection runs through an incompatible firewall or incompatible proxy.
pad.modals.initsocketfail = Server is unreachable.
pad.modals.initsocketfail.explanation = Couldn't connect to the synchronization server.
pad.modals.initsocketfail.cause = This could be because of your browser or Internet connection. #sounds stupid!
pad.modals.slowcommit = Disconnected.
pad.modals.slowcommit.explanation = The server is not responding.
pad.modals.slowcommit.cause = This could be due to problems with network connectivity.
pad.modals.deleted = Deleted.
pad.modals.deleted.explanation = This pad has been removed.
pad.modals.disconnected = You have been disconnected.
pad.modals.disconnected.explanation = The connection to the server was lost
pad.modals.disconnected.cause = The server may be unavailable. Please notify us if this continues to happen.
pad.share = Share this pad
pad.share.readonly = Read only
pad.share.link = Link
pad.share.emebdcode = Embed URL
pad.chat = Chat
pad.chat.title = Open the chat for this pad.
timeslider.pageTitle = {{appTitle}} Timeslider
timeslider.toolbar.returnbutton = Return to pad
timeslider.toolbar.authors = Authors:
timeslider.toolbar.authorsList = No Authors
timeslider.exportCurrent = Export current version as:

86
src/locales/fr.ini Normal file
View file

@ -0,0 +1,86 @@
[fr]
index.newPad = Nouveau Pad
index.createOpenPad = ou créer/ouvrir un Pad intitulé
pad.toolbar.bold.title = Gras (Ctrl-B)
pad.toolbar.italic.title = Italique (Ctrl-I)
pad.toolbar.underline.title = Souligner (Ctrl-U)
pad.toolbar.strikethrough.title = Barrer
pad.toolbar.ol.title = Liste ordonnée
pad.toolbar.ul.title = Liste non-ordonnée
pad.toolbar.indent.title = Indenter
pad.toolbar.unindent.title = Désindenter
pad.toolbar.undo.title = Annuler (Ctrl-Z)
pad.toolbar.redo.title = Rétablir (Ctrl-Y)
pad.toolbar.clearAuthorship.title = Effacer les couleurs identifant les auteurs
pad.toolbar.import_export.title = Importer/Exporter de/vers un format de fichier différent
pad.toolbar.timeslider.title = Navigateur d'historique
pad.toolbar.savedRevision.title = Versions enregistrées
pad.toolbar.settings.title = Paramètres
pad.toolbar.embed.title = Intégrer ce Pad
pad.toolbar.showusers.title = Afficher les utilisateurs du Pad
pad.colorpicker.save = Sauver
pad.colorpicker.cancel = Annuler
pad.loading = Chargement...
pad.settings.padSettings = Paramètres du Pad
pad.settings.myView = Ma vue
pad.settings.stickychat = Messagerie toujours affichée
pad.settings.colorcheck = Couleurs d'identification
pad.settings.linenocheck = Numéros des lignes
pad.settings.fontType = Type de police:
pad.settings.fontType.normal = Normal
pad.settings.fontType.monospaced = Monospace
pad.settings.globalView = Vue d'ensemble
pad.settings.language = Langue:
pad.importExport.import_export = Importer/Exporter
pad.importExport.import = Charger un texte ou un document
pad.importExport.successful = Traitement effectué!
pad.importExport.export = Exporter ce Pad vers
pad.importExport.exporthtml = HTML
pad.importExport.exportplain = Texte brut
pad.importExport.exportword = Microsoft Word
pad.importExport.exportpdf = PDf
pad.importExport.exportopen = ODF (Open Document Format)
pad.importExport.exportdokuwiki = DokuWiki
pad.modals.connected = Connecté.
pad.modals.reconnecting = Reconnexion vers votre Pad...
pad.modals.forcereconnect = Forcer la reconnexion.
pad.modals.uderdup = Ouvrir dans une nouvelle fenêtre
pad.modals.userdup.explanation = Ce Pad semble avoir été ouvert dans plusieurs fenêtres de votre fureteur sur cet ordinateur.
pad.modals.userdup.advice = Se reconnecter en utilisant cette fenêtre.
pad.modals.unauth = Not authorized Non authorisé
pad.modals.unauth.explanation = Vos permissions ont été changées lors de la visualisation de cette page. Essayer de vous reconnecter.
pad.modals.looping = Disconnected. Déconnecté.
pad.modals.looping.explanation = Nous éprouvons un problème de communication au serveur de synchronisation.
pad.modals.looping.cause = Il est possible que leur connection soit protégée par un pare-feu incompatible ou un serveur proxy incompatible.
pad.modals.initsocketfail = Le serveur est introuvable.
pad.modals.initsocketfail.explanation = Impossible de se connecter au serveur de synchronisation.
pad.modals.initsocketfail.cause = La cause de ce problème peut être liée à votre fureteur web.
pad.modals.slowcommit = Disconnected. Déconnecté
pad.modals.slowcommit.explanation = Le serveur ne répond pas.
pad.modals.slowcommit.cause = La cause de ce problème peut être liée à une erreur de connectivité du réseau.
pad.modals.deleted = Supprimé.
pad.modals.deleted.explanation = Ce Pad a été supprimé.
pad.modals.disconnected = Vous avez été déconnecté.
pad.modals.disconnected.explanation = La connexion au serveur a échoué.
pad.modals.disconnected.cause = Ce serveur est possiblement hors-ligne. Veuillez nous joindre si le problème persiste.
pad.share = Partager ce Pad
pad.share.readonly = Lecture seule
pad.share.link = Lien
pad.share.emebdcode = Lien à intégrer
pad.chat = Messagerie
pad.chat.title = Ouvrir la messagerie liée au Pad.
timeslider.pageTitle = {{appTitle}} Curseur temporel
timeslider.toolbar.returnbutton = Retour à ce Pad.
timeslider.toolbar.authors = Auteurs:
timeslider.toolbar.authorsList = Aucun auteurs
timeslider.exportCurrent = Exporter version actuelle vers:

View file

@ -30,6 +30,7 @@ exports.info = {
block_stack: [],
blocks: {},
file_stack: [],
args: []
};
exports._init = function (b, recursive) {
@ -81,7 +82,8 @@ exports.end_define_block = function () {
exports.end_block = function () {
var name = exports.info.block_stack[exports.info.block_stack.length-1];
var args = {content: exports.end_define_block()};
var renderContext = exports.info.args[exports.info.args.length-1];
var args = {content: exports.end_define_block(), renderContext: renderContext};
hooks.callAll("eejsBlock_" + name, args);
exports.info.buf.push(args.content);
}
@ -118,10 +120,13 @@ exports.require = function (name, args, mod) {
args.e = exports;
args.require = require;
var template = '<% e._init(buf); %>' + fs.readFileSync(ejspath).toString() + '<% e._exit(); %>';
exports.info.args.push(args);
exports.info.file_stack.push({path: ejspath, inherit: []});
var res = ejs.render(template, args);
exports.info.file_stack.pop();
exports.info.args.pop();
return res;
}

View file

@ -619,7 +619,7 @@ exports.updatePadClients = function(pad, callback)
//https://github.com/caolan/async#whilst
//send them all new changesets
async.whilst(
function (){ return sessioninfos[session].rev < pad.getHeadRevisionNumber()},
function (){ return sessioninfos[session] && sessioninfos[session].rev < pad.getHeadRevisionNumber()},
function(callback)
{
var author, revChangeset, currentTime;

View file

@ -0,0 +1,8 @@
var eejs = require('ep_etherpad-lite/node/eejs');
exports.expressCreateServer = function (hook_name, args, cb) {
args.app.get('/admin', function(req, res) {
res.send( eejs.require("ep_etherpad-lite/templates/admin/index.html", {}) );
});
}

View file

@ -0,0 +1,54 @@
var path = require('path');
var eejs = require('ep_etherpad-lite/node/eejs');
var settings = require('ep_etherpad-lite/node/utils/Settings');
var installer = require('ep_etherpad-lite/static/js/pluginfw/installer');
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
var fs = require('fs');
exports.expressCreateServer = function (hook_name, args, cb) {
args.app.get('/admin/settings', function(req, res) {
var render_args = {
settings: "",
search_results: {},
errors: []
};
res.send( eejs.require("ep_etherpad-lite/templates/admin/settings.html", render_args) );
});
}
exports.socketio = function (hook_name, args, cb) {
var io = args.io.of("/settings");
io.on('connection', function (socket) {
if (!socket.handshake.session.user || !socket.handshake.session.user.is_admin) return;
socket.on("load", function (query) {
fs.readFile('settings.json', 'utf8', function (err,data) {
if (err) {
return console.log(err);
}
else
{
socket.emit("settings", {results: data});
}
});
});
socket.on("saveSettings", function (settings) {
fs.writeFile('settings.json', settings, function (err) {
if (err) throw err;
socket.emit("saveprogress", "saved");
});
});
socket.on("restartServer", function () {
console.log("Admin request to restart server through a socket on /admin/settings");
settings.reloadSettings();
hooks.aCallAll("restartServer", {}, function () {});
});
});
}

View file

@ -12,12 +12,32 @@ exports.expressCreateServer = function (hook_name, args, cb) {
//serve robots.txt
args.app.get('/robots.txt', function(req, res)
{
var filePath = path.normalize(__dirname + "/../../../static/robots.txt");
res.sendfile(filePath);
var filePath = path.normalize(__dirname + "/../../../static/custom/robots.txt");
res.sendfile(filePath, function(err)
{
//there is no custom favicon, send the default robots.txt which dissallows all
if(err)
{
filePath = path.normalize(__dirname + "/../../../static/robots.txt");
res.sendfile(filePath);
}
});
});
//serve favicon.ico
args.app.get('/favicon.ico', function(req, res)
//serve pad.html under /p
args.app.get('/p/:pad', function(req, res, next)
{
res.send(eejs.require("ep_etherpad-lite/templates/pad.html", {req: req}));
});
//serve timeslider.html under /p/$padname/timeslider
args.app.get('/p/:pad/timeslider', function(req, res, next)
{
res.send(eejs.require("ep_etherpad-lite/templates/timeslider.html", {req: req}));
});
//serve favicon.ico from all path levels except as a pad name
args.app.get( /\/favicon.ico$/, function(req, res)
{
var filePath = path.normalize(__dirname + "/../../../static/custom/favicon.ico");
res.sendfile(filePath, function(err)
@ -31,16 +51,5 @@ exports.expressCreateServer = function (hook_name, args, cb) {
});
});
//serve pad.html under /p
args.app.get('/p/:pad', function(req, res, next)
{
res.send(eejs.require("ep_etherpad-lite/templates/pad.html"));
});
//serve timeslider.html under /p/$padname/timeslider
args.app.get('/p/:pad/timeslider', function(req, res, next)
{
res.send(eejs.require("ep_etherpad-lite/templates/timeslider.html"));
});
}

View file

@ -0,0 +1,46 @@
var path = require("path");
var fs = require("fs");
exports.expressCreateServer = function (hook_name, args, cb) {
args.app.get('/tests/frontend/specs_list.js', function(req, res){
fs.readdir('tests/frontend/specs', function(err, files){
if(err){ return res.send(500); }
res.send("var specs_list = " + JSON.stringify(files.sort()) + ";\n");
});
});
var url2FilePath = function(url){
var subPath = url.substr("/tests/frontend".length);
if (subPath == ""){
subPath = "index.html"
}
subPath = subPath.split("?")[0];
var filePath = path.normalize(__dirname + "/../../../../tests/frontend/")
filePath += subPath.replace("..", "");
return filePath;
}
args.app.get('/tests/frontend/specs/*', function (req, res) {
var specFilePath = url2FilePath(req.url);
var specFileName = path.basename(specFilePath);
fs.readFile(specFilePath, function(err, content){
if(err){ return res.send(500); }
content = "describe(" + JSON.stringify(specFileName) + ", function(){ " + content + " });";
res.send(content);
});
});
args.app.get('/tests/frontend/*', function (req, res) {
var filePath = url2FilePath(req.url);
res.sendfile(filePath);
});
args.app.get('/tests/frontend', function (req, res) {
res.redirect('/tests/frontend/');
});
}

33
src/node/hooks/i18n.js Normal file
View file

@ -0,0 +1,33 @@
var Globalize = require('globalize')
, fs = require('fs')
, path = require('path')
, express = require('express')
var localesPath = __dirname+"/../../locales";
var localeIndex = '[*]\r\n@import url(locales/en.ini)\r\n';
exports.availableLangs = {en: 'English'};
fs.readdir(localesPath, function(er, files) {
files.forEach(function(locale) {
locale = locale.split('.')[0]
if(locale.toLowerCase() == 'en') return;
// build locale index
localeIndex += '['+locale+']\r\n@import url(locales/'+locale+'.ini)\r\n'
require('globalize/lib/cultures/globalize.culture.'+locale+'.js')
var culture = Globalize.cultures[locale];
exports.availableLangs[culture.name] = culture.nativeName;
})
})
exports.expressCreateServer = function(n, args) {
args.app.use('/locales', express.static(localesPath));
args.app.get('/locales.ini', function(req, res) {
res.send(localeIndex);
})
}

View file

@ -29,6 +29,16 @@ var vm = require('vm');
/* Root path of the installation */
exports.root = path.normalize(path.join(npm.dir, ".."));
/**
* The app title, visible e.g. in the browser window
*/
exports.title = "Etherpad Lite";
/**
* The app favicon fully specified url, visible e.g. in the browser window
*/
exports.favicon = "favicon.ico";
/**
* The IP ep-lite should listen to
*/
@ -102,50 +112,58 @@ exports.abiwordAvailable = function()
}
}
// Discover where the settings file lives
var settingsFilename = argv.settings || "settings.json";
settingsFilename = path.resolve(path.join(root, settingsFilename));
var settingsStr;
try{
//read the settings sync
settingsStr = fs.readFileSync(settingsFilename).toString();
} catch(e){
console.warn('No settings file found. Continuing using defaults!');
}
// try to parse the settings
var settings;
try {
if(settingsStr) {
settings = vm.runInContext('exports = '+settingsStr, vm.createContext(), "settings.json");
exports.reloadSettings = function reloadSettings() {
// Discover where the settings file lives
var settingsFilename = argv.settings || "settings.json";
settingsFilename = path.resolve(path.join(root, settingsFilename));
var settingsStr;
try{
//read the settings sync
settingsStr = fs.readFileSync(settingsFilename).toString();
} catch(e){
console.warn('No settings file found. Continuing using defaults!');
}
}catch(e){
console.error('There was an error processing your settings.json file: '+e.message);
process.exit(1);
}
//loop trough the settings
for(var i in settings)
{
//test if the setting start with a low character
if(i.charAt(0).search("[a-z]") !== 0)
// try to parse the settings
var settings;
try {
if(settingsStr) {
settings = vm.runInContext('exports = '+settingsStr, vm.createContext(), "settings.json");
}
}catch(e){
console.error('There was an error processing your settings.json file: '+e.message);
process.exit(1);
}
//loop trough the settings
for(var i in settings)
{
console.warn("Settings should start with a low character: '" + i + "'");
//test if the setting start with a low character
if(i.charAt(0).search("[a-z]") !== 0)
{
console.warn("Settings should start with a low character: '" + i + "'");
}
//we know this setting, so we overwrite it
//or it's a settings hash, specific to a plugin
if(exports[i] !== undefined || i.indexOf('ep_')==0)
{
exports[i] = settings[i];
}
//this setting is unkown, output a warning and throw it away
else
{
console.warn("Unknown Setting: '" + i + "'. This setting doesn't exist or it was removed");
}
}
//we know this setting, so we overwrite it
if(exports[i] !== undefined)
{
exports[i] = settings[i];
}
//this setting is unkown, output a warning and throw it away
else
{
console.warn("Unknown Setting: '" + i + "'. This setting doesn't exist or it was removed");
if(exports.dbType === "dirty"){
console.warn("DirtyDB is used. This is fine for testing but not recommended for production.")
}
}
if(exports.dbType === "dirty"){
console.warn("DirtyDB is used. This is fine for testing but not recommended for production.")
}
// initially load settings
exports.reloadSettings();

View file

@ -17,7 +17,7 @@
"resolve" : "0.2.x",
"socket.io" : "0.9.x",
"ueberDB" : "0.1.8",
"async" : "0.1.22",
"async" : "0.1.x",
"express" : "3.x",
"connect" : "2.4.x",
"clean-css" : "0.3.2",
@ -35,14 +35,16 @@
"security" : "1.0.0",
"tinycon" : "0.0.1",
"underscore" : "1.3.1",
"unorm" : "1.0.0"
"unorm" : "1.0.0",
"globalize" : "0.1.1"
},
"bin": { "etherpad-lite": "./node/server.js" },
"devDependencies": {
"jshint" : "*"
"jshint" : "*",
"wd" : "0.0.26"
},
"engines" : { "node" : ">=0.6.0",
"npm" : ">=1.0"
},
"version" : "1.1.5"
"version" : "1.2.0"
}

View file

@ -119,4 +119,12 @@ td, th {
right: 10px;
padding: 2px;
overflow: auto;
}
}
.settings {
margin-top:10px;
width:100%;
min-height:600px;
}
#response{
display:inline;
}

View file

@ -1,4 +1,3 @@
/* These CSS rules are included in both the outer and inner ACE iframe.
Also see inner.css, included only in the inner one.
*/
@ -39,7 +38,7 @@ ul.list-bullet6 { list-style-type: square; }
ul.list-bullet7 { list-style-type: disc; }
ul.list-bullet8 { list-style-type: circle; }
ol.list-number1 { margin-left: 1.5em; }
ol.list-number1 { margin-left: 1.9em; }
ol.list-number2 { margin-left: 3em; }
ol.list-number3 { margin-left: 4.5em; }
ol.list-number4 { margin-left: 6em; }
@ -127,7 +126,7 @@ body.doesWrap {
.sidedivdelayed { /* class set after sizes are set */
background-color: #eee;
color: #888 !important;
border-right: 1px solid #999;
border-right: 1px solid #ccc;
}
.sidedivhidden {
display: none;

View file

@ -748,6 +748,15 @@ input[type=checkbox] {
.popup p {
margin: 5px 0
}
.popup select {
background: #fff;
padding: 2px;
height: 24px;
border-radius: 3px;
border: 1px solid #ccc;
outline: none;
min-width: 105px;
}
.column {
float: left;
width: 50%;

View file

@ -0,0 +1,180 @@
// Autosize 1.13 - jQuery plugin for textareas
// (c) 2012 Jack Moore - jacklmoore.com
// license: www.opensource.org/licenses/mit-license.php
(function ($) {
var
defaults = {
className: 'autosizejs',
append: "",
callback: false
},
hidden = 'hidden',
borderBox = 'border-box',
lineHeight = 'lineHeight',
copy = '<textarea tabindex="-1" style="position:absolute; top:-9999px; left:-9999px; right:auto; bottom:auto; -moz-box-sizing:content-box; -webkit-box-sizing:content-box; box-sizing:content-box; word-wrap:break-word; height:0 !important; min-height:0 !important; overflow:hidden;"/>',
// line-height is omitted because IE7/IE8 doesn't return the correct value.
copyStyle = [
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'letterSpacing',
'textTransform',
'wordSpacing',
'textIndent'
],
oninput = 'oninput',
onpropertychange = 'onpropertychange',
test = $(copy)[0];
// For testing support in old FireFox
test.setAttribute(oninput, "return");
if ($.isFunction(test[oninput]) || onpropertychange in test) {
// test that line-height can be accurately copied to avoid
// incorrect value reporting in old IE and old Opera
$(test).css(lineHeight, '99px');
if ($(test).css(lineHeight) === '99px') {
copyStyle.push(lineHeight);
}
$.fn.autosize = function (options) {
options = $.extend({}, defaults, options || {});
return this.each(function () {
var
ta = this,
$ta = $(ta),
mirror,
minHeight = $ta.height(),
maxHeight = parseInt($ta.css('maxHeight'), 10),
active,
i = copyStyle.length,
resize,
boxOffset = 0,
value = ta.value,
callback = $.isFunction(options.callback);
if ($ta.css('box-sizing') === borderBox || $ta.css('-moz-box-sizing') === borderBox || $ta.css('-webkit-box-sizing') === borderBox){
boxOffset = $ta.outerHeight() - $ta.height();
}
if ($ta.data('mirror') || $ta.data('ismirror')) {
// if autosize has already been applied, exit.
// if autosize is being applied to a mirror element, exit.
return;
} else {
mirror = $(copy).data('ismirror', true).addClass(options.className)[0];
resize = $ta.css('resize') === 'none' ? 'none' : 'horizontal';
$ta.data('mirror', $(mirror)).css({
overflow: hidden,
overflowY: hidden,
wordWrap: 'break-word',
resize: resize
});
}
// Opera returns '-1px' when max-height is set to 'none'.
maxHeight = maxHeight && maxHeight > 0 ? maxHeight : 9e4;
// Using mainly bare JS in this function because it is going
// to fire very often while typing, and needs to very efficient.
function adjust() {
var height, overflow, original;
// the active flag keeps IE from tripping all over itself. Otherwise
// actions in the adjust function will cause IE to call adjust again.
if (!active) {
active = true;
mirror.value = ta.value + options.append;
mirror.style.overflowY = ta.style.overflowY;
original = parseInt(ta.style.height,10);
// Update the width in case the original textarea width has changed
mirror.style.width = $ta.css('width');
// Needed for IE to reliably return the correct scrollHeight
mirror.scrollTop = 0;
// Set a very high value for scrollTop to be sure the
// mirror is scrolled all the way to the bottom.
mirror.scrollTop = 9e4;
height = mirror.scrollTop;
overflow = hidden;
if (height > maxHeight) {
height = maxHeight;
overflow = 'scroll';
} else if (height < minHeight) {
height = minHeight;
}
height += boxOffset;
ta.style.overflowY = overflow;
if (original !== height) {
ta.style.height = height + 'px';
if (callback) {
options.callback.call(ta);
}
}
// This small timeout gives IE a chance to draw it's scrollbar
// before adjust can be run again (prevents an infinite loop).
setTimeout(function () {
active = false;
}, 1);
}
}
// mirror is a duplicate textarea located off-screen that
// is automatically updated to contain the same text as the
// original textarea. mirror always has a height of 0.
// This gives a cross-browser supported way getting the actual
// height of the text, through the scrollTop property.
while (i--) {
mirror.style[copyStyle[i]] = $ta.css(copyStyle[i]);
}
$('body').append(mirror);
if (onpropertychange in ta) {
if (oninput in ta) {
// Detects IE9. IE9 does not fire onpropertychange or oninput for deletions,
// so binding to onkeyup to catch most of those occassions. There is no way that I
// know of to detect something like 'cut' in IE9.
ta[oninput] = ta.onkeyup = adjust;
} else {
// IE7 / IE8
ta[onpropertychange] = adjust;
}
} else {
// Modern Browsers
ta[oninput] = adjust;
// The textarea overflow is now hidden. But Chrome doesn't reflow the text after the scrollbars are removed.
// This is a hack to get Chrome to reflow it's text.
ta.value = '';
ta.value = value;
}
$(window).resize(adjust);
// Allow for manual triggering if needed.
$ta.bind('autosize', adjust);
// Call adjust in case the textarea already contains text.
adjust();
});
};
} else {
// Makes no changes for older browsers (FireFox3- and Safari4-)
$.fn.autosize = function (callback) {
return this;
};
}
}(jQuery));

View file

@ -0,0 +1,61 @@
/*! JSON.minify()
v0.1 (c) Kyle Simpson
MIT License
*/
(function(global){
if (typeof global.JSON == "undefined" || !global.JSON) {
global.JSON = {};
}
global.JSON.minify = function(json) {
var tokenizer = /"|(\/\*)|(\*\/)|(\/\/)|\n|\r/g,
in_string = false,
in_multiline_comment = false,
in_singleline_comment = false,
tmp, tmp2, new_str = [], ns = 0, from = 0, lc, rc
;
tokenizer.lastIndex = 0;
while (tmp = tokenizer.exec(json)) {
lc = RegExp.leftContext;
rc = RegExp.rightContext;
if (!in_multiline_comment && !in_singleline_comment) {
tmp2 = lc.substring(from);
if (!in_string) {
tmp2 = tmp2.replace(/(\n|\r|\s)*/g,"");
}
new_str[ns++] = tmp2;
}
from = tokenizer.lastIndex;
if (tmp[0] == "\"" && !in_multiline_comment && !in_singleline_comment) {
tmp2 = lc.match(/(\\)*$/);
if (!in_string || !tmp2 || (tmp2[0].length % 2) == 0) { // start of string with ", or unescaped " character found to end string
in_string = !in_string;
}
from--; // include " character in next catch
rc = json.substring(from);
}
else if (tmp[0] == "/*" && !in_string && !in_multiline_comment && !in_singleline_comment) {
in_multiline_comment = true;
}
else if (tmp[0] == "*/" && !in_string && in_multiline_comment && !in_singleline_comment) {
in_multiline_comment = false;
}
else if (tmp[0] == "//" && !in_string && !in_multiline_comment && !in_singleline_comment) {
in_singleline_comment = true;
}
else if ((tmp[0] == "\n" || tmp[0] == "\r") && !in_string && !in_multiline_comment && in_singleline_comment) {
in_singleline_comment = false;
}
else if (!in_multiline_comment && !in_singleline_comment && !(/\n|\r|\s/.test(tmp[0]))) {
new_str[ns++] = tmp[0];
}
}
new_str[ns++] = rc;
return new_str.join("");
};
})(this);

View file

@ -0,0 +1,70 @@
$(document).ready(function () {
var socket,
loc = document.location,
port = loc.port == "" ? (loc.protocol == "https:" ? 443 : 80) : loc.port,
url = loc.protocol + "//" + loc.hostname + ":" + port + "/",
pathComponents = location.pathname.split('/'),
// Strip admin/plugins
baseURL = pathComponents.slice(0,pathComponents.length-2).join('/') + '/',
resource = baseURL.substring(1) + "socket.io";
//connect
socket = io.connect(url, {resource : resource}).of("/settings");
socket.on('settings', function (settings) {
/* Check to make sure the JSON is clean before proceeding */
if(isJSONClean(settings.results))
{
$('.settings').append(settings.results);
$('.settings').focus();
$('.settings').autosize();
}
else{
alert("YOUR JSON IS BAD AND YOU SHOULD FEEL BAD");
}
});
/* When the admin clicks save Settings check the JSON then send the JSON back to the server */
$('#saveSettings').on('click', function(){
var editedSettings = $('.settings').val();
if(isJSONClean(editedSettings)){
// JSON is clean so emit it to the server
socket.emit("saveSettings", $('.settings').val());
}else{
alert("YOUR JSON IS BAD AND YOU SHOULD FEEL BAD")
$('.settings').focus();
}
});
/* Tell Etherpad Server to restart */
$('#restartEtherpad').on('click', function(){
socket.emit("restartServer");
});
socket.on('saveprogress', function(progress){
$('#response').show();
$('#response').text(progress);
$('#response').fadeOut('slow');
});
socket.emit("load"); // Load the JSON from the server
});
function isJSONClean(data){
var cleanSettings = JSON.minify(data);
try{
var response = jQuery.parseJSON(cleanSettings);
}
catch(e){
return false; // the JSON failed to be parsed
}
if(typeof response !== 'object'){
return false;
}else{
return true;
}
}

View file

@ -150,7 +150,7 @@ var chat = (function()
$("#chatinput").keypress(function(evt)
{
//if the user typed enter, fire the send
if(evt.which == 13)
if(evt.which == 13 || evt.which == 10)
{
evt.preventDefault();
self.send();

View file

@ -516,7 +516,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class
{
var type;
var rr = cls && /(?:^| )list-([a-z]+[12345678])\b/.exec(cls);
type = rr && rr[1] || "bullet" + String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1));
type = rr && rr[1] || (tname == "ul" ? "bullet" : "number") + String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1));
oldListTypeOrNull = (_enterList(state, type) || 'none');
}
else if ((tname == "div" || tname == "p") && cls && cls.match(/(?:^| )ace-line\b/))

1028
src/static/js/l10n.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -64,7 +64,13 @@ function createCookie(name, value, days, path)
if(!path)
path = "/";
document.cookie = name + "=" + value + expires + "; path=" + path;
//Check if the browser is IE and if so make sure the full path is set in the cookie
if(navigator.appName=='Microsoft Internet Explorer'){
document.cookie = name + "=" + value + expires + "; path="+document.location;
}
else{
document.cookie = name + "=" + value + expires + "; path=" + path;
}
}
function readCookie(name)
@ -105,6 +111,7 @@ function getParams()
var IsnoColors = params["noColors"];
var rtl = params["rtl"];
var alwaysShowChat = params["alwaysShowChat"];
var lang = params["lang"];
if(IsnoColors)
{
@ -167,6 +174,13 @@ function getParams()
chat.stickToScreen();
}
}
if(lang)
{
if(lang !== "")
{
document.webL10n.setLanguage(lang);
}
}
}
function getUrlVars()
@ -383,6 +397,10 @@ function handshake()
});
// Bind the colorpicker
var fb = $('#colorpicker').farbtastic({ callback: '#mycolorpickerpreview', width: 220});
// Bind the read only button
$('#readonlyinput').on('click',function(){
padeditbar.setEmbedLinks();
});
}
var pad = {
@ -441,6 +459,7 @@ var pad = {
{
pad.collabClient.sendClientMessage(msg);
},
createCookie: createCookie,
init: function()
{

View file

@ -75,6 +75,11 @@ var padeditor = (function()
{
pad.changeViewOption('useMonospaceFont', $("#viewfontmenu").val() == 'monospace');
});
$("#languagemenu").val(document.webL10n.getLanguage());
$("#languagemenu").change(function() {
pad.createCookie("language",$("#languagemenu").val(),null,'/');
document.webL10n.setLanguage($("#languagemenu").val());
});
},
setViewOptions: function(newOptions)
{

View file

@ -0,0 +1,20 @@
<html>
<head>
<title>Etherpad Lite Admin Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
<link rel="stylesheet" href="../static/css/admin.css">
<script src="../static/js/jquery.js"></script>
<script src="../socket.io/socket.io.js"></script>
</head>
<body>
<div id="wrapper">
<h1>Etherpad Lite Admin Dashboard</h1>
<div>
<a href="../admin/plugins">Install and Uninstall plugins</a>
</div>
<div>
<a href="../admin/settings">Modify Server and Plugin Settings</a>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,34 @@
<html>
<head>
<title>Settings manager</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
<link rel="stylesheet" href="../static/css/admin.css">
<script src="../static/js/jquery.js"></script>
<script src="../socket.io/socket.io.js"></script>
<script src="../static/js/admin/minify.json.js"></script>
<script src="../static/js/admin/settings.js"></script>
<script src="../static/js/admin/jquery.autosize.js"></script>
</head>
<body>
<div id="wrapper">
<% if (errors.length) { %>
<div class="errors">
<% errors.forEach(function (item) { %>
<div class="error"><%= item.toString() %></div>
<% }) %>
</div>
<% } %>
<h1>Etherpad Lite Settings</h1>
<a href='https://github.com/Pita/etherpad-lite/wiki/Example-Production-Settings.JSON'>Example production settings template</a>
<a href='https://github.com/Pita/etherpad-lite/wiki/Example-Development-Settings.JSON'>Example development settings template</a>
<textarea class="settings"></textarea>
<input type="button" class="settingsButton" id="saveSettings" value="Save Settings">
<input type="button" class="settingsButton" id="restartEtherpad" value="Restart Etherpad">
<div id="response"></div>
</div>
</body>
</html>

View file

@ -1,7 +1,10 @@
<%
var settings = require("ep_etherpad-lite/node/utils/Settings");
%>
<!doctype html>
<html>
<title>Etherpad Lite</title>
<title><%=settings.title%></title>
<script>
/*
|@licstart The following is the entire license notice for the
@ -28,8 +31,8 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
<link rel="shortcut icon" href="favicon.ico">
<link rel="resource" type="application/l10n" href="locales.ini" />
<link rel="shortcut icon" href="<%=settings.favicon%>">
<style>
html, body {
@ -145,8 +148,8 @@
<div id="wrapper">
<div id="inner">
<div id="button" onclick="go2Random()" class="translate">New Pad</div>
<div id="label" class="translate">or create/open a Pad with the name</div>
<div id="button" onclick="go2Random()" data-l10n-id="index.newPad"></div>
<div id="label" data-l10n-id="index.createOpenPad"></div>
<form action="#" onsubmit="go2Name();return false;">
<input type="text" id="padname" autofocus x-webkit-speech>
<button type="submit">OK</button>
@ -181,8 +184,14 @@
return randomstring;
}
(function(document) {
// Set language for l10n
var language = document.cookie.match(/language=(\w{2})/);
if(language) document.documentElement.lang = language[1];
})(document)
// start the custom js
if (typeof customStart == "function") customStart();
</script>
<script type="text/javascript" src="static/js/l10n.js"></script>
</html>

View file

@ -1,9 +1,10 @@
<%
var settings = require("ep_etherpad-lite/node/utils/Settings");
var settings = require("ep_etherpad-lite/node/utils/Settings")
, langs = require("ep_etherpad-lite/node/hooks/i18n").availableLangs
%>
<!doctype html>
<html>
<title>Etherpad Lite</title>
<title><%=settings.title%></title>
<script>
/*
|@licstart The following is the entire license notice for the
@ -32,7 +33,10 @@
<meta name="robots" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
<link rel="shortcut icon" href="../favicon.ico">
<link rel="resource" type="application/l10n" href="../locales.ini" />
<link rel="shortcut icon" href="<%=settings.favicon%>">
<% e.begin_block("styles"); %>
<link href="../static/css/pad.css" rel="stylesheet">
@ -50,60 +54,60 @@
<ul class="menu_left">
<% e.begin_block("editbarMenuLeft"); %>
<li class="acl-write" id="bold" data-key="bold">
<a class="grouped-left" title="Bold (ctrl-B)">
<a class="grouped-left" data-l10n-id="pad.toolbar.bold">
<span class="buttonicon buttonicon-bold"></span>
</a>
</li>
<li class="acl-write" id="italic" data-key="italic">
<a class="grouped-middle" title="Italics (ctrl-I)">
<a class="grouped-middle" data-l10n-id="pad.toolbar.italic">
<span class="buttonicon buttonicon-italic"></span>
</a>
</li>
<li class="acl-write" id="underline" data-key="underline">
<a class="grouped-middle" title="Underline (ctrl-U)">
<a class="grouped-middle" data-l10n-id="pad.toolbar.underline">
<span class="buttonicon buttonicon-underline"></span>
</a>
</li>
<li class="acl-write" id="strikethrough" data-key="strikethrough">
<a class="grouped-right" title="Strikethrough">
<a class="grouped-right" data-l10n-id="pad.toolbar.strikethrough">
<span class="buttonicon buttonicon-strikethrough"></span>
</a>
</li>
<li class="acl-write separator"></li>
<li class="acl-write" id="oderedlist" data-key="insertorderedlist">
<a class="grouped-left" title="Toggle Ordered List">
<a class="grouped-left" data-l10n-id="pad.toolbar.ol">
<span class="buttonicon buttonicon-insertorderedlist"></span>
</a>
</li>
<li class="acl-write" id="unoderedlist" data-key="insertunorderedlist">
<a class="grouped-middle" title="Toggle Bullet List">
<a class="grouped-middle" data-l10n-id="pad.toolbar.ul">
<span class="buttonicon buttonicon-insertunorderedlist"></span>
</a>
</li>
<li class="acl-write" id="indent" data-key="indent">
<a class="grouped-middle" title="Indent">
<a class="grouped-middle" data-l10n-id="pad.toolbar.indent">
<span class="buttonicon buttonicon-indent"></span>
</a>
</li>
<li class="acl-write" id="outdent" data-key="outdent">
<a class="grouped-right" title="Unindent">
<a class="grouped-right" data-l10n-id="pad.toolbar.unindent">
<span class="buttonicon buttonicon-outdent"></span>
</a>
</li>
<li class="acl-write separator"></li>
<li class="acl-write" id="undo" data-key="undo">
<a class="grouped-left" title="Undo (ctrl-Z)">
<a class="grouped-left" data-l10n-id="pad.toolbar.undo">
<span class="buttonicon buttonicon-undo"></span>
</a>
</li>
<li class="acl-write" id="redo" data-key="redo">
<a class="grouped-right" title="Redo (ctrl-Y)">
<a class="grouped-right" data-l10n-id="pad.toolbar.redo">
<span class="buttonicon buttonicon-redo"></span>
</a>
</li>
<li class="acl-write separator"></li>
<li class="acl-write" id="clearAuthorship" data-key="clearauthorship">
<a title="Clear Authorship Colors">
<a data-l10n-id="pad.toolbar.clearAuthorship">
<span class="buttonicon buttonicon-clearauthorship"></span>
</a>
</li>
@ -112,34 +116,34 @@
<ul class="menu_right">
<% e.begin_block("editbarMenuRight"); %>
<li data-key="import_export">
<a class="grouped-left" id="importexportlink" title="Import/Export from/to different document formats">
<a class="grouped-left" id="importexportlink" data-l10n-id="pad.toolbar.import_export">
<span class="buttonicon buttonicon-import_export"></span>
</a>
</li>
<li onClick="document.location = document.location.pathname+ '/timeslider'">
<a id="timesliderlink" class="grouped-middle" title="Show the history of this pad">
<a id="timesliderlink" class="grouped-middle" data-l10n-id="pad.toolbar.timeslider">
<span class="buttonicon buttonicon-history"></span>
</a>
</li>
<li class="acl-write" data-key="savedRevision">
<a class="grouped-right" id="revisionlink" title="Mark this revision as a saved revision">
<a class="grouped-right" id="revisionlink" data-l10n-id="pad.toolbar.savedRevision">
<span class="buttonicon buttonicon-savedRevision"></span>
</a>
</li>
<li class="acl-write separator"></li>
<li class="acl-write" data-key="settings">
<a class="grouped-left" id="settingslink" title="Settings of this pad">
<a class="grouped-left" id="settingslink" data-l10n-id="pad.toolbar.settings">
<span class="buttonicon buttonicon-settings"></span>
</a>
</li>
<li data-key="embed">
<a class="grouped-right" id="embedlink" title="Share and Embed this pad">
<a class="grouped-right" id="embedlink" data-l10n-id="pad.toolbar.embed">
<span class="grouped-right buttonicon buttonicon-embed"></span>
</a>
</li>
<li class="separator"></li>
<li id="usericon" data-key="showusers">
<a title="Show connected users">
<a data-l10n-id="pad.toolbar.showusers">
<span class="buttonicon buttonicon-showusers"></span>
<span id="online_count">1</span>
</a>
@ -153,8 +157,8 @@
<div id="myuser">
<div id="mycolorpicker">
<div id="colorpicker"></div>
<button id="mycolorpickersave">Save</button>
<button id="mycolorpickercancel">Cancel</button>
<button id="mycolorpickersave" data-l10n-id="pad.colorpicker.save"></button>
<button id="mycolorpickercancel" data-l10n-id="pad.colorpicker.cancel"></button>
<span id="mycolorpickerpreview" class="myswatchboxhoverable"></span>
</div>
<div id="myswatchbox"><div id="myswatch"></div></div>
@ -174,56 +178,76 @@
<div id="editorcontainerbox">
<div id="editorcontainer"></div>
<div id="editorloadingbox">
<p>Loading...</p>
<p data-l10n-id="pad.loading">Loading...</p>
<noscript><strong>Sorry, you have to enable Javascript in order to use this.</strong></noscript>
</div>
</div>
<div id="settings" class="popup">
<h1>Pad settings</h1>
<h1 data-l10n-id="pad.settings.padSettings"></h1>
<div class="column">
<% e.begin_block("mySettings"); %>
<h2>My view</h2>
<h2 data-l10n-id="pad.settings.myView"></h2>
<p>
<input type="checkbox" id="options-stickychat" onClick="chat.stickToScreen();">
<label for="options-stickychat">Chat always on screen</label>
<label for="options-stickychat" data-l10n-id="pad.settings.stickychat"></label>
</p>
<p>
<input type="checkbox" id="options-colorscheck">
<label for="options-colorscheck">Authorship colors</label>
<label for="options-colorscheck" data-l10n-id="pad.settings.colorcheck"></label>
</p>
<p>
<input type="checkbox" id="options-linenoscheck" checked>
<label for="options-linenoscheck">Line numbers</label>
</p>
<p>
Font type:
<select id="viewfontmenu">
<option value="normal">Normal</option>
<option value="monospace">Monospaced</option>
</select>
<label for="options-linenoscheck" data-l10n-id="pad.settings.linenocheck"></label>
</p>
<% e.end_block(); %>
<table>
<% e.begin_block("mySettings.dropdowns"); %>
<tr>
<td>
<label for="viewfontmenu" data-l10n-id="pad.settings.fontType">Font type:</label>
</td>
<td>
<select id="viewfontmenu">
<option value="normal" data-l10n-id="pad.settings.fontType.normal"></option>
<option value="monospace" data-l10n-id="pad.settings.fontType.monospaced"></option>
</select>
</td>
</tr>
<tr>
<td>
<label for="languagemenu" data-l10n-id="pad.settings.language">Language:</label>
</td>
<td>
<select id="languagemenu">
<% for (lang in langs) { %>
<option value="<%=lang%>"><%=langs[lang]%></option>
<% } %>
</select>
</td>
</tr>
<% e.end_block(); %>
</table>
</div>
<div class="column">
<% e.begin_block("globalSettings"); %>
<h2>Global view</h2>
<h2 data-l10n-id="pad.settings.globalView"></h2>
<% e.end_block(); %>
</div>
</div>
<div id="importexport" class="popup">
<h1>Import/Export</h1>
<h1 data-l10n-id="pad.importExport.import_export"></h1>
<div class="column acl-write">
<% e.begin_block("importColumn"); %>
<h2>Upload any text file or document</h2><br>
<h2 data-l10n-id="pad.importExport.import"></h2><br>
<form id="importform" method="post" action="" target="importiframe" enctype="multipart/form-data">
<div class="importformdiv" id="importformfilediv">
<input type="file" name="file" size="15" id="importfileinput">
<div class="importmessage" id="importmessagefail"></div>
</div>
<div id="import"></div>
<div class="importmessage" id="importmessagesuccess">Successful!</div>
<div class="importmessage" id="importmessagesuccess" data-l10n-id="pad.importExport.successful"></div>
<div class="importformdiv" id="importformsubmitdiv">
<input type="hidden" name="padId" value="blpmaXT35R">
<span class="nowrap">
@ -236,14 +260,14 @@
<% e.end_block(); %>
</div>
<div class="column">
<h2>Export current pad as</h2>
<h2 data-l10n-id="pad.importExport.export"></h2>
<% e.begin_block("exportColumn"); %>
<a id="exporthtmla" target="_blank" class="exportlink"><div class="exporttype" id="exporthtml">HTML</div></a>
<a id="exportplaina" target="_blank" class="exportlink"><div class="exporttype" id="exportplain">Plain text</div></a>
<a id="exportworda" target="_blank" class="exportlink"><div class="exporttype" id="exportword">Microsoft Word</div></a>
<a id="exportpdfa" target="_blank" class="exportlink"><div class="exporttype" id="exportpdf">PDF</div></a>
<a id="exportopena" target="_blank" class="exportlink"><div class="exporttype" id="exportopen">OpenDocument</div></a>
<a id="exportdokuwikia" target="_blank" class="exportlink"><div class="exporttype" id="exportdokuwiki">DokuWiki text</div></a>
<a id="exporthtmla" target="_blank" class="exportlink"><div class="exporttype" id="exporthtml" data-l10n-id="pad.importExport.exporthtml"></div></a>
<a id="exportplaina" target="_blank" class="exportlink"><div class="exporttype" id="exportplain" data-l10n-id="pad.importExport.exportplain"></div></a>
<a id="exportworda" target="_blank" class="exportlink"><div class="exporttype" id="exportword" data-l10n-id="pad.importExport.exportword"></div></a>
<a id="exportpdfa" target="_blank" class="exportlink"><div class="exporttype" id="exportpdf" data-l10n-id="pad.importExport.exportpdf"></div></a>
<a id="exportopena" target="_blank" class="exportlink"><div class="exporttype" id="exportopen" data-l10n-id="pad.importExport.exportopen"></div></a>
<a id="exportdokuwikia" target="_blank" class="exportlink"><div class="exporttype" id="exportdokuwiki" data-l10n-id="pad.importExport.exportdokuwiki"></div></a>
<% e.end_block(); %>
</div>
</div>
@ -251,48 +275,48 @@
<div id="connectivity" class="popup">
<% e.begin_block("modals"); %>
<div class="connected visible">
<h2>Connected.</h2>
<h2 data-l10n-id="pad.modals.connected"></h2>
</div>
<div class="reconnecting">
<h1>Reestablishing connection...</h1>
<h1 data-l10n-id="pad.modals.reconnecting"></h1>
<p><img alt="" border="0" src="../static/img/connectingbar.gif" /></p>
</div>
<div class="userdup">
<h1>Opened in another window.</h1>
<h2>You seem to have opened this pad in another browser window.</h2>
<p>If you'd like to use this window instead, you can reconnect.</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.uderdup"></h1>
<h2 data-l10n-id="pad.modals.userdup.explanation"></h2>
<p data-l10n-id="pad.modals.connected.advice"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="unauth">
<h1>No Authorization.</h1>
<p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.unauth"></h1>
<p data-l10n-id="pad.modals.unauth.explanation"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="looping">
<h1>Disconnected.</h1>
<h2>We're having trouble talking to the EtherPad lite synchronization server.</h2>
<p>You may be connecting through an incompatible firewall or proxy server.</p>
<h1 data-l10n-id="pad.modals.looping"></h1>
<h2 data-l10n-id="pad.modals.looping.explanation"></h2>
<p data-l10n-id="pad.modals.looping.cause"></p>
</div>
<div class="initsocketfail">
<h1>Disconnected.</h1>
<h2>We were unable to connect to the EtherPad lite synchronization server.</h2>
<p>This may be due to an incompatibility with your web browser or internet connection.</p>
<h1 data-l10n-id="pad.modals.initsocketfail"></h1>
<h2 data-l10n-id="pad.modals.initsocketfail.explanation"></h2>
<p data-l10n-id="pad.modals.initsocketfail.cause"></p>
</div>
<div class="slowcommit">
<h1>Disconnected.</h1>
<h2>Server not responding.</h2>
<p>This may be due to network connectivity issues or high load on the server.</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.slowcommit"></h1>
<h2 data-l10n-id="pad.modals.slowcommit.explanation"></h2>
<p data-l10n-id="pad.modals.slowcommit.cause"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="deleted">
<h1>Disconnected.</h1>
<p>This pad was deleted.</p>
<h1 data-l10n-id="pad.modals.deleted"></h1>
<p data-l10n-id="pad.modals.deleted.explanation"></p>
</div>
<div class="disconnected">
<h1>Disconnected.</h1>
<h2>Lost connection with the EtherPad lite synchronization server.</h2>
<p>This may be due to a loss of network connectivity. If this continues to happen, please let us know</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.disconnected.explanation"></h2>
<p data-l10n-id="pad.modals.disconnected.cause"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;">
<input type="hidden" class="padId" name="padId">
@ -305,17 +329,17 @@
<div id="embed" class="popup">
<% e.begin_block("embedPopup"); %>
<div id="embedreadonly" class="right acl-write">
<input type="checkbox" id="readonlyinput" onClick="padeditbar.setEmbedLinks();">
<label for="readonlyinput">Read only</label>
<input type="checkbox" id="readonlyinput">
<label for="readonlyinput" data-l10n-id="pad.share.readonly"></label>
</div>
<h1>Share this pad</h1>
<h1 data-l10n-id="pad.share"></h1>
<div id="linkcode">
<h2>Link</h2>
<h2 data-l10n-id="pad.share.link"></h2>
<input id="linkinput" type="text" value="">
</div>
<br>
<div id="embedcode">
<h2>Embed URL</h2>
<h2 data-l10n-id="pad.share.emebdcode"></h2>
<input id="embedinput" type="text" value="">
</div>
<% e.end_block(); %>
@ -323,14 +347,14 @@
<div id="chatthrob"></div>
<div id="chaticon" title="Open the chat for this pad" onclick="chat.show();return false;">
<span id="chatlabel">Chat</span>
<div id="chaticon" data-l10n-id="pad.chat" onclick="chat.show();return false;">
<span id="chatlabel" data-l10n-id="pad.chat"></span>
<span class="buttonicon buttonicon-chat"></span>
<span id="chatcounter">0</span>
</div>
<div id="chatbox">
<div id="titlebar"><span id ="titlelabel">Chat</span><a id="titlecross" onClick="chat.hide();return false;">-&nbsp;</a></div>
<div id="titlebar"><span id ="titlelabel" data-l10n-id="pad.chat"></span><a id="titlecross" onClick="chat.hide();return false;">-&nbsp;</a></div>
<div id="chattext" class="authorColors"></div>
<div id="chatinputbox">
<form>
@ -345,10 +369,9 @@
<% e.begin_block("scripts"); %>
<script type="text/javascript">
/* Display errors on page load to the user
(Gets overridden by padutils.setupGlobalExceptionHandler)
*/
(function() {
// Display errors on page load to the user
// (Gets overridden by padutils.setupGlobalExceptionHandler)
var originalHandler = window.onerror;
window.onerror = function(msg, url, line) {
var box = document.getElementById('editorloadingbox');
@ -358,9 +381,14 @@
// call original error handler
if(typeof(originalHandler) == 'function') originalHandler.call(null, arguments);
};
// Set language for l10n
var language = document.cookie.match(/language=(\w{2})/);
if(language) document.documentElement.lang = language[1];
})();
</script>
<script type="text/javascript" src="../static/js/l10n.js"></script>
<script type="text/javascript" src="../static/js/require-kernel.js"></script>
<script type="text/javascript" src="../socket.io/socket.io.js"></script>
@ -374,8 +402,6 @@
<script type="text/javascript">
var clientVars = {};
(function () {
var pathComponents = location.pathname.split('/');
// Strip 'p' and the padname from the pathname and set as baseURL

View file

@ -1,6 +1,10 @@
<%
var settings = require("ep_etherpad-lite/node/utils/Settings")
, langs = require("ep_etherpad-lite/node/hooks/i18n").availableLangs
%>
<!doctype html>
<html lang="en">
<title>Etherpad Lite Timeslider</title>
<html>
<title data-l10n-id="timeslider.pageTitle" data-l10n-args='{ "appTitle": "<%=settings.title%>" }'><%=settings.title%> Timeslider</title>
<script>
/*
|@licstart The following is the entire license notice for the
@ -27,7 +31,9 @@
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex, nofollow">
<link rel="shortcut icon" href="../../favicon.ico">
<link rel="shortcut icon" href="<%=settings.favicon%>">
<script type="text/javascript" src="../../static/js/l10n.js"></script>
<link rel="resource" type="application/l10n" href="../../locales.ini" />
<link rel="stylesheet" href="../../static/css/pad.css">
<link rel="stylesheet" href="../../static/css/timeslider.css">
<link rel="stylesheet" href="../../static/custom/timeslider.css">
@ -66,12 +72,12 @@
<div class="editbarright toolbar" id="editbar">
<ul>
<li onClick="window.padeditbar.toolbarClick('import_export');return false;">
<a id="exportlink" title="Export to different document formats">
<a id="exportlink" data-l10n-id="pad.importExport.export">
<div class="buttonicon buttonicon-import_export"></div>
</a>
</li>
</ul>
<a id="returnbutton">Return to pad</a>
<a id="returnbutton" data-l10n-id="timeslider.toolbar.returnbutton"></a>
</div>
<div>
@ -79,9 +85,8 @@
<span id="revision_label"></span>
<span id="revision_date"></span>
</h1>
<p>Authors:
<span id="authorsList">
<span>No Authors</span>
<p data-l10n-id="timeslider.toolbar.authors">
<span id="authorsList" data-l10n-id="timeslider.toolbar.authorsList"></span>
</span> </p>
</div>
</div>
@ -98,73 +103,81 @@
</div><!-- /padpage -->
<div id="connectivity" class="popup">
<% e.begin_block("modals"); %>
<% e.begin_block("modals"); %>
<div class="connected visible">
<h2>Connected.</h2>
<h2 data-l10n-id="pad.modals.connected"></h2>
</div>
<div class="reconnecting">
<h1>Reestablishing connection...</h1>
<h1 data-l10n-id="pad.modals.reconnecting"></h1>
<p><img alt="" border="0" src="../../static/img/connectingbar.gif" /></p>
</div>
<div class="userdup">
<h1>Opened in another window.</h1>
<h2>You seem to have opened this pad in another browser window.</h2>
<p>If you'd like to use this window instead, you can reconnect.</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.uderdup"></h1>
<h2 data-l10n-id="pad.modals.userdup.explanation"></h2>
<p data-l10n-id="pad.modals.connected.advice"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="unauth">
<h1>No Authorization.</h1>
<p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.unauth"></h1>
<p data-l10n-id="pad.modals.unauth.explanation"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="looping">
<h1>Disconnected.</h1>
<h2>We're having trouble talking to the EtherPad lite synchronization server.</h2>
<p>You may be connecting through an incompatible firewall or proxy server.</p>
<h1 data-l10n-id="pad.modals.looping"></h1>
<h2 data-l10n-id="pad.modals.looping.explanation"></h2>
<p data-l10n-id="pad.modals.looping.cause"></p>
</div>
<div class="initsocketfail">
<h1>Disconnected.</h1>
<h2>We were unable to connect to the EtherPad lite synchronization server.</h2>
<p>This may be due to an incompatibility with your web browser or internet connection.</p>
<h1 data-l10n-id="pad.modals.initsocketfail"></h1>
<h2 data-l10n-id="pad.modals.initsocketfail.explanation"></h2>
<p data-l10n-id="pad.modals.initsocketfail.cause"></p>
</div>
<div class="slowcommit">
<h1>Disconnected.</h1>
<h2>Server not responding.</h2>
<p>This may be due to network connectivity issues or high load on the server.</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.slowcommit"></h1>
<h2 data-l10n-id="pad.modals.slowcommit.explanation"></h2>
<p data-l10n-id="pad.modals.slowcommit.cause"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="deleted">
<h1>Disconnected.</h1>
<p>This pad was deleted.</p>
<h1 data-l10n-id="pad.modals.deleted"></h1>
<p data-l10n-id="pad.modals.deleted.explanation"></p>
</div>
<div class="disconnected">
<h1>Disconnected.</h1>
<h2>Lost connection with the EtherPad lite synchronization server.</h2>
<p>This may be due to a loss of network connectivity. If this continues to happen, please let us know</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.disconnected.explanation"></h2>
<p data-l10n-id="pad.modals.disconnected.cause"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;">
<input type="hidden" class="padId" name="padId">
<input type="hidden" class="diagnosticInfo" name="diagnosticInfo">
<input type="hidden" class="missedChanges" name="missedChanges">
</form>
<% e.end_block(); %>
<% e.end_block(); %>
</div>
<!-- export code -->
<div id="importexport">
<div id="export" class="popup">
Export current version as:
<a id="exporthtmla" target="_blank" class="exportlink"><div class="exporttype" id="exporthtml">HTML</div></a>
<a id="exportplaina" target="_blank" class="exportlink"><div class="exporttype" id="exportplain">Plain text</div></a>
<a id="exportworda" target="_blank" class="exportlink"><div class="exporttype" id="exportword">Microsoft Word</div></a>
<a id="exportpdfa" target="_blank" class="exportlink"><div class="exporttype" id="exportpdf">PDF</div></a>
<a id="exportopena" target="_blank" class="exportlink"><div class="exporttype" id="exportopen">OpenDocument</div></a>
<a id="exportdokuwikia" target="_blank" class="exportlink"><div class="exporttype" id="exportdokuwiki">DokuWiki text</div></a>
<div id="export" class="popup" data-l10n-id="timeslider.exportCurrent">
<a id="exporthtmla" target="_blank" class="exportlink"><div class="exporttype" id="exporthtml" data-l10n-id="pad.importExport.exporthtml"></div></a>
<a id="exportplaina" target="_blank" class="exportlink"><div class="exporttype" id="exportplain" data-l10n-id="pad.importExport.exportplain"></div></a>
<a id="exportworda" target="_blank" class="exportlink"><div class="exporttype" id="exportword" data-l10n-id="pad.importExport.exportword"></div></a>
<a id="exportpdfa" target="_blank" class="exportlink"><div class="exporttype" id="exportpdf" data-l10n-id="pad.importExport.exportpdf"></div></a>
<a id="exportopena" target="_blank" class="exportlink"><div class="exporttype" id="exportopen" data-l10n-id="pad.importExport.exportopen"></div></a>
<a id="exportdokuwikia" target="_blank" class="exportlink"><div class="exporttype" id="exportdokuwiki" data-l10n-id="pad.importExport.exportdokuwiki"></div></a>
</div>
</div>
<script type="text/javascript">
(function() {
// Set language for l10n
var language = document.cookie.match(/language=(\w{2})/);
if(language) document.documentElement.lang = language[1];
})();
</script>
<script type="text/javascript" src="../../static/js/l10n.js"></script>
<script type="text/javascript" src="../../static/js/require-kernel.js"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
@ -179,7 +192,6 @@
var clientVars = {};
(function () {
var pathComponents = location.pathname.split('/');
// Strip 'p', the padname and 'timeslider' from the pathname and set as baseURL

159
tests/frontend/helper.js Normal file
View file

@ -0,0 +1,159 @@
var helper = {};
(function(){
var $iframeContainer, $iframe, jsLibraries = {};
helper.init = function(cb){
$iframeContainer = $("#iframe-container");
$.get('/static/js/jquery.js').done(function(code){
// make sure we don't override existing jquery
jsLibraries["jquery"] = "if(typeof $ === 'undefined') {\n" + code + "\n}";
$.get('/tests/frontend/lib/sendkeys.js').done(function(code){
jsLibraries["sendkeys"] = code;
cb();
});
});
}
helper.randomString = function randomString(len)
{
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
var randomstring = '';
for (var i = 0; i < len; i++)
{
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum, rnum + 1);
}
return randomstring;
}
var getFrameJQuery = function($iframe){
/*
I tried over 9000 ways to inject javascript into iframes.
This is the only way I found that worked in IE 7+8+9, FF and Chrome
*/
var win = $iframe[0].contentWindow;
var doc = win.document;
//IE 8+9 Hack to make eval appear
//http://stackoverflow.com/questions/2720444/why-does-this-window-object-not-have-the-eval-function
win.execScript && win.execScript("null");
win.eval(jsLibraries["jquery"]);
win.eval(jsLibraries["sendkeys"]);
win.$.window = win;
win.$.document = doc;
return win.$;
}
helper.clearCookies = function(){
window.document.cookie = "";
}
helper.newPad = function(){
//build opts object
var opts = {clearCookies: true}
if(typeof arguments[0] === 'function'){
opts.cb = arguments[0]
} else {
opts = _.defaults(arguments[0], opts);
}
//clear cookies
if(opts.clearCookies){
helper.clearCookies();
}
var padName = "FRONTEND_TEST_" + helper.randomString(20);
$iframe = $("<iframe src='/p/" + padName + "'></iframe>");
//clean up inner iframe references
helper.padChrome$ = helper.padOuter$ = helper.padInner$ = null;
//clean up iframes properly to prevent IE from memoryleaking
$iframeContainer.find("iframe").purgeFrame().done(function(){
$iframeContainer.append($iframe);
$iframe.one('load', function(){
helper.waitFor(function(){
return !$iframe.contents().find("#editorloadingbox").is(":visible");
}, 50000).done(function(){
helper.padChrome$ = getFrameJQuery( $('#iframe-container iframe'));
helper.padOuter$ = getFrameJQuery(helper.padChrome$('iframe.[name="ace_outer"]'));
helper.padInner$ = getFrameJQuery( helper.padOuter$('iframe.[name="ace_inner"]'));
//disable all animations, this makes tests faster and easier
helper.padChrome$.fx.off = true;
helper.padOuter$.fx.off = true;
helper.padInner$.fx.off = true;
opts.cb();
}).fail(function(){
throw new Error("Pad never loaded");
});
});
});
return padName;
}
helper.waitFor = function(conditionFunc, _timeoutTime, _intervalTime){
var timeoutTime = _timeoutTime || 1000;
var intervalTime = _intervalTime || 10;
var deferred = $.Deferred();
var _fail = deferred.fail;
var listenForFail = false;
deferred.fail = function(){
listenForFail = true;
_fail.apply(this, arguments);
}
var intervalCheck = setInterval(function(){
var passed = false;
passed = conditionFunc();
if(passed){
clearInterval(intervalCheck);
clearTimeout(timeout);
deferred.resolve();
}
}, intervalTime);
var timeout = setTimeout(function(){
clearInterval(intervalCheck);
var error = new Error("wait for condition never became true " + conditionFunc.toString());
deferred.reject(error);
if(!listenForFail){
throw error;
}
}, timeoutTime);
return deferred;
}
/* Ensure console.log doesn't blow up in IE, ugly but ok for a test framework imho*/
window.console = window.console || {};
window.console.log = window.console.log || function(){}
//force usage of callbacks in it
var _it = it;
it = function(name, func){
if(func && func.length !== 1){
func = function(){
throw new Error("Please use always a callback with it() - " + func.toString());
}
}
_it(name, func);
}
})()

26
tests/frontend/index.html Normal file
View file

@ -0,0 +1,26 @@
<!doctype html>
<html>
<title>Frontend tests</title>
<meta charset="utf-8">
<link rel="stylesheet" href="runner.css" />
<div id="console"></div>
<div id="mocha"></div>
<div id="iframe-container"></div>
<script src="/static/js/jquery.js"></script>
<script src="lib/underscore.js"></script>
<script src="lib/mocha.js"></script>
<script> mocha.setup('bdd') </script>
<script src="lib/expect.js"></script>
<script src="lib/sendkeys.js"></script>
<script src="lib/jquery.iframe.js"></script>
<script src="helper.js"></script>
<script src="specs_list.js"></script>
<script src="runner.js"></script>
</html>

1247
tests/frontend/lib/expect.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,39 @@
//copied from http://stackoverflow.com/questions/8407946/is-it-possible-to-use-iframes-in-ie-without-memory-leaks
(function($) {
$.fn.purgeFrame = function() {
var deferred;
if ($.browser.msie && parseFloat($.browser.version, 10) < 9) {
deferred = purge(this);
} else {
this.remove();
deferred = $.Deferred();
deferred.resolve();
}
return deferred;
};
function purge($frame) {
var sem = $frame.length
, deferred = $.Deferred();
$frame.load(function() {
var frame = this;
frame.contentWindow.document.innerHTML = '';
sem -= 1;
if (sem <= 0) {
$frame.remove();
deferred.resolve();
}
});
$frame.attr('src', 'about:blank');
if ($frame.length === 0) {
deferred.resolve();
}
return deferred.promise();
}
})(jQuery);

4868
tests/frontend/lib/mocha.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,467 @@
// Cross-broswer implementation of text ranges and selections
// documentation: http://bililite.com/blog/2011/01/11/cross-browser-.and-selections/
// Version: 1.1
// Copyright (c) 2010 Daniel Wachsstock
// MIT license:
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
(function($){
bililiteRange = function(el, debug){
var ret;
if (debug){
ret = new NothingRange(); // Easier to force it to use the no-selection type than to try to find an old browser
}else if (document.selection && !document.addEventListener){
// Internet Explorer 8 and lower
ret = new IERange();
}else if (window.getSelection && el.setSelectionRange){
// Standards. Element is an input or textarea
ret = new InputRange();
}else if (window.getSelection){
// Standards, with any other kind of element
ret = new W3CRange()
}else{
// doesn't support selection
ret = new NothingRange();
}
ret._el = el;
ret._doc = el.ownerDocument;
ret._win = 'defaultView' in ret._doc ? ret._doc.defaultView : ret._doc.parentWindow;
ret._textProp = textProp(el);
ret._bounds = [0, ret.length()];
return ret;
}
function textProp(el){
// returns the property that contains the text of the element
if (typeof el.value != 'undefined') return 'value';
if (typeof el.text != 'undefined') return 'text';
if (typeof el.textContent != 'undefined') return 'textContent';
return 'innerText';
}
// base class
function Range(){}
Range.prototype = {
length: function() {
return this._el[this._textProp].replace(/\r/g, '').length; // need to correct for IE's CrLf weirdness
},
bounds: function(s){
if (s === 'all'){
this._bounds = [0, this.length()];
}else if (s === 'start'){
this._bounds = [0, 0];
}else if (s === 'end'){
this._bounds = [this.length(), this.length()];
}else if (s === 'selection'){
this.bounds ('all'); // first select the whole thing for constraining
this._bounds = this._nativeSelection();
}else if (s){
this._bounds = s; // don't error check now; the element may change at any moment, so constrain it when we need it.
}else{
var b = [
Math.max(0, Math.min (this.length(), this._bounds[0])),
Math.max(0, Math.min (this.length(), this._bounds[1]))
];
return b; // need to constrain it to fit
}
return this; // allow for chaining
},
select: function(){
this._nativeSelect(this._nativeRange(this.bounds()));
return this; // allow for chaining
},
text: function(text, select){
if (arguments.length){
this._nativeSetText(text, this._nativeRange(this.bounds()));
if (select == 'start'){
this.bounds ([this._bounds[0], this._bounds[0]]);
this.select();
}else if (select == 'end'){
this.bounds ([this._bounds[0]+text.length, this._bounds[0]+text.length]);
this.select();
}else if (select == 'all'){
this.bounds ([this._bounds[0], this._bounds[0]+text.length]);
this.select();
}
return this; // allow for chaining
}else{
return this._nativeGetText(this._nativeRange(this.bounds()));
}
},
insertEOL: function (){
this._nativeEOL();
this._bounds = [this._bounds[0]+1, this._bounds[0]+1]; // move past the EOL marker
return this;
}
};
function IERange(){}
IERange.prototype = new Range();
IERange.prototype._nativeRange = function (bounds){
var rng;
if (this._el.tagName == 'INPUT'){
// IE 8 is very inconsistent; textareas have createTextRange but it doesn't work
rng = this._el.createTextRange();
}else{
rng = this._doc.body.createTextRange ();
rng.moveToElementText(this._el);
}
if (bounds){
if (bounds[1] < 0) bounds[1] = 0; // IE tends to run elements out of bounds
if (bounds[0] > this.length()) bounds[0] = this.length();
if (bounds[1] < rng.text.replace(/\r/g, '').length){ // correct for IE's CrLf wierdness
// block-display elements have an invisible, uncounted end of element marker, so we move an extra one and use the current length of the range
rng.moveEnd ('character', -1);
rng.moveEnd ('character', bounds[1]-rng.text.replace(/\r/g, '').length);
}
if (bounds[0] > 0) rng.moveStart('character', bounds[0]);
}
return rng;
};
IERange.prototype._nativeSelect = function (rng){
rng.select();
};
IERange.prototype._nativeSelection = function (){
// returns [start, end] for the selection constrained to be in element
var rng = this._nativeRange(); // range of the element to constrain to
var len = this.length();
if (this._doc.selection.type != 'Text') return [0,0]; // append to the end
var sel = this._doc.selection.createRange();
try{
return [
iestart(sel, rng),
ieend (sel, rng)
];
}catch (e){
// IE gets upset sometimes about comparing text to input elements, but the selections cannot overlap, so make a best guess
return (sel.parentElement().sourceIndex < this._el.sourceIndex) ? [0,0] : [len, len];
}
};
IERange.prototype._nativeGetText = function (rng){
return rng.text.replace(/\r/g, ''); // correct for IE's CrLf weirdness
};
IERange.prototype._nativeSetText = function (text, rng){
rng.text = text;
};
IERange.prototype._nativeEOL = function(){
if (typeof this._el.value != 'undefined'){
this.text('\n'); // for input and textarea, insert it straight
}else{
this._nativeRange(this.bounds()).pasteHTML('<br/>');
}
};
// IE internals
function iestart(rng, constraint){
// returns the position (in character) of the start of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness
if (rng.compareEndPoints ('StartToStart', constraint) <= 0) return 0; // at or before the beginning
if (rng.compareEndPoints ('StartToEnd', constraint) >= 0) return len;
for (var i = 0; rng.compareEndPoints ('StartToStart', constraint) > 0; ++i, rng.moveStart('character', -1));
return i;
}
function ieend (rng, constraint){
// returns the position (in character) of the end of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness
if (rng.compareEndPoints ('EndToEnd', constraint) >= 0) return len; // at or after the end
if (rng.compareEndPoints ('EndToStart', constraint) <= 0) return 0;
for (var i = 0; rng.compareEndPoints ('EndToStart', constraint) > 0; ++i, rng.moveEnd('character', -1));
return i;
}
// an input element in a standards document. "Native Range" is just the bounds array
function InputRange(){}
InputRange.prototype = new Range();
InputRange.prototype._nativeRange = function(bounds) {
return bounds || [0, this.length()];
};
InputRange.prototype._nativeSelect = function (rng){
this._el.setSelectionRange(rng[0], rng[1]);
};
InputRange.prototype._nativeSelection = function(){
return [this._el.selectionStart, this._el.selectionEnd];
};
InputRange.prototype._nativeGetText = function(rng){
return this._el.value.substring(rng[0], rng[1]);
};
InputRange.prototype._nativeSetText = function(text, rng){
var val = this._el.value;
this._el.value = val.substring(0, rng[0]) + text + val.substring(rng[1]);
};
InputRange.prototype._nativeEOL = function(){
this.text('\n');
};
function W3CRange(){}
W3CRange.prototype = new Range();
W3CRange.prototype._nativeRange = function (bounds){
var rng = this._doc.createRange();
rng.selectNodeContents(this._el);
if (bounds){
w3cmoveBoundary (rng, bounds[0], true, this._el);
rng.collapse (true);
w3cmoveBoundary (rng, bounds[1]-bounds[0], false, this._el);
}
return rng;
};
W3CRange.prototype._nativeSelect = function (rng){
this._win.getSelection().removeAllRanges();
this._win.getSelection().addRange (rng);
};
W3CRange.prototype._nativeSelection = function (){
// returns [start, end] for the selection constrained to be in element
var rng = this._nativeRange(); // range of the element to constrain to
if (this._win.getSelection().rangeCount == 0) return [this.length(), this.length()]; // append to the end
var sel = this._win.getSelection().getRangeAt(0);
return [
w3cstart(sel, rng),
w3cend (sel, rng)
];
}
W3CRange.prototype._nativeGetText = function (rng){
return rng.toString();
};
W3CRange.prototype._nativeSetText = function (text, rng){
rng.deleteContents();
rng.insertNode (this._doc.createTextNode(text));
this._el.normalize(); // merge the text with the surrounding text
};
W3CRange.prototype._nativeEOL = function(){
var rng = this._nativeRange(this.bounds());
rng.deleteContents();
var br = this._doc.createElement('br');
br.setAttribute ('_moz_dirty', ''); // for Firefox
rng.insertNode (br);
rng.insertNode (this._doc.createTextNode('\n'));
rng.collapse (false);
};
// W3C internals
function nextnode (node, root){
// in-order traversal
// we've already visited node, so get kids then siblings
if (node.firstChild) return node.firstChild;
if (node.nextSibling) return node.nextSibling;
if (node===root) return null;
while (node.parentNode){
// get uncles
node = node.parentNode;
if (node == root) return null;
if (node.nextSibling) return node.nextSibling;
}
return null;
}
function w3cmoveBoundary (rng, n, bStart, el){
// move the boundary (bStart == true ? start : end) n characters forward, up to the end of element el. Forward only!
// if the start is moved after the end, then an exception is raised
if (n <= 0) return;
var node = rng[bStart ? 'startContainer' : 'endContainer'];
if (node.nodeType == 3){
// we may be starting somewhere into the text
n += rng[bStart ? 'startOffset' : 'endOffset'];
}
while (node){
if (node.nodeType == 3){
if (n <= node.nodeValue.length){
rng[bStart ? 'setStart' : 'setEnd'](node, n);
// special case: if we end next to a <br>, include that node.
if (n == node.nodeValue.length){
// skip past zero-length text nodes
for (var next = nextnode (node, el); next && next.nodeType==3 && next.nodeValue.length == 0; next = nextnode(next, el)){
rng[bStart ? 'setStartAfter' : 'setEndAfter'](next);
}
if (next && next.nodeType == 1 && next.nodeName == "BR") rng[bStart ? 'setStartAfter' : 'setEndAfter'](next);
}
return;
}else{
rng[bStart ? 'setStartAfter' : 'setEndAfter'](node); // skip past this one
n -= node.nodeValue.length; // and eat these characters
}
}
node = nextnode (node, el);
}
}
var START_TO_START = 0; // from the w3c definitions
var START_TO_END = 1;
var END_TO_END = 2;
var END_TO_START = 3;
// from the Mozilla documentation, for range.compareBoundaryPoints(how, sourceRange)
// -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange.
// * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range.
// * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range.
// * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range.
// * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range.
function w3cstart(rng, constraint){
if (rng.compareBoundaryPoints (START_TO_START, constraint) <= 0) return 0; // at or before the beginning
if (rng.compareBoundaryPoints (END_TO_START, constraint) >= 0) return constraint.toString().length;
rng = rng.cloneRange(); // don't change the original
rng.setEnd (constraint.endContainer, constraint.endOffset); // they now end at the same place
return constraint.toString().length - rng.toString().length;
}
function w3cend (rng, constraint){
if (rng.compareBoundaryPoints (END_TO_END, constraint) >= 0) return constraint.toString().length; // at or after the end
if (rng.compareBoundaryPoints (START_TO_END, constraint) <= 0) return 0;
rng = rng.cloneRange(); // don't change the original
rng.setStart (constraint.startContainer, constraint.startOffset); // they now start at the same place
return rng.toString().length;
}
function NothingRange(){}
NothingRange.prototype = new Range();
NothingRange.prototype._nativeRange = function(bounds) {
return bounds || [0,this.length()];
};
NothingRange.prototype._nativeSelect = function (rng){ // do nothing
};
NothingRange.prototype._nativeSelection = function(){
return [0,0];
};
NothingRange.prototype._nativeGetText = function (rng){
return this._el[this._textProp].substring(rng[0], rng[1]);
};
NothingRange.prototype._nativeSetText = function (text, rng){
var val = this._el[this._textProp];
this._el[this._textProp] = val.substring(0, rng[0]) + text + val.substring(rng[1]);
};
NothingRange.prototype._nativeEOL = function(){
this.text('\n');
};
})(jQuery);
// insert characters in a textarea or text input field
// special characters are enclosed in {}; use {{} for the { character itself
// documentation: http://bililite.com/blog/2008/08/20/the-fnsendkeys-plugin/
// Version: 2.0
// Copyright (c) 2010 Daniel Wachsstock
// MIT license:
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
(function($){
$.fn.sendkeys = function (x, opts){
return this.each( function(){
var localkeys = $.extend({}, opts, $(this).data('sendkeys')); // allow for element-specific key functions
// most elements to not keep track of their selection when they lose focus, so we have to do it for them
var rng = $.data (this, 'sendkeys.selection');
if (!rng){
rng = bililiteRange(this).bounds('selection');
$.data(this, 'sendkeys.selection', rng);
$(this).bind('mouseup.sendkeys', function(){
// we have to update the saved range. The routines here update the bounds with each press, but actual keypresses and mouseclicks do not
$.data(this, 'sendkeys.selection').bounds('selection');
}).bind('keyup.sendkeys', function(evt){
// restore the selection if we got here with a tab (a click should select what was clicked on)
if (evt.which == 9){
// there's a flash of selection when we restore the focus, but I don't know how to avoid that.
$.data(this, 'sendkeys.selection').select();
}else{
$.data(this, 'sendkeys.selection').bounds('selection');
}
});
}
this.focus();
if (typeof x === 'undefined') return; // no string, so we just set up the event handlers
$.data(this, 'sendkeys.originalText', rng.text());
x.replace(/\n/g, '{enter}'). // turn line feeds into explicit break insertions
replace(/{[^}]*}|[^{]+/g, function(s){
(localkeys[s] || $.fn.sendkeys.defaults[s] || $.fn.sendkeys.defaults.simplechar)(rng, s);
});
$(this).trigger({type: 'sendkeys', which: x});
});
}; // sendkeys
// add the functions publicly so they can be overridden
$.fn.sendkeys.defaults = {
simplechar: function (rng, s){
rng.text(s, 'end');
for (var i =0; i < s.length; ++i){
var x = s.charCodeAt(i);
// a bit of cheating: rng._el is the element associated with rng.
$(rng._el).trigger({type: 'keypress', keyCode: x, which: x, charCode: x});
}
},
'{{}': function (rng){
$.fn.sendkeys.defaults.simplechar (rng, '{')
},
'{enter}': function (rng){
rng.insertEOL();
rng.select();
var x = '\n'.charCodeAt(0);
$(rng._el).trigger({type: 'keypress', keyCode: x, which: x, charCode: x});
},
'{backspace}': function (rng){
var b = rng.bounds();
if (b[0] == b[1]) rng.bounds([b[0]-1, b[0]]); // no characters selected; it's just an insertion point. Remove the previous character
rng.text('', 'end'); // delete the characters and update the selection
},
'{del}': function (rng){
var b = rng.bounds();
if (b[0] == b[1]) rng.bounds([b[0], b[0]+1]); // no characters selected; it's just an insertion point. Remove the next character
rng.text('', 'end'); // delete the characters and update the selection
},
'{rightarrow}': function (rng){
var b = rng.bounds();
if (b[0] == b[1]) ++b[1]; // no characters selected; it's just an insertion point. Move to the right
rng.bounds([b[1], b[1]]).select();
},
'{leftarrow}': function (rng){
var b = rng.bounds();
if (b[0] == b[1]) --b[0]; // no characters selected; it's just an insertion point. Move to the left
rng.bounds([b[0], b[0]]).select();
},
'{selectall}' : function (rng){
rng.bounds('all').select();
},
'{selection}': function (rng){
$.fn.sendkeys.defaults.simplechar(rng, $.data(rng._el, 'sendkeys.originalText'));
},
'{mark}' : function (rng){
var bounds = rng.bounds();
$(rng._el).one('sendkeys', function(){
// set up the event listener to change the selection after the sendkeys is done
rng.bounds(bounds).select();
});
}
};
})(jQuery)

File diff suppressed because it is too large Load diff

228
tests/frontend/runner.css Normal file
View file

@ -0,0 +1,228 @@
html {
height: 100%;
}
body {
padding: 0px;
margin: 0px;
height: 100%;
}
#console {
display: none;
}
#iframe-container {
width: 50%;
height: 100%;
float:right;
}
#iframe-container iframe {
width: 100%;
height: 100%;
}
#mocha {
font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
border-right: 2px solid #999;
width: 50%;
height: 100%;
position: absolute;
overflow: auto;
float:left;
}
#mocha #report {
margin-top: 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:visited
{
color: #00E;
}
#mocha .suite .suite h1 {
margin-top: 0;
font-size: .8em;
}
#mocha h2 {
font-size: 12px;
font-weight: normal;
cursor: pointer;
}
#mocha .suite {
margin-left: 15px;
}
#mocha .test {
margin-left: 15px;
}
#mocha .test:hover h2::after {
position: relative;
top: 0;
right: -10px;
content: '(view source)';
font-size: 12px;
font-family: arial;
color: #888;
}
#mocha .test.pending:hover h2::after {
content: '(pending)';
font-family: arial;
}
#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;
}
#mocha .test pre {
display: inline-block;
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;
}
#report.pass .test.fail {
display: none;
}
#report.fail .test.pass {
display: none;
}
#error {
color: #c00;
font-size: 1.5 em;
font-weight: 100;
letter-spacing: 1px;
}
#stats {
position: absolute;
top: 15px;
right: 10px;
font-size: 12px;
margin: 0;
color: #888;
}
#stats .progress {
float: right;
padding-top: 0;
}
#stats em {
color: black;
}
#stats a {
text-decoration: none;
color: inherit;
}
#stats a:hover {
border-bottom: 1px solid #eee;
}
#stats li {
display: inline-block;
margin: 0 5px;
list-style: none;
padding-top: 11px;
}
code .comment { color: #ddd }
code .init { color: #2F6FAD }
code .string { color: #5890AD }
code .keyword { color: #8A6343 }
code .number { color: #2F6FAD }

192
tests/frontend/runner.js Normal file
View file

@ -0,0 +1,192 @@
$(function(){
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.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++;
});
}
/*
This reporter wraps the original html reporter plus reports plain text into a hidden div.
This allows the webdriver client to pick up the test results
*/
var WebdriverAndHtmlReporter = function(html_reporter){
return function(runner){
Base.call(this, runner);
//initalize the html reporter first
html_reporter(runner);
var $console = $("#console");
var level = 0;
var append = function(){
var text = Array.prototype.join.apply(arguments, [" "]);
var oldText = $console.text();
var space = "";
for(var i=0;i<level*2;i++){
space+=" ";
}
var splitedText = "";
_(text.split("\n")).each(function(line){
while(line.length > 0){
var split = line.substr(0,100);
line = line.substr(100);
if(splitedText.length > 0) splitedText+="\n";
splitedText += split;
}
});
//indent all lines with the given amount of space
var newText = _(splitedText.split("\n")).map(function(line){
return space + line;
}).join("\\n");
$console.text(oldText + newText + "\\n");
}
runner.on('suite', function(suite){
if (suite.root) return;
append(suite.title);
level++;
});
runner.on('suite end', function(suite){
if (suite.root) return;
level--;
if(level == 0) {
append("");
}
});
var stringifyException = function(exception){
var err = exception.stack || exception.toString();
// FF / Opera do not add the message
if (!~err.indexOf(exception.message)) {
err = exception.message + '\n' + err;
}
// <=IE7 stringifies to [Object Error]. Since it can be overloaded, we
// check for the result of the stringifying.
if ('[object Error]' == err) err = exception.message;
// Safari doesn't give you a stack. Let's at least provide a source line.
if (!exception.stack && exception.sourceURL && exception.line !== undefined) {
err += "\n(" + exception.sourceURL + ":" + exception.line + ")";
}
return err;
}
var killTimeout;
runner.on('test end', function(test){
if ('passed' == test.state) {
append("->","[green]PASSED[clear] :", test.title);
} else if (test.pending) {
append("->","[yellow]PENDING[clear]:", test.title);
} else {
append("->","[red]FAILED[clear] :", test.title, stringifyException(test.err));
}
if(killTimeout) clearTimeout(killTimeout);
killTimeout = setTimeout(function(){
append("FINISHED - [red]no test started since 3 minutes, tests stopped[clear]");
}, 60000 * 3);
});
var total = runner.total;
runner.on('end', function(){
if(stats.tests >= total){
var minutes = Math.floor(stats.duration / 1000 / 60);
var seconds = Math.round((stats.duration / 1000) % 60);
append("FINISHED -", stats.passes, "tests passed,", stats.failures, "tests failed, duration: " + minutes + ":" + seconds);
}
});
}
}
//allow cross iframe access
if ((!$.browser.msie) && (!($.browser.mozilla && $.browser.version.indexOf("1.8.") == 0))) {
document.domain = document.domain; // for comet
}
//http://stackoverflow.com/questions/1403888/get-url-parameter-with-jquery
var getURLParameter = function (name) {
return decodeURI(
(RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[,null])[1]
);
}
//get the list of specs and filter it if requested
var specs = specs_list.slice();
//inject spec scripts into the dom
var $body = $('body');
$.each(specs, function(i, spec){
$body.append('<script src="specs/' + spec + '"></script>')
});
//initalize the test helper
helper.init(function(){
//configure and start the test framework
var grep = getURLParameter("grep");
if(grep != "null"){
mocha.grep(grep);
}
mocha.ignoreLeaks();
mocha.reporter(WebdriverAndHtmlReporter(mocha._reporter));
mocha.run();
});
});

View file

@ -0,0 +1,36 @@
describe("bold button", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes text bold", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//get the first text element out of the inner iframe
var $firstTextElement = inner$("div").first();
//select this text element
$firstTextElement.sendkeys('{selectall}');
//get the bold button and click it
var $boldButton = chrome$(".buttonicon-bold");
$boldButton.click();
//ace creates a new dom element when you press a button, so just get the first text element again
var $newFirstTextElement = inner$("div").first();
// is there a <b> element now?
var isBold = $newFirstTextElement.find("b").length === 1;
//expect it to be bold
expect(isBold).to.be(true);
//make sure the text hasn't changed
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
done();
});
});

View file

@ -0,0 +1,54 @@
describe("clear authorship colors button", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes text clear authorship colors", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
// override the confirm dialogue functioon
helper.padChrome$.window.confirm = function(){
return true;
}
//get the first text element out of the inner iframe
var $firstTextElement = inner$("div").first();
// Get the original text
var originalText = inner$("div").first().text();
// Set some new text
var sentText = "Hello";
//select this text element
$firstTextElement.sendkeys('{selectall}');
$firstTextElement.sendkeys(sentText);
$firstTextElement.sendkeys('{rightarrow}');
helper.waitFor(function(){
return inner$("div span").first().attr("class").indexOf("author") !== -1; // wait until we have the full value available
}).done(function(){
//IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship
inner$("div").first().focus();
//get the clear authorship colors button and click it
var $clearauthorshipcolorsButton = chrome$(".buttonicon-clearauthorship");
$clearauthorshipcolorsButton.click();
// does the first divs span include an author class?
console.log(inner$("div span").first().attr("class"));
var hasAuthorClass = inner$("div span").first().attr("class").indexOf("author") !== -1;
//expect(hasAuthorClass).to.be(false);
// does the first div include an author class?
var hasAuthorClass = inner$("div").first().attr("class").indexOf("author") !== -1;
expect(hasAuthorClass).to.be(false);
done();
});
});
});

View file

@ -0,0 +1,179 @@
describe("indentation button", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("indent text", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
var $indentButton = chrome$(".buttonicon-indent");
$indentButton.click();
helper.waitFor(function(){
return inner$("div").first().find("ul li").length === 1;
}).done(done);
});
it("keeps the indent on enter for the new line", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
var $indentButton = chrome$(".buttonicon-indent");
$indentButton.click();
//type a bit, make a line break and type again
var $firstTextElement = inner$("div span").first();
$firstTextElement.sendkeys('line 1');
$firstTextElement.sendkeys('{enter}');
$firstTextElement.sendkeys('line 2');
$firstTextElement.sendkeys('{enter}');
helper.waitFor(function(){
return inner$("div span").first().text().indexOf("line 2") === -1;
}).done(function(){
var $newSecondLine = inner$("div").first().next();
var hasULElement = $newSecondLine.find("ul li").length === 1;
expect(hasULElement).to.be(true);
expect($newSecondLine.text()).to.be("line 2");
done();
});
});
/*
it("makes text indented and outdented", function() {
//get the inner iframe
var $inner = testHelper.$getPadInner();
//get the first text element out of the inner iframe
var firstTextElement = $inner.find("div").first();
//select this text element
testHelper.selectText(firstTextElement[0], $inner);
//get the indentation button and click it
var $indentButton = testHelper.$getPadChrome().find(".buttonicon-indent");
$indentButton.click();
//ace creates a new dom element when you press a button, so just get the first text element again
var newFirstTextElement = $inner.find("div").first();
// is there a list-indent class element now?
var firstChild = newFirstTextElement.children(":first");
var isUL = firstChild.is('ul');
//expect it to be the beginning of a list
expect(isUL).to.be(true);
var secondChild = firstChild.children(":first");
var isLI = secondChild.is('li');
//expect it to be part of a list
expect(isLI).to.be(true);
//indent again
$indentButton.click();
var newFirstTextElement = $inner.find("div").first();
// is there a list-indent class element now?
var firstChild = newFirstTextElement.children(":first");
var hasListIndent2 = firstChild.hasClass('list-indent2');
//expect it to be part of a list
expect(hasListIndent2).to.be(true);
//make sure the text hasn't changed
expect(newFirstTextElement.text()).to.eql(firstTextElement.text());
// test outdent
//get the unindentation button and click it twice
var $outdentButton = testHelper.$getPadChrome().find(".buttonicon-outdent");
$outdentButton.click();
$outdentButton.click();
//ace creates a new dom element when you press a button, so just get the first text element again
var newFirstTextElement = $inner.find("div").first();
// is there a list-indent class element now?
var firstChild = newFirstTextElement.children(":first");
var isUL = firstChild.is('ul');
//expect it not to be the beginning of a list
expect(isUL).to.be(false);
var secondChild = firstChild.children(":first");
var isLI = secondChild.is('li');
//expect it to not be part of a list
expect(isLI).to.be(false);
//make sure the text hasn't changed
expect(newFirstTextElement.text()).to.eql(firstTextElement.text());
// Next test tests multiple line indentation
//select this text element
testHelper.selectText(firstTextElement[0], $inner);
//indent twice
$indentButton.click();
$indentButton.click();
//get the first text element out of the inner iframe
var firstTextElement = $inner.find("div").first();
//select this text element
testHelper.selectText(firstTextElement[0], $inner);
/* this test creates the below content, both should have double indentation
line1
line2
firstTextElement.sendkeys('{rightarrow}'); // simulate a keypress of enter
firstTextElement.sendkeys('{enter}'); // simulate a keypress of enter
firstTextElement.sendkeys('line 1'); // simulate writing the first line
firstTextElement.sendkeys('{enter}'); // simulate a keypress of enter
firstTextElement.sendkeys('line 2'); // simulate writing the second line
//get the second text element out of the inner iframe
setTimeout(function(){ // THIS IS REALLY BAD
var secondTextElement = $('iframe').contents().find('iframe').contents().find('iframe').contents().find('body > div').get(1); // THIS IS UGLY
// is there a list-indent class element now?
var firstChild = secondTextElement.children(":first");
var isUL = firstChild.is('ul');
//expect it to be the beginning of a list
expect(isUL).to.be(true);
var secondChild = secondChild.children(":first");
var isLI = secondChild.is('li');
//expect it to be part of a list
expect(isLI).to.be(true);
//get the first text element out of the inner iframe
var thirdTextElement = $('iframe').contents().find('iframe').contents().find('iframe').contents().find('body > div').get(2); // THIS IS UGLY TOO
// is there a list-indent class element now?
var firstChild = thirdTextElement.children(":first");
var isUL = firstChild.is('ul');
//expect it to be the beginning of a list
expect(isUL).to.be(true);
var secondChild = firstChild.children(":first");
var isLI = secondChild.is('li');
//expect it to be part of a list
expect(isLI).to.be(true);
},1000);
});*/
});

View file

@ -0,0 +1,36 @@
describe("italic button", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes text italic", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//get the first text element out of the inner iframe
var $firstTextElement = inner$("div").first();
//select this text element
$firstTextElement.sendkeys('{selectall}');
//get the bold button and click it
var $boldButton = chrome$(".buttonicon-italic");
$boldButton.click();
//ace creates a new dom element when you press a button, so just get the first text element again
var $newFirstTextElement = inner$("div").first();
// is there a <i> element now?
var isItalic = $newFirstTextElement.find("i").length === 1;
//expect it to be bold
expect(isItalic).to.be(true);
//make sure the text hasn't changed
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
done();
});
});

View file

@ -0,0 +1,47 @@
describe("assign ordered list", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("insert ordered list text", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
var $insertorderedlistButton = chrome$(".buttonicon-insertorderedlist");
$insertorderedlistButton.click();
helper.waitFor(function(){
return inner$("div").first().find("ol li").length === 1;
}).done(done);
});
xit("issue #1125 keeps the numbered list on enter for the new line - EMULATES PASTING INTO A PAD", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
var $insertorderedlistButton = chrome$(".buttonicon-insertorderedlist");
$insertorderedlistButton.click();
//type a bit, make a line break and type again
var $firstTextElement = inner$("div span").first();
$firstTextElement.sendkeys('line 1');
$firstTextElement.sendkeys('{enter}');
$firstTextElement.sendkeys('line 2');
$firstTextElement.sendkeys('{enter}');
helper.waitFor(function(){
return inner$("div span").first().text().indexOf("line 2") === -1;
}).done(function(){
var $newSecondLine = inner$("div").first().next();
var hasOLElement = $newSecondLine.find("ol li").length === 1;
console.log($newSecondLine.find("ol"));
expect(hasOLElement).to.be(true);
expect($newSecondLine.text()).to.be("line 2");
var hasLineNumber = $newSecondLine.find("ol").attr("start") === 2;
expect(hasLineNumber).to.be(true); // This doesn't work because pasting in content doesn't work
done();
});
});
});

View file

@ -0,0 +1,37 @@
describe("undo button then redo button", function(){
beforeEach(function(cb){
helper.newPad(cb); // creates a new pad
this.timeout(60000);
});
it("undo some typing", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
// get the first text element inside the editable space
var $firstTextElement = inner$("div span").first();
var originalValue = $firstTextElement.text(); // get the original value
var newString = "Foo";
$firstTextElement.sendkeys(newString); // send line 1 to the pad
var modifiedValue = $firstTextElement.text(); // get the modified value
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
// get undo and redo buttons
var $undoButton = chrome$(".buttonicon-undo");
var $redoButton = chrome$(".buttonicon-redo");
// click the buttons
$undoButton.click(); // removes foo
$redoButton.click(); // resends foo
helper.waitFor(function(){
console.log(inner$("div span").first().text());
return inner$("div span").first().text() === newString;
}).done(function(){
var finalValue = inner$("div").first().text();
expect(finalValue).to.be(modifiedValue); // expect the value to change
done();
});
});
});

View file

@ -0,0 +1,36 @@
describe("strikethrough button", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes text strikethrough", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//get the first text element out of the inner iframe
var $firstTextElement = inner$("div").first();
//select this text element
$firstTextElement.sendkeys('{selectall}');
//get the strikethrough button and click it
var $strikethroughButton = chrome$(".buttonicon-strikethrough");
$strikethroughButton.click();
//ace creates a new dom element when you press a button, so just get the first text element again
var $newFirstTextElement = inner$("div").first();
// is there a <i> element now?
var isstrikethrough = $newFirstTextElement.find("s").length === 1;
//expect it to be strikethrough
expect(isstrikethrough).to.be(true);
//make sure the text hasn't changed
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
done();
});
});

View file

@ -0,0 +1,47 @@
//deactivated, we need a nice way to get the timeslider, this is ugly
xdescribe("timeslider button takes you to the timeslider of a pad", function(){
beforeEach(function(cb){
helper.newPad(cb); // creates a new pad
this.timeout(60000);
});
it("timeslider contained in URL", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
// get the first text element inside the editable space
var $firstTextElement = inner$("div span").first();
var originalValue = $firstTextElement.text(); // get the original value
var newValue = "Testing"+originalValue;
$firstTextElement.sendkeys("Testing"); // send line 1 to the pad
var modifiedValue = $firstTextElement.text(); // get the modified value
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
helper.waitFor(function(){
return modifiedValue !== originalValue; // The value has changed so we can..
}).done(function(){
var $timesliderButton = chrome$("#timesliderlink");
$timesliderButton.click(); // So click the timeslider link
helper.waitFor(function(){
var iFrameURL = chrome$.window.location.href;
if(iFrameURL){
return iFrameURL.indexOf("timeslider") !== -1;
}else{
return false; // the URL hasnt been set yet
}
}).done(function(){
// click the buttons
var iFrameURL = chrome$.window.location.href; // get the url
var inTimeslider = iFrameURL.indexOf("timeslider") !== -1;
expect(inTimeslider).to.be(true); // expect the value to change
done();
});
});
});
});

View file

@ -0,0 +1,33 @@
describe("undo button", function(){
beforeEach(function(cb){
helper.newPad(cb); // creates a new pad
this.timeout(60000);
});
it("undo some typing", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
// get the first text element inside the editable space
var $firstTextElement = inner$("div span").first();
var originalValue = $firstTextElement.text(); // get the original value
$firstTextElement.sendkeys("foo"); // send line 1 to the pad
var modifiedValue = $firstTextElement.text(); // get the modified value
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
// get clear authorship button as a variable
var $undoButton = chrome$(".buttonicon-undo");
// click the button
$undoButton.click();
helper.waitFor(function(){
return inner$("div span").first().text() === originalValue;
}).done(function(){
var finalValue = inner$("div span").first().text();
expect(finalValue).to.be(originalValue); // expect the value to change
done();
});
});
});

View file

@ -0,0 +1,72 @@
describe("change username value", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("Remembers the user name after a refresh", function(done) {
this.timeout(60000);
var chrome$ = helper.padChrome$;
//click on the settings button to make settings visible
var $userButton = chrome$(".buttonicon-showusers");
$userButton.click();
var $usernameInput = chrome$("#myusernameedit");
$usernameInput.click();
$usernameInput.val('John McLear');
$usernameInput.blur();
setTimeout(function(){ //give it a second to save the username on the server side
helper.newPad({ // get a new pad, but don't clear the cookies
clearCookies: false
, cb: function(){
var chrome$ = helper.padChrome$;
//click on the settings button to make settings visible
var $userButton = chrome$(".buttonicon-showusers");
$userButton.click();
var $usernameInput = chrome$("#myusernameedit");
expect($usernameInput.val()).to.be('John McLear')
done();
}
});
}, 1000);
});
it("Own user name is shown when you enter a chat", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//click on the settings button to make settings visible
var $userButton = chrome$(".buttonicon-showusers");
$userButton.click();
var $usernameInput = chrome$("#myusernameedit");
$usernameInput.click();
$usernameInput.val('John McLear');
$usernameInput.blur();
//click on the chat button to make chat visible
var $chatButton = chrome$("#chaticon");
$chatButton.click();
var $chatInput = chrome$("#chatinput");
$chatInput.sendkeys('O hi'); // simulate a keypress of typing JohnMcLear
$chatInput.sendkeys('{enter}'); // simulate a keypress of enter actually does evt.which = 10 not 13
//check if chat shows up
helper.waitFor(function(){
return chrome$("#chattext").children("p").length !== 0; // wait until the chat message shows up
}).done(function(){
var $firstChatMessage = chrome$("#chattext").children("p");
var containsJohnMcLear = $firstChatMessage.text().indexOf("John McLear") !== -1; // does the string contain John McLear
expect(containsJohnMcLear).to.be(true); // expect the first chat message to contain JohnMcLear
done();
});
});
});

View file

@ -0,0 +1,40 @@
describe("chat always ons creen select", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes chat stick to right side of the screen", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//click on the settings button to make settings visible
var $settingsButton = chrome$(".buttonicon-settings");
$settingsButton.click();
//get the chat selector
var $stickychatCheckbox = chrome$("#options-stickychat");
//select chat always on screen and fire change event
$stickychatCheckbox.attr('selected','selected');
$stickychatCheckbox.change();
$stickychatCheckbox.click();
//check if chat changed to get the stickychat Class
var $chatbox = chrome$("#chatbox");
var hasStickyChatClass = $chatbox.hasClass("stickyChat");
expect(hasStickyChatClass).to.be(true);
//select chat always on screen and fire change event
$stickychatCheckbox.attr('selected','selected');
$stickychatCheckbox.change();
$stickychatCheckbox.click();
//check if chat changed to remove the stickychat Class
var hasStickyChatClass = $chatbox.hasClass("stickyChat");
expect(hasStickyChatClass).to.be(false);
done();
});
});

View file

@ -0,0 +1,133 @@
describe("embed links", function(){
var objectify = function (str)
{
var hash = {};
var parts = str.split('&');
for(var i = 0; i < parts.length; i++)
{
var keyValue = parts[i].split('=');
hash[keyValue[0]] = keyValue[1];
}
return hash;
}
var checkiFrameCode = function(embedCode, readonly){
//turn the code into an html element
var $embediFrame = $(embedCode);
//read and check the frame attributes
var width = $embediFrame.attr("width");
var height = $embediFrame.attr("height");
var name = $embediFrame.attr("name");
expect(width).to.be('600');
expect(height).to.be('400');
expect(name).to.be(readonly ? "embed_readonly" : "embed_readwrite");
//parse the url
var src = $embediFrame.attr("src");
var questionMark = src.indexOf("?");
var url = src.substr(0,questionMark);
var paramsStr = src.substr(questionMark+1);
var params = objectify(paramsStr);
var expectedParams = {
showControls: 'true'
, showChat: 'true'
, showLineNumbers: 'true'
, useMonospaceFont: 'false'
}
//check the url
if(readonly){
expect(url.indexOf("r.") > 0).to.be(true);
} else {
expect(url).to.be(helper.padChrome$.window.location.href);
}
//check if all parts of the url are like expected
expect(params).to.eql(expectedParams);
}
describe("read and write", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
describe("the share link", function(){
it("is the actual pad url", function(done){
var chrome$ = helper.padChrome$;
//open share dropdown
chrome$(".buttonicon-embed").click();
//get the link of the share field + the actual pad url and compare them
var shareLink = chrome$("#linkinput").val();
var padURL = chrome$.window.location.href;
expect(shareLink).to.be(padURL);
done();
});
});
describe("the embed as iframe code", function(){
it("is an iframe with the the correct url parameters and correct size", function(done){
var chrome$ = helper.padChrome$;
//open share dropdown
chrome$(".buttonicon-embed").click();
//get the link of the share field + the actual pad url and compare them
var embedCode = chrome$("#embedinput").val();
checkiFrameCode(embedCode, false)
done();
});
});
});
describe("when read only option is set", function(){
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
describe("the share link", function(){
it("shows a read only url", function(done){
var chrome$ = helper.padChrome$;
//open share dropdown
chrome$(".buttonicon-embed").click();
//check read only checkbox, a bit hacky
chrome$('#readonlyinput').attr('checked','checked').click().attr('checked','checked');
//get the link of the share field + the actual pad url and compare them
var shareLink = chrome$("#linkinput").val();
var containsReadOnlyLink = shareLink.indexOf("r.") > 0
expect(containsReadOnlyLink).to.be(true);
done();
});
});
describe("the embed as iframe code", function(){
it("is an iframe with the the correct url parameters and correct size", function(done){
var chrome$ = helper.padChrome$;
//open share dropdown
chrome$(".buttonicon-embed").click();
//check read only checkbox, a bit hacky
chrome$('#readonlyinput').attr('checked','checked').click().attr('checked','checked');
//get the link of the share field + the actual pad url and compare them
var embedCode = chrome$("#embedinput").val();
checkiFrameCode(embedCode, true);
done();
});
});
});
});

View file

@ -0,0 +1,30 @@
describe("font select", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes text monospace", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//click on the settings button to make settings visible
var $settingsButton = chrome$(".buttonicon-settings");
$settingsButton.click();
//get the font menu and monospace option
var $viewfontmenu = chrome$("#viewfontmenu");
var $monospaceoption = $viewfontmenu.find("[value=monospace]");
//select monospace and fire change event
$monospaceoption.attr('selected','selected');
$viewfontmenu.change();
//check if font changed to monospace
var fontFamily = inner$("body").css("font-family").toLowerCase();
expect(fontFamily).to.be("monospace");
done();
});
});

View file

@ -0,0 +1,99 @@
describe("the test helper", function(){
describe("the newPad method", function(){
xit("doesn't leak memory if you creates iframes over and over again", function(done){
this.timeout(100000);
var times = 10;
var loadPad = function(){
helper.newPad(function(){
times--;
if(times > 0){
loadPad();
} else {
done();
}
})
}
loadPad();
});
it("gives me 3 jquery instances of chrome, outer and inner", function(done){
this.timeout(5000);
helper.newPad(function(){
//check if the jquery selectors have the desired elements
expect(helper.padChrome$("#editbar").length).to.be(1);
expect(helper.padOuter$("#outerdocbody").length).to.be(1);
expect(helper.padInner$("#innerdocbody").length).to.be(1);
//check if the document object was set correctly
expect(helper.padChrome$.window.document).to.be(helper.padChrome$.document);
expect(helper.padOuter$.window.document).to.be(helper.padOuter$.document);
expect(helper.padInner$.window.document).to.be(helper.padInner$.document);
done();
});
});
});
describe("the waitFor method", function(){
it("takes a timeout and waits long enough", function(done){
this.timeout(2000);
var startTime = new Date().getTime();
helper.waitFor(function(){
return false;
}, 1500).fail(function(){
var duration = new Date().getTime() - startTime;
expect(duration).to.be.greaterThan(1400);
done();
});
});
it("takes an interval and checks on every interval", function(done){
this.timeout(4000);
var checks = 0;
helper.waitFor(function(){
checks++;
return false;
}, 2000, 100).fail(function(){
expect(checks).to.be.greaterThan(10);
expect(checks).to.be.lessThan(30);
done();
});
});
describe("returns a deferred object", function(){
it("it calls done after success", function(done){
helper.waitFor(function(){
return true;
}).done(function(){
done();
});
});
it("calls fail after failure", function(done){
helper.waitFor(function(){
return false;
},0).fail(function(){
done();
});
});
xit("throws if you don't listen for fails", function(done){
var onerror = window.onerror;
window.onerror = function(){
window.onerror = onerror;
done();
}
helper.waitFor(function(){
return false;
},100);
});
});
});
});

View file

@ -0,0 +1,39 @@
describe("send chat message", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("opens chat, sends a message and makes sure it exists on the page", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
var chatValue = "JohnMcLear";
//click on the chat button to make chat visible
var $chatButton = chrome$("#chaticon");
$chatButton.click();
var $chatInput = chrome$("#chatinput");
$chatInput.sendkeys('JohnMcLear'); // simulate a keypress of typing JohnMcLear
$chatInput.sendkeys('{enter}'); // simulate a keypress of enter actually does evt.which = 10 not 13
//check if chat shows up
helper.waitFor(function(){
return chrome$("#chattext").children("p").length !== 0; // wait until the chat message shows up
}).done(function(){
var $firstChatMessage = chrome$("#chattext").children("p");
var containsMessage = $firstChatMessage.text().indexOf("JohnMcLear") !== -1; // does the string contain JohnMcLear?
expect(containsMessage).to.be(true); // expect the first chat message to contain JohnMcLear
// do a slightly more thorough check
var username = $firstChatMessage.children("b");
var usernameValue = username.text();
var time = $firstChatMessage.children(".time");
var timeValue = time.text();
var expectedStringIncludingUserNameAndTime = usernameValue + timeValue + " " + "JohnMcLear";
expect(expectedStringIncludingUserNameAndTime).to.be($firstChatMessage.text());
done();
});
});
});

View file

@ -0,0 +1,37 @@
describe("delete keystroke", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes text delete", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//get the first text element out of the inner iframe
var $firstTextElement = inner$("div").first();
// get the original length of this element
var elementLength = $firstTextElement.text().length;
// get the original string value minus the last char
var originalTextValue = $firstTextElement.text();
originalTextValueMinusFirstChar = originalTextValue.substring(1, originalTextValue.length );
// simulate key presses to delete content
$firstTextElement.sendkeys('{leftarrow}'); // simulate a keypress of the left arrow key
$firstTextElement.sendkeys('{del}'); // simulate a keypress of delete
//ace creates a new dom element when you press a keystroke, so just get the first text element again
var $newFirstTextElement = inner$("div").first();
// get the new length of this element
var newElementLength = $newFirstTextElement.text().length;
//expect it to be one char less in length
expect(newElementLength).to.be((elementLength-1));
done();
});
});

View file

@ -0,0 +1,34 @@
describe("enter keystroke", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("creates a enw line & puts cursor onto a new line", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//get the first text element out of the inner iframe
var $firstTextElement = inner$("div").first();
// get the original string value minus the last char
var originalTextValue = $firstTextElement.text();
// simulate key presses to enter content
$firstTextElement.sendkeys('{enter}');
//ace creates a new dom element when you press a keystroke, so just get the first text element again
var $newFirstTextElement = inner$("div").first();
helper.waitFor(function(){
return inner$("div").first().text() === "";
}).done(function(){
var $newSecondLine = inner$("div").first().next();
var newFirstTextElementValue = inner$("div").first().text();
expect(newFirstTextElementValue).to.be(""); // expect the first line to be blank
expect($newSecondLine.text()).to.be(originalTextValue); // expect the second line to be the same as the original first line.
done();
});
});
});

View file

@ -0,0 +1,24 @@
describe("urls", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("when you enter an url, it becomes clickable", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//get the first text element out of the inner iframe
var firstTextElement = inner$("div").first();
// simulate key presses to delete content
firstTextElement.sendkeys('{selectall}'); // select all
firstTextElement.sendkeys('{del}'); // clear the first line
firstTextElement.sendkeys('http://etherpad.org'); // insert a URL
helper.waitFor(function(){
return inner$("div").first().find("a").length === 1;
}, 2000).done(done);
});
});

View file

@ -0,0 +1,83 @@
describe("Language select and change", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes text german", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//click on the settings button to make settings visible
var $settingsButton = chrome$(".buttonicon-settings");
$settingsButton.click();
//click the language button
var $language = chrome$("#languagemenu");
var $languageoption = $language.find("[value=de]");
//select german
$languageoption.attr('selected','selected');
$language.change();
helper.waitFor(function(){
var $boldButton = chrome$(".buttonicon-bold").parent();
//get the title of the bold button
var boldButtonTitle = $boldButton[0]["title"];
return boldButtonTitle !== undefined;
}).done(function(){
//get the value of the bold button
var $boldButton = chrome$(".buttonicon-bold").parent();
//get the title of the bold button
var boldButtonTitle = $boldButton[0]["title"];
//check if the language is now german
expect(boldButtonTitle).to.be("Fett (Strg-B)");
done();
});
});
it("makes text English", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//click on the settings button to make settings visible
var $settingsButton = chrome$(".buttonicon-settings");
$settingsButton.click();
//click the language button
var $language = chrome$("#languagemenu");
var $languageoption = $language.find("[value=en]");
//select german
$languageoption.attr('selected','selected');
$language.change();
helper.waitFor(function(){
var $boldButton = chrome$(".buttonicon-bold").parent();
//get the title of the bold button
var boldButtonTitle = $boldButton[0]["title"];
return boldButtonTitle !== undefined;
}).done(function(){
//get the value of the bold button
var $boldButton = chrome$(".buttonicon-bold").parent();
//get the title of the bold button
var boldButtonTitle = $boldButton[0]["title"];
//check if the language is now English
expect(boldButtonTitle).to.be("Bold (Ctrl-B)");
done();
});
});
});

2
tests/frontend/travis/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
sauce_connect.log
sauce_connect.log.*

View file

@ -0,0 +1,110 @@
var srcFolder = "../../../src/node_modules/";
var wd = require(srcFolder + "wd");
var async = require(srcFolder + "async");
var config = {
host: "ondemand.saucelabs.com"
, port: 80
, username: process.env.SAUCE_USER
, accessKey: process.env.SAUCE_KEY
}
var allTestsPassed = true;
var sauceTestWorker = async.queue(function (testSettings, callback) {
var browser = wd.remote(config.host, config.port, config.username, config.accessKey);
var browserChain = browser.chain();
var name = process.env.GIT_HASH + " - " + testSettings.browserName + " " + testSettings.version + ", " + testSettings.platform;
testSettings.name = name;
testSettings["public"] = true;
testSettings["build"] = process.env.GIT_HASH;
browserChain.init(testSettings).get("http://localhost:9001/tests/frontend/", function(){
var url = "https://saucelabs.com/jobs/" + browser.sessionID;
console.log("Remote sauce test '" + name + "' started! " + url);
//tear down the test excecution
var stopSauce = function(success){
getStatusInterval && clearInterval(getStatusInterval);
clearTimeout(timeout);
browserChain.quit();
if(!success){
allTestsPassed = false;
}
var testResult = knownConsoleText.replace(/\[red\]/g,'\x1B[31m').replace(/\[yellow\]/g,'\x1B[33m')
.replace(/\[green\]/g,'\x1B[32m').replace(/\[clear\]/g, '\x1B[39m');
testResult = testResult.split("\\n").map(function(line){
return "[" + testSettings.browserName + (testSettings.version === "" ? '' : (" " + testSettings.version)) + "] " + line;
}).join("\n");
console.log(testResult);
console.log("Remote sauce test '" + name + "' finished! " + url);
callback();
}
//timeout for the case the test hangs
var timeout = setTimeout(function(){
stopSauce(false);
}, 60000 * 10);
var knownConsoleText = "";
var getStatusInterval = setInterval(function(){
browserChain.eval("$('#console').text()", function(err, consoleText){
if(!consoleText || err){
return;
}
knownConsoleText = consoleText;
if(knownConsoleText.indexOf("FINISHED") > 0){
var success = knownConsoleText.indexOf("FAILED") === -1;
stopSauce(success);
}
});
}, 5000);
});
}, 5); //run 5 tests in parrallel
// Firefox
sauceTestWorker.push({
'platform' : 'Linux'
, 'browserName' : 'firefox'
, 'version' : ''
});
// Chrome
sauceTestWorker.push({
'platform' : 'Linux'
, 'browserName' : 'googlechrome'
, 'version' : ''
});
// IE 8
sauceTestWorker.push({
'platform' : 'Windows 2003'
, 'browserName' : 'iexplore'
, 'version' : '8'
});
// IE 9
sauceTestWorker.push({
'platform' : 'Windows 2008'
, 'browserName' : 'iexplore'
, 'version' : '9'
});
// IE 10
sauceTestWorker.push({
'platform' : 'Windows 2012'
, 'browserName' : 'iexplore'
, 'version' : '10'
});
sauceTestWorker.drain = function() {
setTimeout(function(){
process.exit(allTestsPassed ? 0 : 1);
}, 3000);
}

18
tests/frontend/travis/runner.sh Executable file
View file

@ -0,0 +1,18 @@
#!/bin/sh
#Move to the base folder
cd `dirname $0`
#start etherpad lite
../../../bin/run.sh > /dev/null &
sleep 10
#start remote runner
node remote_runner.js
exit_code=$?
kill $!
kill $(cat /tmp/sauce.pid)
sleep 30
exit $exit_code

View file

@ -0,0 +1,16 @@
#!/bin/bash
# download and unzip the sauce connector
curl http://saucelabs.com/downloads/Sauce-Connect-latest.zip > /tmp/sauce.zip
unzip /tmp/sauce.zip -d /tmp
# start the sauce connector in background and make sure it doesn't output the secret key
(java -jar /tmp/Sauce-Connect.jar $SAUCE_USER $SAUCE_KEY -f /tmp/tunnel > /dev/null )&
# save the sauce pid in a file
echo $! > /tmp/sauce.pid
# wait for the tunnel to build up
while [ ! -e "/tmp/tunnel" ]
do
sleep 1
done