Compare commits

...

5 Commits

Author SHA1 Message Date
DadaMonad
93e75e0111 updated npm+bower version to v0.3.1 2015-01-30 11:30:11 +00:00
DadaMonad
cbcdebf33e updated connector functionality 2015-01-30 11:29:05 +00:00
DadaMonad
7c842efd52 updated README 2015-01-27 19:32:51 +00:00
DadaMonad
60daa082f7 updated README 2015-01-27 17:59:58 +00:00
DadaMonad
5681ba84bf added lost dependencies 2015-01-26 23:35:36 +00:00
20 changed files with 38426 additions and 18 deletions

View File

@@ -1,23 +1,23 @@
# ![Yatta!](https://dadamonad.github.io/files/layout/y_logo.png)
# ![Yatta!](https://dadamonad.github.io/files/layout/yjs.svg)
[![Build Status](http://layers.dbis.rwth-aachen.de/jenkins/job/Yatta/badge/icon)](http://layers.dbis.rwth-aachen.de/jenkins/job/Yatta/)
Y is a framework for optimistic concurrency control and automatic conflict resolution on arbitrary data types. The framework implements a new OT-like concurrency algorithm and provides similar functionality as [ShareJs] and [OpenCoweb]. Y was designed to take away the pain from concurrently editing complex data types like Text, Json, and XML. For more information you should check out the [website](https://dadamonad.github.io/yjs/)!
Yjs is a framework for optimistic concurrency control and automatic conflict resolution on arbitrary data types. The framework implements a new OT-like concurrency algorithm and provides similar functionality as [ShareJs] and [OpenCoweb]. Yjs was designed to take away the pain from concurrently editing complex data types like Text, Json, and XML. You can find some applications for this framework [here](https://dadamonad.github.io/yjs/examples/).
In the future, we want to enable users to implement their own collaborative types. Currently we provide data types for
* Text
* Json
* XML
Unlike other frameworks, Y supports P2P message propagation and is not bound to a specific communication protocol. Therefore, Y is extremely scalable and can be used in a wide range of application scenarios.
Unlike other frameworks, Yjs supports P2P message propagation and is not bound to a specific communication protocol. Therefore, Yjs is extremely scalable and can be used in a wide range of application scenarios.
We support several communication protocols as so called *Connectors*. You find a bunch of Connectors in the [Y-Connectors](https://github.com/rwth-acis/y-connectors) repository. Currently supported communication protocols:
We support several communication protocols as so called *Connectors*. You find a bunch of Connectors in the [y-connectors](https://github.com/rwth-acis/y-connectors) repository. Currently supported communication protocols:
* [XMPP-Connector](http://xmpp.org) - Propagates updates in a XMPP multi-user-chat room
* [WebRTC-Connector](http://peerjs.com/) - Propagate updates directly with WebRTC
* [IWC-Connector](http://dbis.rwth-aachen.de/cms/projects/the-xmpp-experience#interwidget-communication) - Inter-widget Communication
You can use Y client-, and server- side. You can get it as via npm, and bower. We even provide a polymer element for Y!
You can use Yjs client-, and server- side. You can get it as via npm, and bower. We even provide a polymer element for Yjs!
The theoretical advantages over similar frameworks are support for
* .. P2P message propagation and arbitrary communication protocols
@@ -26,6 +26,7 @@ The theoretical advantages over similar frameworks are support for
* .. AnyUndo: Undo *any* action that was executed in constant time (unimplemented)
* .. Intention Preservation: When working on Text, the intention of your changes are preserved. This is particularily important when working offline.
## Use it!
You find a tutorial, examples, and documentation on the [website](https://dadamonad.github.io/yjs/).
@@ -33,7 +34,7 @@ Either clone this git repository, install it with [bower](http://bower.io/), or
### Bower
```
bower install yjs
bower install rwth-acis/yjs
```
Then you include the libraries directly from the installation folder.
```
@@ -51,7 +52,7 @@ Y = require("yjs");
```
## Status
yjs is still in an early development phase. Don't expect that everything is working fine.
Yjs is still in an early development phase. Don't expect that everything is working fine.
But I would become really motivated if you gave me some feedback :) ([github](https://github.com/rwth-acis/yjs/issues)).
### Current Issues
@@ -65,7 +66,7 @@ Please report _any_ issues to the [Github issue page](https://github.com/rwth-ac
I would appreciate if developers give me feedback on how _convenient_ the framework is, and if it is easy to use. Particularly the XML-support may not support every DOM-methods - if you encounter a method that does not cause any change on other peers, please state function name, and sample parameters. However, there are browser-specific features, that Y won't support.
## License
yjs is licensed under the [MIT License](./LICENSE.txt).
Yjs is licensed under the [MIT License](./LICENSE.txt).
[ShareJs]: https://github.com/share/ShareJS
[OpenCoweb]: https://github.com/opencoweb/coweb

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "0.3.0",
"version": "0.3.1",
"homepage": "https://github.com/DadaMonad/yjs",
"authors": [
"Kevin Jahns <kevin.jahns@rwth-aachen.de>"

2161
build/browser/y-object.js Normal file

File diff suppressed because one or more lines are too long

2066
build/browser/y.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -55,14 +55,12 @@ adaptConnector = function(connector, engine, HB, execution_listener) {
connector.getStateVector = getStateVector;
connector.getHB = getHB;
connector.applyHB = applyHB;
connector.whenReceiving(function(sender, op) {
connector.receive_handlers.push(function(sender, op) {
if (op.uid.creator !== HB.getUserId()) {
return engine.applyOp(op);
}
});
if (connector._whenBoundToY != null) {
return connector._whenBoundToY();
}
return connector.setIsBoundToY();
};
module.exports = adaptConnector;

92
build/node/y-object.js Normal file
View File

@@ -0,0 +1,92 @@
var Y, bindToChildren;
Y = require('./y');
bindToChildren = function(that) {
var attr, i, _i, _ref;
for (i = _i = 0, _ref = that.children.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
attr = that.children.item(i);
if (attr.name != null) {
attr.val = that.val.val(attr.name);
}
}
return that.val.observe(function(events) {
var event, newVal, _j, _len, _results;
_results = [];
for (_j = 0, _len = events.length; _j < _len; _j++) {
event = events[_j];
if (event.name != null) {
_results.push((function() {
var _k, _ref1, _results1;
_results1 = [];
for (i = _k = 0, _ref1 = that.children.length; 0 <= _ref1 ? _k < _ref1 : _k > _ref1; i = 0 <= _ref1 ? ++_k : --_k) {
attr = that.children.item(i);
if ((attr.name != null) && attr.name === event.name) {
newVal = that.val.val(attr.name);
if (attr.val !== newVal) {
_results1.push(attr.val = newVal);
} else {
_results1.push(void 0);
}
} else {
_results1.push(void 0);
}
}
return _results1;
})());
} else {
_results.push(void 0);
}
}
return _results;
});
};
Polymer("y-object", {
ready: function() {
if (this.connector != null) {
this.val = new Y(this.connector);
return bindToChildren(this);
} else if (this.val != null) {
return bindToChildren(this);
}
},
valChanged: function() {
if ((this.val != null) && this.val.type === "Object") {
return bindToChildren(this);
}
},
connectorChanged: function() {
if (this.val == null) {
this.val = new Y(this.connector);
return bindToChildren(this);
}
}
});
Polymer("y-property", {
ready: function() {
if ((this.val != null) && (this.name != null)) {
if (this.val.constructor === Object) {
this.val = this.parentElement.val(this.name, this.val).val(this.name);
} else if (typeof this.val === "string") {
this.parentElement.val(this.name, this.val);
}
if (this.val.type === "Object") {
return bindToChildren(this);
}
}
},
valChanged: function() {
var _ref;
if ((this.val != null) && (this.name != null)) {
if (this.val.constructor === Object) {
return this.val = this.parentElement.val.val(this.name, this.val).val(this.name);
} else if (this.val.type === "Object") {
return bindToChildren(this);
} else if ((((_ref = this.parentElement.val) != null ? _ref.val : void 0) != null) && this.val !== this.parentElement.val.val(this.name)) {
return this.parentElement.val.val(this.name, this.val);
}
}
}
});

54
build/node/y.js Normal file
View File

@@ -0,0 +1,54 @@
var Engine, HistoryBuffer, adaptConnector, createY, json_types_uninitialized,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
json_types_uninitialized = require("./Types/JsonTypes");
HistoryBuffer = require("./HistoryBuffer");
Engine = require("./Engine");
adaptConnector = require("./ConnectorAdapter");
createY = function(connector) {
var HB, Y, type_manager, types, user_id;
user_id = null;
if (connector.id != null) {
user_id = connector.id;
} else {
user_id = "_temp";
connector.onUserIdSet(function(id) {
user_id = id;
return HB.resetUserId(id);
});
}
HB = new HistoryBuffer(user_id);
type_manager = json_types_uninitialized(HB);
types = type_manager.types;
Y = (function(_super) {
__extends(Y, _super);
function Y() {
this.connector = connector;
this.HB = HB;
this.types = types;
this.engine = new Engine(this.HB, type_manager.types);
adaptConnector(this.connector, this.engine, this.HB, type_manager.execution_listener);
Y.__super__.constructor.apply(this, arguments);
}
Y.prototype.getConnector = function() {
return this.connector;
};
return Y;
})(types.Object);
return new Y(HB.getReservedUniqueIdentifier()).execute();
};
module.exports = createY;
if ((typeof window !== "undefined" && window !== null) && (window.Y == null)) {
window.Y = createY;
}

16760
build/test/Json_test.js Normal file

File diff suppressed because one or more lines are too long

16532
build/test/Text_test.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,38 @@
<link rel="import" href="../../y-object.html">
<link rel="import" href="../../../y-connectors/y-xmpp/y-xmpp.html">
<link rel="import" href="../../../paper-slider/paper-slider.html">
<polymer-element name="y-test" attributes="y connector stuff">
<template>
<h1 id="text" contentEditable> Check this out !</h1>
<y-xmpp id="connector" connector={{connector}} room="testy-xmpp-polymer"></y-xmpp>
<y-object connector={{connector}} val={{y}}>
<y-property name="slider" val={{slider}}>
</y-property>
<y-property name="stuff" val={{stuff}}>
<y-property id="otherstuff" name="otherstuff" val={{otherstuff}}>
</y-property>
</y-property>
</y-object>
<y-object val={{otherstuff}}>
<y-property name="nostuff" val={{nostuff}}>
</y-property>
</y-object>
<paper-slider min="0" max="200" immediateValue={{slider}}></paper-slider>
</template>
<script>
Polymer({
ready: function(){
window.y_stuff_property = this.$.otherstuff;
this.y.val("slider",50)
var that = this;
this.connector.whenSynced(function(){
if(that.y.val("text") == null){
that.y.val("text","stuff","mutable");
}
that.y.val("text").bind(that.$.text,that.shadowRoot)
})
}
})
</script>
</polymer-element>

View File

@@ -46,11 +46,10 @@ adaptConnector = (connector, engine, HB, execution_listener)->
connector.getHB = getHB
connector.applyHB = applyHB
connector.whenReceiving (sender, op)->
connector.receive_handlers.push (sender, op)->
if op.uid.creator isnt HB.getUserId()
engine.applyOp op
if connector._whenBoundToY?
connector._whenBoundToY()
connector.setIsBoundToY()
module.exports = adaptConnector

59
lib/y-object.coffee Normal file
View File

@@ -0,0 +1,59 @@
Y = require './y'
bindToChildren = (that)->
for i in [0...that.children.length]
attr = that.children.item(i)
if attr.name?
attr.val = that.val.val(attr.name)
that.val.observe (events)->
for event in events
if event.name?
for i in [0...that.children.length]
attr = that.children.item(i)
if attr.name? and attr.name is event.name
newVal = that.val.val(attr.name)
if attr.val isnt newVal
attr.val = newVal
Polymer "y-object",
ready: ()->
if @connector?
@val = new Y @connector
bindToChildren @
else if @val?
bindToChildren @
valChanged: ()->
if @val? and @val.type is "Object"
bindToChildren @
connectorChanged: ()->
if (not @val?)
@val = new Y @connector
bindToChildren @
Polymer "y-property",
ready: ()->
if @val? and @name?
if @val.constructor is Object
@val = @parentElement.val(@name,@val).val(@name)
# TODO: please use instanceof instead of .type,
# since it is more safe (consider someone putting a custom Object type here)
else if typeof @val is "string"
@parentElement.val(@name,@val)
if @val.type is "Object"
bindToChildren @
valChanged: ()->
if @val? and @name?
if @val.constructor is Object
@val = @parentElement.val.val(@name,@val).val(@name)
# TODO: please use instanceof instead of .type,
# since it is more safe (consider someone putting a custom Object type here)
else if @val.type is "Object"
bindToChildren @
else if @parentElement.val?.val? and @val isnt @parentElement.val.val(@name)
@parentElement.val.val @name, @val

48
lib/y.coffee Normal file
View File

@@ -0,0 +1,48 @@
json_types_uninitialized = require "./Types/JsonTypes"
HistoryBuffer = require "./HistoryBuffer"
Engine = require "./Engine"
adaptConnector = require "./ConnectorAdapter"
createY = (connector)->
user_id = null
if connector.id?
user_id = connector.id # TODO: change to getUniqueId()
else
user_id = "_temp"
connector.onUserIdSet (id)->
user_id = id
HB.resetUserId id
HB = new HistoryBuffer user_id
type_manager = json_types_uninitialized HB
types = type_manager.types
#
# Framework for Json data-structures.
# Known values that are supported:
# * String
# * Integer
# * Array
#
class Y extends types.Object
#
# @param {String} user_id Unique id of the peer.
# @param {Connector} Connector the connector class.
#
constructor: ()->
@connector = connector
@HB = HB
@types = types
@engine = new Engine @HB, type_manager.types
adaptConnector @connector, @engine, @HB, type_manager.execution_listener
super
getConnector: ()->
@connector
return new Y(HB.getReservedUniqueIdentifier()).execute()
module.exports = createY
if window? and not window.Y?
window.Y = createY

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "0.3.0",
"version": "0.3.1",
"description": "A Framework that enables Real-Time Collaboration on arbitrary data structures.",
"main": "./build/node/y.js",
"scripts": {

274
test/Json_test.coffee Normal file
View File

@@ -0,0 +1,274 @@
chai = require('chai')
expect = chai.expect
should = chai.should()
sinon = require('sinon')
sinonChai = require('sinon-chai')
_ = require("underscore")
chai.use(sinonChai)
Connector = require "../../y-connectors/lib/y-test/y-test.coffee"
Y = require "../lib/y.coffee"
Test = require "./TestSuite"
class JsonTest extends Test
makeNewUser: (userId)->
conn = new Connector userId
super new Y conn
type: "JsonTest"
getRandomRoot: (user_num, root)->
root ?= @users[user_num]
types = @users[user_num].types
if _.random(0,1) is 1 # take root
root
else # take child
elems = null
if root.type is "Object"
elems =
for oname,val of root.val()
val
else if root.type is "Array"
elems = root.val()
else
return root
elems = elems.filter (elem)->
(elem.type is "Array") or (elem.type is "Object")
if elems.length is 0
root
else
p = elems[_.random(0, elems.length-1)]
@getRandomRoot user_num, p
getContent: (user_num)->
@users[user_num].toJson(true)
getGeneratingFunctions: (user_num)->
types = @users[user_num].types
super(user_num).concat [
f : (y)=> # SET PROPERTY
l = y.val().length
y.val(_.random(0, l-1), @getRandomText(), 'immutable')
null
types : [types.Array]
, f : (y)=> # Delete Array Element
list = y.val()
if list.length > 0
key = list[_random(0,list.length-1)]
y.delete(key)
types: [types.Array]
, f : (y)=> # insert TEXT mutable
l = y.val().length
y.val(_.random(0, l-1), @getRamdomObject())
types: [types.Array]
, f : (y)=> # insert string
l = y.val().length
y.val(_.random(0, l-1), @getRandomText(), 'immutable')
null
types : [types.Array]
, f : (y)=> # Delete Object Property
list = for name, o of y.val()
name
if list.length > 0
key = list[_random(0,list.length-1)]
y.delete(key)
types: [types.Object]
, f : (y)=> # SET Object Property
y.val(@getRandomKey(), @getRandomObject())
types: [types.Object]
,
f : (y)=> # SET PROPERTY TEXT
y.val(@getRandomKey(), @getRandomText(), 'mutable')
types: [types.Object]
]
describe "JsonFramework", ->
beforeEach (done)->
@timeout 50000
@yTest = new JsonTest()
@users = @yTest.users
@test_user = @yTest.makeNewUser "test_user"
done()
it "can handle many engines, many operations, concurrently (random)", ->
console.log "" # TODO
@yTest.run()
it "has a working test suite", ->
@yTest.compareAll()
it "handles double-late-join", ->
test = new JsonTest("double")
test.run()
@yTest.run()
u1 = test.users[0]
u2 = @yTest.users[1]
ops1 = u1.HB._encode()
ops2 = u2.HB._encode()
u1.HB.renewStateVector u2.HB.getOperationCounter()
u2.HB.renewStateVector u1.HB.getOperationCounter()
u1.engine.applyOps ops2
u2.engine.applyOps ops1
expect(test.getContent(0)).to.deep.equal(@yTest.getContent(1))
it "can handle creaton of complex json (1)", ->
@yTest.users[0].val('a', 'q', "mutable")
@yTest.users[1].val('a', 't', "mutable")
@yTest.compareAll()
q = @yTest.users[2].val('a')
q.insert(0,'A')
@yTest.compareAll()
expect(@yTest.getSomeUser().val("a").val()).to.equal("At")
it "can handle creaton of complex json (2)", ->
@yTest.getSomeUser().val('x', {'a':'b'})
@yTest.getSomeUser().val('a', {'a':{q:"dtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt"}}, "mutable")
@yTest.getSomeUser().val('b', {'a':{}})
@yTest.getSomeUser().val('c', {'a':'c'})
@yTest.getSomeUser().val('c', {'a':'b'})
@yTest.compareAll()
q = @yTest.getSomeUser().val("a").val("a").val("q")
q.insert(0,'A')
@yTest.compareAll()
expect(@yTest.getSomeUser().val("a").val("a").val("q").val()).to.equal("Adtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt")
it "can handle creaton of complex json (3)", ->
@yTest.users[0].val('l', [1,2,3], "mutable")
@yTest.users[1].val('l', [4,5,6], "mutable")
@yTest.compareAll()
@yTest.users[2].val('l').insert(0,'A')
w = @yTest.users[1].val('l').insert(0,'B', "mutable").val(0)
w.insert 1, "C"
expect(w.val()).to.equal("BC")
@yTest.compareAll()
it "handles immutables and primitive data types", ->
@yTest.getSomeUser().val('string', "text", "immutable")
@yTest.getSomeUser().val('number', 4, "immutable")
@yTest.getSomeUser().val('object', {q:"rr"}, "immutable")
@yTest.getSomeUser().val('null', null)
@yTest.compareAll()
expect(@yTest.getSomeUser().val('string')).to.equal "text"
expect(@yTest.getSomeUser().val('number')).to.equal 4
expect(@yTest.getSomeUser().val('object').val('q')).to.equal "rr"
expect(@yTest.getSomeUser().val('null') is null).to.be.ok
it "handles immutables and primitive data types (2)", ->
@yTest.users[0].val('string', "text", "immutable")
@yTest.users[1].val('number', 4, "immutable")
@yTest.users[2].val('object', {q:"rr"}, "immutable")
@yTest.users[0].val('null', null)
@yTest.compareAll()
expect(@yTest.getSomeUser().val('string')).to.equal "text"
expect(@yTest.getSomeUser().val('number')).to.equal 4
expect(@yTest.getSomeUser().val('object').val('q')).to.equal "rr"
expect(@yTest.getSomeUser().val('null') is null).to.be.ok
it "Observers work on JSON Types (add type observers, local and foreign)", ->
u = @yTest.users[0]
@yTest.flushAll()
last_task = null
observer1 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("add")
expect(change.object).to.equal(u)
expect(change.changedBy).to.equal('0')
expect(change.name).to.equal("newStuff")
last_task = "observer1"
u.observe observer1
u.val("newStuff","someStuff","mutable")
expect(last_task).to.equal("observer1")
u.unobserve observer1
observer2 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("add")
expect(change.object).to.equal(u)
expect(change.changedBy).to.equal('1')
expect(change.name).to.equal("moreStuff")
last_task = "observer2"
u.observe observer2
v = @yTest.users[1]
v.val("moreStuff","someMoreStuff")
@yTest.flushAll()
expect(last_task).to.equal("observer2")
u.unobserve observer2
it "Observers work on JSON Types (update type observers, local and foreign)", ->
u = @yTest.users[0].val("newStuff","oldStuff","mutable").val("moreStuff","moreOldStuff","mutable")
@yTest.flushAll()
last_task = null
observer1 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("update")
expect(change.object).to.equal(u)
expect(change.changedBy).to.equal('0')
expect(change.name).to.equal("newStuff")
expect(change.oldValue.val()).to.equal("oldStuff")
last_task = "observer1"
u.observe observer1
u.val("newStuff","someStuff")
expect(last_task).to.equal("observer1")
u.unobserve observer1
observer2 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("update")
expect(change.object).to.equal(u)
expect(change.changedBy).to.equal('1')
expect(change.name).to.equal("moreStuff")
expect(change.oldValue.val()).to.equal("moreOldStuff")
last_task = "observer2"
u.observe observer2
v = @yTest.users[1]
v.val("moreStuff","someMoreStuff")
@yTest.flushAll()
expect(last_task).to.equal("observer2")
u.unobserve observer2
it "Observers work on JSON Types (delete type observers, local and foreign)", ->
u = @yTest.users[0].val("newStuff","oldStuff","mutable").val("moreStuff","moreOldStuff","mutable")
@yTest.flushAll()
last_task = null
observer1 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("delete")
expect(change.object).to.equal(u)
expect(change.changedBy).to.equal('0')
expect(change.name).to.equal("newStuff")
expect(change.oldValue.val()).to.equal("oldStuff")
last_task = "observer1"
u.observe observer1
u.delete("newStuff")
expect(last_task).to.equal("observer1")
u.unobserve observer1
observer2 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("delete")
expect(change.object).to.equal(u)
expect(change.changedBy).to.equal('1')
expect(change.name).to.equal("moreStuff")
expect(change.oldValue.val()).to.equal("moreOldStuff")
last_task = "observer2"
u.observe observer2
v = @yTest.users[1]
v.delete("moreStuff")
@yTest.flushAll()
expect(last_task).to.equal("observer2")
u.unobserve observer2

119
test/Text_test.coffee Normal file
View File

@@ -0,0 +1,119 @@
chai = require('chai')
expect = chai.expect
should = chai.should()
sinon = require('sinon')
sinonChai = require('sinon-chai')
_ = require("underscore")
chai.use(sinonChai)
Y = require "../lib/y"
Connector = require "../../y-connectors/lib/y-test/y-test.coffee"
Test = require "./TestSuite"
class TextTest extends Test
type: "TextTest"
makeNewUser: (userId)->
conn = new Connector userId
new Y conn
initUsers: (u)->
u.val("TextTest","","mutable")
getRandomRoot: (user_num)->
@users[user_num].val("TextTest")
getContent: (user_num)->
@users[user_num].val("TextTest").val()
describe "TextFramework", ->
beforeEach (done)->
@timeout 50000
@yTest = new TextTest()
done()
it "simple multi-char insert", ->
u = @yTest.users[0].val("TextTest")
u.insert 0, "abc"
u = @yTest.users[1].val("TextTest")
u.insert 0, "xyz"
@yTest.compareAll()
u.delete 0, 1
@yTest.compareAll()
expect(u.val()).to.equal("bcxyz")
it "Observers work on shared Text (insert type observers, local and foreign)", ->
u = @yTest.users[0].val("TextTest","my awesome Text","mutable").val("TextTest")
@yTest.flushAll()
last_task = null
observer1 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("insert")
expect(change.object).to.equal(u)
expect(change.value).to.equal("a")
expect(change.position).to.equal(1)
expect(change.changedBy).to.equal('0')
last_task = "observer1"
u.observe observer1
u.insert 1, "a"
expect(last_task).to.equal("observer1")
u.unobserve observer1
observer2 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("insert")
expect(change.object).to.equal(u)
expect(change.value).to.equal("x")
expect(change.position).to.equal(0)
expect(change.changedBy).to.equal('1')
last_task = "observer2"
u.observe observer2
v = @yTest.users[1].val("TextTest")
v.insert 0, "x"
@yTest.flushAll()
expect(last_task).to.equal("observer2")
u.unobserve observer2
it "Observers work on shared Text (delete type observers, local and foreign)", ->
u = @yTest.users[0].val("TextTest","my awesome Text","mutable").val("TextTest")
@yTest.flushAll()
last_task = null
observer1 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("delete")
expect(change.object).to.equal(u)
expect(change.position).to.equal(1)
expect(change.length).to.equal(1)
expect(change.changedBy).to.equal('0')
last_task = "observer1"
u.observe observer1
u.delete 1, 1
expect(last_task).to.equal("observer1")
u.unobserve observer1
observer2 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("delete")
expect(change.object).to.equal(u)
expect(change.position).to.equal(0)
expect(change.length).to.equal(1)
expect(change.changedBy).to.equal('1')
last_task = "observer2"
u.observe observer2
v = @yTest.users[1].val("TextTest")
v.delete 0, 1
@yTest.flushAll()
expect(last_task).to.equal("observer2")
u.unobserve observer2
it "can handle many engines, many operations, concurrently (random)", ->
console.log("testiy deleted this TODO:dtrn")
@yTest.run()

View File

@@ -0,0 +1,196 @@
chai = require('chai')
expect = chai.expect
should = chai.should()
sinon = require('sinon')
sinonChai = require('sinon-chai')
_ = require("underscore")
$ = require("jquery")
document?.$ = $ # for browser
require 'coffee-errors'
chai.use(sinonChai)
Y = require "../lib/index"
Connector = require "../../Yatta-Connectors/lib/test-connector/test-connector.coffee"
Test = require "./TestSuite"
class XmlTest extends Test
type: "XmlTest"
makeNewUser: (user, conn)->
super new Y.XmlFramework user, conn
getRandomRoot: (user_num)->
@users[user_num].getSharedObject()
getContent: (user_num)->
@users[user_num].val()
describe "XmlFramework", ->
beforeEach (done)->
@timeout 50000
@yTest = new XmlTest()
###
@users = @yTest.users
###
test_users = []
connector = (new Connector 0, test_users)
@test_user = @yTest.makeNewUser 0, connector
test_users.push @test_user
# test_user_listen listens to the actions of test_user. He will update his dom when he receives from test_user.
@test_user_listen = @yTest.makeNewUser 2, connector
test_users.push @test_user_listen
@test_user2 = @yTest.makeNewUser 1, (Connector_uninitialized [])
$("#test_dom").replaceWith('<div id="test_dom" test_attribute="the test" class="stuffy" style="color: blue"><p id="replaceme">replace me</p><p id="removeme">remove me</p><p>This is a test object for <b>XmlFramework</b></p><span class="span_element"><p>span</p></span></div>')
@$dom = $("#test_dom")
@dom = @$dom[0]
@test_user.val(@dom)
@test_user_listen.getConnector().flushAll()
@test_user_listen_dom = @test_user_listen.val()
@check = ()=>
dom_ = @dom.outerHTML
# now test if other collaborators can parse the HB and result in the same content
hb = @test_user.HB._encode()
@test_user2.engine.applyOps(hb)
dom2 = @test_user2.val()
expect(dom_).to.equal(dom2.outerHTML)
@test_user_listen.getConnector().flushAll()
expect(dom_).to.equal(@test_user_listen_dom.outerHTML)
done()
it "can transform to a new real Dom element", ->
dom_ = @test_user.val(true)
expect(dom_ isnt @dom).to.be.true
it "supports dom.insertBefore", ->
newdom = $("<p>dtrn</p>")[0]
newdom2 = $("<p>dtrn2</p>")[0]
n = $("#removeme")[0]
@dom.insertBefore(newdom, null)
@dom.insertBefore(newdom2, n)
@check()
it "supports dom.appendChild", ->
newdom = $("<p>dtrn</p>")[0]
@dom.appendChild(newdom)
@check()
it "supports dom.setAttribute", ->
@dom.setAttribute("test_attribute", "newVal")
@check()
it "supports dom.removeAttribute", ->
@dom.removeAttribute("test_attribute")
@check()
it "supports dom.removeChild", ->
@dom.removeChild($("#removeme")[0])
expect($("#removeme").length).to.equal(0)
@check()
it "supports dom.replaceChild", ->
newdom = $("<p>replaced</p>")[0]
replace = $("#replaceme")[0]
@dom.replaceChild(newdom,replace)
expect($("#replaceme").length).to.equal(0)
@check()
it "supports dom.classList.add", ->
@dom.classList.add "classy"
@check()
it "supports dom.textcontent", -> #TODO!!!!
@dom.classList.add "classy"
@check()
it "supports jquery.addClass", ->
@$dom.addClass("testy")
@check()
it "supports jquery.after", ->
d = $("#test_dom p")
d.after("<div class=\"inserted_after\">after</div>")
@check()
it "supports jquery.append", ->
d = $("#test_dom p")
d.after("<b>appended</b>")
@check()
it "supports jquery.appendTo", ->
$("<b>appendedTo</b>").appendTo("#test_dom p")
$("p").appendTo("#test_dom")
@check()
it "supports jquery.before", ->
d = $("#test_dom p")
d.before("<div>before</div>")
@check()
it "supports jquery.detach", ->
d = $(".inserted_after")
d.detach()
@check()
it "supports jquery.empty", ->
d = $("#test_dom p")
d.empty()
@check()
it "supports jquery.insertAfter", ->
$("<p>after span</p>").insertAfter(".span_element")
@check()
it "supports jquery.insertBefore", ->
$("<p>before span</p>").insertBefore(".span_element")
@check()
it "supports jquery.prepend", ->
d = $("#test_dom div")
d.prepend("<p>prepended</p>")
@check()
it "supports jquery.prependTo", ->
$("<p atone=false attwo=\"dtrn\" class=\"attr_node sudo su\">prepended to</p>").prependTo("#test_dom div")
@check()
it "supports jquery.remove", ->
d = $("#test_dom b")
d.remove()
@check()
it "supports jquery.removeAttr", ->
d = $(".attr_node")
d.removeAttr("attwo")
@check()
it "supports jquery.removeClass", ->
d = $(".attr_node")
d.removeClass("sudo")
@check()
it "supports jquery.attr", ->
d = $(".attr_node")
d.attr("atone", true)
@check()
it "supports jquery.replaceAll", ->
$("<span>New span content </span>").replaceAll("#test_dom div")
@check()
it "supports jquery.replaceWith", ->
d = $("#test_dom span")
d.replaceWith("<div>me is div again </div>")
@check()

8
y-object.html Normal file
View File

@@ -0,0 +1,8 @@
<polymer-element name="y-object" hidden attributes="val connector y">
</polymer-element>
<polymer-element name="y-property" hidden attributes="val name y">
</polymer-element>
<script src="./build/browser/y-object.js"></script>

2
y-object.js Normal file

File diff suppressed because one or more lines are too long

1
y.js Normal file

File diff suppressed because one or more lines are too long