loleaflet/README                                      |   19 +
 loleaflet/build/deps.js                               |   12 +
 loleaflet/debug/document/document_simple_example.html |    1 
 loleaflet/dist/leaflet.css                            |    5 
 loleaflet/package.json                                |    5 
 loleaflet/spec/headlessLoadTest.js                    |  179 ++++++++++++++++++
 loleaflet/spec/loadtest/LoadTestSpec.js               |    7 
 loleaflet/spec/suites/layer/marker/MarkerSpec.js      |    2 
 loleaflet/spec/suites/layer/tile/GridLayerSpec.js     |   74 -------
 loleaflet/spec/suites/layer/vector/CircleSpec.js      |    2 
 loleaflet/spec/suites/map/MapSpec.js                  |    2 
 loleaflet/src/control/Control.Styles.js               |   61 ++++++
 loleaflet/src/control/Styles.js                       |   17 +
 loleaflet/src/layer/Layer.js                          |    3 
 loleaflet/src/layer/tile/GridLayer.js                 |   24 +-
 loleaflet/src/layer/tile/TileLayer.js                 |    5 
 loleaflet/src/map/Map.js                              |   18 +
 loolwsd/LOOLSession.cpp                               |   40 +++-
 loolwsd/LOOLSession.hpp                               |    6 
 loolwsd/TileCache.cpp                                 |   42 ++--
 loolwsd/TileCache.hpp                                 |    6 
 loolwsd/protocol.txt                                  |    4 
 22 files changed, 412 insertions(+), 122 deletions(-)

New commits:
commit 573c6e5b3d3b82029560210878b13498b4f2e5c0
Author: Mihai Varga <[email protected]>
Date:   Thu Aug 20 14:43:40 2015 +0300

    loleaflet: always return the current size

diff --git a/loleaflet/src/map/Map.js b/loleaflet/src/map/Map.js
index c3f091a..ebe67ee 100644
--- a/loleaflet/src/map/Map.js
+++ b/loleaflet/src/map/Map.js
@@ -333,13 +333,11 @@ L.Map = L.Evented.extend({
        },
 
        getSize: function () {
-               if (!this._size || this._sizeChanged) {
-                       this._size = new L.Point(
-                               this._container.clientWidth,
-                               this._container.clientHeight);
+               this._size = new L.Point(
+                       this._container.clientWidth,
+                       this._container.clientHeight);
 
-                       this._sizeChanged = false;
-               }
+               this._sizeChanged = false;
                return this._size.clone();
        },
 
commit 7bd000a8fc3caeb36724f94c2dbe77cb7bfc16b0
Author: Mihai Varga <[email protected]>
Date:   Wed Aug 19 18:28:14 2015 +0300

    loleaflet: adapted some of the unit tests

diff --git a/loleaflet/spec/suites/layer/marker/MarkerSpec.js 
b/loleaflet/spec/suites/layer/marker/MarkerSpec.js
index 2d28fac..d33a8c9 100644
--- a/loleaflet/spec/suites/layer/marker/MarkerSpec.js
+++ b/loleaflet/spec/suites/layer/marker/MarkerSpec.js
@@ -32,7 +32,7 @@ describe("Marker", function () {
                        var afterIcon = marker._icon;
 
                        expect(beforeIcon).to.be(afterIcon);
-                       
expect(afterIcon.src).to.contain(icon2._getIconUrl('icon'));
+                       expect(afterIcon.src.replace(/\.\.\//g, 
'')).to.be.(icon2._getIconUrl('icon').replace(/\.\.\//g, ''));
                });
 
                it("preserves draggability", function () {
diff --git a/loleaflet/spec/suites/layer/tile/GridLayerSpec.js 
b/loleaflet/spec/suites/layer/tile/GridLayerSpec.js
index 1ca4227..4181f7e 100644
--- a/loleaflet/spec/suites/layer/tile/GridLayerSpec.js
+++ b/loleaflet/spec/suites/layer/tile/GridLayerSpec.js
@@ -32,80 +32,6 @@ describe('GridLayer', function () {
                });
        });
 
-       it('positions tiles correctly with wrapping and bounding', function () {
-
-               map.setView([0, 0], 1);
-
-               var tiles = [];
-
-               var grid = L.gridLayer();
-               grid.createTile = function (coords) {
-                       var tile = document.createElement('div');
-                       tiles.push({coords: coords, tile: tile});
-                       return tile;
-               };
-
-               map.addLayer(grid);
-
-               var loaded = {};
-
-               for (var i = 0; i < tiles.length; i++) {
-                       var coords = tiles[i].coords,
-                               pos = L.DomUtil.getPosition(tiles[i].tile);
-
-                       loaded[pos.x + ':' + pos.y] = [coords.x, coords.y];
-               }
-
-               expect(loaded).to.eql({
-                       '144:44': [0, 0],
-                       '400:44': [1, 0],
-                       '144:300': [0, 1],
-                       '400:300': [1, 1],
-                       '-112:44': [1, 0],
-                       '656:44': [0, 0],
-                       '-112:300': [1, 1],
-                       '656:300': [0, 1]
-               });
-       });
-
-       describe('tile pyramid', function () {
-               var clock;
-
-               beforeEach(function () {
-                       clock = sinon.useFakeTimers();
-               });
-
-               afterEach(function () {
-                       clock.restore();
-               });
-
-               it('removes tiles for unused zoom levels', function (done) {
-                       map.remove();
-                       map = L.map(div, {fadeAnimation: false});
-                       map.setView([0, 0], 1);
-
-                       var grid = L.gridLayer();
-                       var tiles = {};
-
-                       grid.createTile = function (coords) {
-                               tiles[grid._tileCoordsToKey(coords)] = true;
-                               return document.createElement('div');
-                       };
-
-                       grid.on('tileunload', function (e) {
-                               delete tiles[grid._tileCoordsToKey(e.coords)];
-                               if (Object.keys(tiles).length === 1) {
-                                       
expect(Object.keys(tiles)).to.eql(['0:0:0']);
-                                       done();
-                               }
-                       });
-
-                       map.addLayer(grid);
-                       map.setZoom(0, {animate: false});
-                       clock.tick(250);
-               });
-       });
-
        describe("#onAdd", function () {
                it('is called after viewreset on first map load', function () {
                        var layer = L.gridLayer().addTo(map);
diff --git a/loleaflet/spec/suites/layer/vector/CircleSpec.js 
b/loleaflet/spec/suites/layer/vector/CircleSpec.js
index 0b45bc8..d48a2c2 100644
--- a/loleaflet/spec/suites/layer/vector/CircleSpec.js
+++ b/loleaflet/spec/suites/layer/vector/CircleSpec.js
@@ -4,7 +4,7 @@ describe('Circle', function () {
                var map, circle;
 
                beforeEach(function () {
-                       map = L.map(document.createElement('div')).setView([0, 
0], 4);
+                       map = L.map(document.createElement('div'), {crs: 
L.CRS.EPSG3857}).setView([0, 0], 4);
                        circle = L.circle([50, 30], 200).addTo(map);
                });
 
diff --git a/loleaflet/spec/suites/map/MapSpec.js 
b/loleaflet/spec/suites/map/MapSpec.js
index e7261ce..2125eae 100644
--- a/loleaflet/spec/suites/map/MapSpec.js
+++ b/loleaflet/spec/suites/map/MapSpec.js
@@ -2,7 +2,7 @@ describe("Map", function () {
        var map,
                spy;
        beforeEach(function () {
-               map = L.map(document.createElement('div'));
+               map = L.map(document.createElement('div'), {crs: 
L.CRS.EPSG3857});
        });
 
        describe("#remove", function () {
commit 8a99c8877acda14e7848a8d5724726b8549820dc
Author: Mihai Varga <[email protected]>
Date:   Wed Aug 19 18:27:33 2015 +0300

    loleaflet: fixed some things reported by failing unit tests

diff --git a/loleaflet/src/layer/Layer.js b/loleaflet/src/layer/Layer.js
index 9d6fb1c..0af279f 100644
--- a/loleaflet/src/layer/Layer.js
+++ b/loleaflet/src/layer/Layer.js
@@ -59,7 +59,8 @@ L.Layer = L.Evented.extend({
        },
 
        _initDocument: function () {},
-       _onMessage: function () {}
+       _onMessage: function () {},
+       sendMessage: function () {}
 });
 
 
diff --git a/loleaflet/src/layer/tile/GridLayer.js 
b/loleaflet/src/layer/tile/GridLayer.js
index 5d5cbfd..9ba1301 100644
--- a/loleaflet/src/layer/tile/GridLayer.js
+++ b/loleaflet/src/layer/tile/GridLayer.js
@@ -40,8 +40,10 @@ L.GridLayer = L.Layer.extend({
                this._viewReset();
                this._map._docLayer = this;
 
-               this._map.socket.onopen = L.bind(this._initDocument, this);
-               this._map.socket.onmessage = L.bind(this._onMessage, this);
+               if (this._map.socket) {
+                       this._map.socket.onopen = L.bind(this._initDocument, 
this);
+                       this._map.socket.onmessage = L.bind(this._onMessage, 
this);
+               }
                if (this._map.socket && this._map.socket.readyState === 1) {
                        // the connection is already open
                        this._initDocument();
@@ -65,18 +67,24 @@ L.GridLayer = L.Layer.extend({
                this._tileZoom = null;
                clearTimeout(this._preFetchIdle);
                clearInterval(this._tilesPreFetcher);
-               this._map.socket.onmessage = function () {};
-               this._map.socket.onclose = function () {};
-               this._map.socket.onerror = function () {};
-               this._map.socket.close();
+               if (this._map.socket) {
+                       this._map.socket.onmessage = function () {};
+                       this._map.socket.onclose = function () {};
+                       this._map.socket.onerror = function () {};
+                       this._map.socket.close();
+               }
                if (this._cursorMarker) {
                        this._cursorMarker.remove();
                }
                if (this._graphicMarker) {
                        this._graphicMarker.remove();
                }
-               this._startMarker.remove();
-               this._endMarker.remove();
+               if (this._startMarker) {
+                       this._startMarker.remove();
+               }
+               if (this._endMarker) {
+                       this._endMarker.remove();
+               }
        },
 
        bringToFront: function () {
diff --git a/loleaflet/src/map/Map.js b/loleaflet/src/map/Map.js
index 5799ca7..c3f091a 100644
--- a/loleaflet/src/map/Map.js
+++ b/loleaflet/src/map/Map.js
@@ -249,8 +249,12 @@ L.Map = L.Evented.extend({
                        this.fire('unload');
                }
 
-               this.removeLayer(this._docLayer._selections);
-               this.removeLayer(this._docLayer);
+               if (this._docLayer) {
+                       if (this._docLayer._selections) {
+                               this.removeLayer(this._docLayer._selections);
+                       }
+                       this.removeLayer(this._docLayer);
+               }
                this.removeControls();
                return this;
        },
commit e46766de1904f51b356aba1cb4e3258b5bb5d180
Author: Mihai Varga <[email protected]>
Date:   Wed Aug 19 10:57:42 2015 +0300

    loleaflet: styles control

diff --git a/loleaflet/build/deps.js b/loleaflet/build/deps.js
index 98b5ee6..5488e50 100644
--- a/loleaflet/build/deps.js
+++ b/loleaflet/build/deps.js
@@ -289,6 +289,13 @@ var deps = {
                desc: 'Handles vex dialogs for displaying alerts'
        },
 
+       ControlStyles: {
+               src: ['control/Control.js',
+                     'control/Control.Styles.js'],
+               heading: 'Controls',
+               desc: 'Handles styles selection'
+       },
+
        ControlAttrib: {
                src: ['control/Control.js',
                      'control/Control.Attribution.js'],
diff --git a/loleaflet/debug/document/document_simple_example.html 
b/loleaflet/debug/document/document_simple_example.html
index 9d31f45..eb55c1e 100644
--- a/loleaflet/debug/document/document_simple_example.html
+++ b/loleaflet/debug/document/document_simple_example.html
@@ -67,6 +67,7 @@
             });
 
     ////// Controls /////
+    globalMap.addControl(L.control.styles());
     globalMap.addControl(L.control.buttons());
     globalMap.addControl(L.control.zoom());
     globalMap.addControl(L.control.parts());
diff --git a/loleaflet/dist/leaflet.css b/loleaflet/dist/leaflet.css
index c7c43b7..7c09fb6 100644
--- a/loleaflet/dist/leaflet.css
+++ b/loleaflet/dist/leaflet.css
@@ -762,6 +762,11 @@ input.leaflet-control-search-bar {
     padding: 0.3em;
 }
 
+.leaflet-control-styles {
+       padding: 0.2em 0.4em 0.4em 0em;
+       background-color: white;
+}
+
 .leaflet-control-zoom.leaflet-bar {
     margin-left: 0.5em;
 }
diff --git a/loleaflet/src/control/Control.Styles.js 
b/loleaflet/src/control/Control.Styles.js
new file mode 100644
index 0000000..7bedc43
--- /dev/null
+++ b/loleaflet/src/control/Control.Styles.js
@@ -0,0 +1,61 @@
+/*
+ * L.Control.Styles is used to display a dropdown list of styles
+ */
+
+L.Control.Styles = L.Control.extend({
+       options: {
+               info: '- Please select a style -'
+       },
+
+       onAdd: function (map) {
+               var stylesName = 'leaflet-control-styles';
+               this._container = L.DomUtil.create('select', stylesName + ' 
leaflet-bar'),
+
+               map.on('updatepermission', this._onUpdatePermission, this);
+               map.on('updatestyles', this._initList, this);
+               L.DomEvent.on(this._container, 'change', this._onChange, this);
+
+               return this._container;
+       },
+
+       onRemove: function (map) {
+               map.off('updatepermission', this._searchResultFound, this);
+       },
+
+       _initList: function (e) {
+               var container = this._container;
+               var first = L.DomUtil.create('option', '', container);
+               first.innerHTML = this.options.info;
+               if (this._map._docLayer._docType === 'text') {
+                       var styles = e.styles.ParagraphStyles.slice(0, 12);
+                       styles.forEach(function (style) {
+                               var item = L.DomUtil.create('option', '', 
container);
+                               item.value = style;
+                               item.innerHTML = style;
+                       });
+               }
+       },
+
+       _onUpdatePermission: function (e) {
+               if (e.perm === 'edit') {
+                       this._container.disabled = false;
+               }
+               else {
+                       this._container.disabled = true;
+               }
+       },
+
+       _onChange: function (e) {
+               var style = e.target.value;
+               if (style === this.options.info) {
+                       return;
+               }
+               if (this._map._docLayer._docType === 'text') {
+                       this._map.setStyle(style, 'ParagraphStyles');
+               }
+       }
+});
+
+L.control.styles = function (options) {
+       return new L.Control.Styles(options);
+};
commit 438e4008591313c2be7ce2263e9091bf0d5e90e5
Author: Mihai Varga <[email protected]>
Date:   Tue Aug 18 21:24:58 2015 +0300

    loleaflet: document style setter and getter

diff --git a/loleaflet/README b/loleaflet/README
index 78853f9..995f3ed 100644
--- a/loleaflet/README
+++ b/loleaflet/README
@@ -172,6 +172,15 @@ Writer pages:
             + e.pages = number of pages
             + e.docType = document type, should be 'text'
 
+Styles:
+    - API:
+        map.getStyles()
+            + returns a JSON object as a mapping of style families to a list 
of styles
+        map.setStyle(style, familyName)
+    - events:
+        map.on('updatestyles', function (e) {}) where:
+            e.styles = a JSON object as a mapping of style families to a list 
of styles
+
 Error:
     - events
         map.on('error', function (e) {}) where
diff --git a/loleaflet/build/deps.js b/loleaflet/build/deps.js
index d527e7d..98b5ee6 100644
--- a/loleaflet/build/deps.js
+++ b/loleaflet/build/deps.js
@@ -332,6 +332,11 @@ var deps = {
                desc: 'Scroll handler.'
        },
 
+       Styles: {
+               src: ['control/Styles.js'],
+               desc: 'Document styles handler.'
+       },
+
        AnimationPan: {
                src: [
                        'dom/DomEvent.js',
diff --git a/loleaflet/src/control/Styles.js b/loleaflet/src/control/Styles.js
new file mode 100644
index 0000000..ca233de
--- /dev/null
+++ b/loleaflet/src/control/Styles.js
@@ -0,0 +1,17 @@
+L.Map.include({
+       getStyles: function () {
+               return this._docLayer._docStyles;
+       },
+
+       setStyle: function (style, familyName) {
+               if (!style || !familyName) {
+                       this.fire('error', {cmd: 'setStyle', kind: 
'incorrectparam'});
+                       return;
+               }
+               var msg = 'uno .uno:StyleApply {' +
+                               '"Style":{"type":"string", "value": "' + style 
+ '"},' +
+                               '"FamilyName":{"type":"string", "value":"' + 
familyName + '"}' +
+                               '}';
+               this._docLayer.sendMessage(msg);
+       }
+});
diff --git a/loleaflet/src/layer/tile/TileLayer.js 
b/loleaflet/src/layer/tile/TileLayer.js
index 8d7191e..010f7b0 100644
--- a/loleaflet/src/layer/tile/TileLayer.js
+++ b/loleaflet/src/layer/tile/TileLayer.js
@@ -105,6 +105,7 @@ L.TileLayer = L.GridLayer.extend({
                        }
                        this.sendMessage(msg);
                        this.sendMessage('status');
+                       this.sendMessage('styles');
                }
                this._map.on('drag resize zoomend', this._updateScrollOffset, 
this);
                this._map.on('clearselection', this._clearSelections, this);
@@ -509,6 +510,10 @@ L.TileLayer = L.GridLayer.extend({
                        var originalPhrase = textMsg.substring(16);
                        this._map.fire('search', {originalPhrase: 
originalPhrase, count: 0});
                }
+               else if (textMsg.startsWith('styles:')) {
+                       this._docStyles = JSON.parse(textMsg.substring(8));
+                       this._map.fire('updatestyles', {styles: 
this._docStyles});
+               }
                else if (textMsg.startsWith('error:')) {
                        command = this._parseServerCmd(textMsg);
                        this._map.fire('error', {cmd: command.errorCmd, kind: 
command.errorKind});
commit 1cfd1352ce70c546f2b11e32eadf8a408b49382d
Author: Mihai Varga <[email protected]>
Date:   Tue Aug 18 21:01:05 2015 +0300

    loolwsd: renamed getStatus/saveStatus to getTextFile/saveTextFile
    
    And added a getStyles method that uses the above methods to cache
    document styles

diff --git a/loolwsd/LOOLSession.cpp b/loolwsd/LOOLSession.cpp
index 1202639..b48af37 100644
--- a/loolwsd/LOOLSession.cpp
+++ b/loolwsd/LOOLSession.cpp
@@ -195,7 +195,11 @@ bool MasterProcessSession::handleInput(const char *buffer, 
int length)
             }
             else if (tokens[0] == "status:")
             {
-                peer->_tileCache->saveStatus(std::string(buffer, length));
+                peer->_tileCache->saveTextFile(std::string(buffer, length), 
"status.txt");
+            }
+            else if (tokens[0] == "styles:")
+            {
+                peer->_tileCache->saveTextFile(std::string(buffer, length), 
"styles.txt");
             }
             else if (tokens[0] == "invalidatetiles:")
             {
@@ -277,6 +281,7 @@ bool MasterProcessSession::handleInput(const char *buffer, 
int length)
              tokens[0] != "setclientpart" &&
              tokens[0] != "setpage" &&
              tokens[0] != "status" &&
+             tokens[0] != "styles" &&
              tokens[0] != "tile" &&
              tokens[0] != "uno")
     {
@@ -301,6 +306,10 @@ bool MasterProcessSession::handleInput(const char *buffer, 
int length)
     {
         return getStatus(buffer, length);
     }
+    else if (tokens[0] == "styles")
+    {
+        return getStyles(buffer, length);
+    }
     else if (tokens[0] == "tile")
     {
         sendTile(buffer, length, tokens);
@@ -394,7 +403,7 @@ bool MasterProcessSession::getStatus(const char *buffer, 
int length)
 {
     std::string status;
 
-    status = _tileCache->getStatus();
+    status = _tileCache->getTextFile("status.txt");
     if (status.size() > 0)
     {
         sendTextFrame(status);
@@ -407,6 +416,23 @@ bool MasterProcessSession::getStatus(const char *buffer, 
int length)
     return true;
 }
 
+bool MasterProcessSession::getStyles(const char *buffer, int length)
+{
+    std::string styles;
+
+    styles = _tileCache->getTextFile("styles.txt");
+    if (styles.size() > 0)
+    {
+        sendTextFrame(styles);
+        return true;
+    }
+
+    if (_peer.expired())
+        dispatchChild();
+    forwardToPeer(buffer, length);
+    return true;
+}
+
 void MasterProcessSession::sendTile(const char *buffer, int length, 
StringTokenizer& tokens)
 {
     int part, width, height, tilePosX, tilePosY, tileWidth, tileHeight;
@@ -608,6 +634,10 @@ bool ChildProcessSession::handleInput(const char *buffer, 
int length)
     {
         return getStatus(buffer, length);
     }
+    else if (tokens[0] == "styles")
+    {
+        return getStyles(buffer, length);
+    }
     else if (tokens[0] == "tile")
     {
         sendTile(buffer, length, tokens);
@@ -819,6 +849,12 @@ bool ChildProcessSession::getStatus(const char *buffer, 
int length)
     return true;
 }
 
+bool ChildProcessSession::getStyles(const char *buffer, int length)
+{
+    sendTextFrame("styles: " + 
std::string(_loKitDocument->pClass->getStyles(_loKitDocument)));
+    return true;
+}
+
 void ChildProcessSession::sendTile(const char *buffer, int length, 
StringTokenizer& tokens)
 {
     int part, width, height, tilePosX, tilePosY, tileWidth, tileHeight;
diff --git a/loolwsd/LOOLSession.hpp b/loolwsd/LOOLSession.hpp
index 0030f28..a63e40e 100644
--- a/loolwsd/LOOLSession.hpp
+++ b/loolwsd/LOOLSession.hpp
@@ -45,6 +45,8 @@ public:
 
     virtual bool getStatus(const char *buffer, int length) = 0;
 
+    virtual bool getStyles(const char *buffer, int length) = 0;
+
     virtual bool handleInput(const char *buffer, int length) = 0;
 
 protected:
@@ -109,6 +111,8 @@ public:
 
     virtual bool getStatus(const char *buffer, int length);
 
+    virtual bool getStyles(const char *buffer, int length);
+
  protected:
     bool invalidateTiles(const char *buffer, int length, 
Poco::StringTokenizer& tokens);
 
@@ -154,6 +158,8 @@ public:
 
     virtual bool getStatus(const char *buffer, int length);
 
+    virtual bool getStyles(const char *buffer, int length);
+
     LibreOfficeKitDocument *_loKitDocument;
     std::string _docType;
 
diff --git a/loolwsd/TileCache.cpp b/loolwsd/TileCache.cpp
index 01714a4..76ca730 100644
--- a/loolwsd/TileCache.cpp
+++ b/loolwsd/TileCache.cpp
@@ -106,37 +106,37 @@ void TileCache::saveTile(int part, int width, int height, 
int tilePosX, int tile
     outStream.close();
 }
 
-std::string TileCache::getStatus()
+std::string TileCache::getTextFile(std::string fileName)
 {
-    const char statusFile[] = "/status.txt";
+    const char *textFile = std::string("/" + fileName).c_str();
 
     std::string dirName = cacheDirName(false);
     if (_hasUnsavedChanges)
     {
-        // try the Editing cache first, and prefer its status.txt if it exists
+        // try the Editing cache first, and prefer it if it exists
         std::string editingDirName = cacheDirName(true);
         File dir(editingDirName);
 
-        File status(editingDirName + statusFile);
-        if (dir.exists() && dir.isDirectory() && status.exists() && 
!status.isDirectory())
+        File text(editingDirName + textFile);
+        if (dir.exists() && dir.isDirectory() && text.exists() && 
!text.isDirectory())
             dirName = editingDirName;
     }
 
     if (!File(dirName).exists() || !File(dirName).isDirectory())
         return "";
 
-    std::string fileName = dirName + statusFile;
-    std::fstream statusStream(fileName, std::ios::in);
-    if (!statusStream.is_open())
+    fileName = dirName + textFile;
+    std::fstream textStream(fileName, std::ios::in);
+    if (!textStream.is_open())
         return "";
 
     std::vector<char> result;
-    statusStream.seekg(0, std::ios_base::end);
-    std::streamsize size = statusStream.tellg();
+    textStream.seekg(0, std::ios_base::end);
+    std::streamsize size = textStream.tellg();
     result.resize(size);
-    statusStream.seekg(0, std::ios_base::beg);
-    statusStream.read(result.data(), size);
-    statusStream.close();
+    textStream.seekg(0, std::ios_base::beg);
+    textStream.read(result.data(), size);
+    textStream.close();
 
     if (result[result.size()-1] == '\n')
         result.resize(result.size() - 1);
@@ -170,24 +170,22 @@ void TileCache::setEditing(bool editing)
     _isEditing = editing;
 }
 
-void TileCache::saveStatus(const std::string& status)
+void TileCache::saveTextFile(const std::string& text, std::string fileName)
 {
     std::string dirName = cacheDirName(_hasUnsavedChanges);
 
     File(dirName).createDirectories();
 
-    StringTokenizer tokens(status, " ", StringTokenizer::TOK_IGNORE_EMPTY | 
StringTokenizer::TOK_TRIM);
+    StringTokenizer tokens(text, " ", StringTokenizer::TOK_IGNORE_EMPTY | 
StringTokenizer::TOK_TRIM);
 
-    assert(tokens[0] == "status:");
+    fileName = dirName + "/" + fileName;
+    std::fstream textStream(fileName, std::ios::out);
 
-    std::string fileName = dirName + "/status.txt";
-    std::fstream statusStream(fileName, std::ios::out);
-
-    if (!statusStream.is_open())
+    if (!textStream.is_open())
         return;
 
-    statusStream << status << std::endl;
-    statusStream.close();
+    textStream << text << std::endl;
+    textStream.close();
 }
 
 void TileCache::invalidateTiles(int part, int x, int y, int width, int height)
diff --git a/loolwsd/TileCache.hpp b/loolwsd/TileCache.hpp
index 1e01078..8294784 100644
--- a/loolwsd/TileCache.hpp
+++ b/loolwsd/TileCache.hpp
@@ -40,7 +40,7 @@ public:
 
     std::unique_ptr<std::fstream> lookupTile(int part, int width, int height, 
int tilePosX, int tilePosY, int tileWidth, int tileHeight);
     void saveTile(int part, int width, int height, int tilePosX, int tilePosY, 
int tileWidth, int tileHeight, const char *data, size_t size);
-    std::string getStatus();
+    std::string getTextFile(std::string fileName);
 
     /// Notify the cache that the document was saved - to copy tiles from the 
Editing cache to Persistent.
     void documentSaved();
@@ -48,8 +48,8 @@ public:
     /// Notify whether we need to use the Editing cache.
     void setEditing(bool editing = true);
 
-    // The parameter is a status: message
-    void saveStatus(const std::string& status);
+    // The parameter is a message
+    void saveTextFile(const std::string& text, std::string fileName);
 
     // The tiles parameter is an invalidatetiles: message as sent by the child 
process
     void invalidateTiles(const std::string& tiles);
diff --git a/loolwsd/protocol.txt b/loolwsd/protocol.txt
index 734d317..369bb69 100644
--- a/loolwsd/protocol.txt
+++ b/loolwsd/protocol.txt
@@ -65,6 +65,8 @@ selectgraphic type=<type> x=<x> y=<y>
 
 status
 
+styles
+
 tile part=<partNumber> width=<width> height=<height> tileposx=<xpos> 
tileposy=<ypos> tilewidth=<tileWidth> tileheight=<tileHeight>
 
     All parameters are numbers.
@@ -113,6 +115,8 @@ status: type=<typeName> parts=<numberOfParts> 
current=<currentPartNumber> width=
     <typeName> is 'text, 'spreadsheet', 'presentation', 'drawing' or 'other. 
Others are numbers.
     if the document has multiple parts and those have names, part names follow 
separated by '\n'
 
+styles: {"styleFamily": ["styles in family"], etc. }
+
 textselectioncontent: <content>
 
     Current selection's content
commit b5688840adb7eb72340e28d0e40d0e463b0780c4
Author: Mihai Varga <[email protected]>
Date:   Tue Aug 18 14:34:10 2015 +0300

    loleaflet: mention how to run the tests

diff --git a/loleaflet/README b/loleaflet/README
index 0ce0f70..78853f9 100644
--- a/loleaflet/README
+++ b/loleaflet/README
@@ -179,6 +179,16 @@ Error:
             + [e.cmd] = the command that caused the error
             + [e.kind] = the kind of error
 
+Testing
+-------
+    - to simulate an editing session and to get the tile loading times
+        + open spec/tilebench.html in the browser
+    - to simulate a client opening several documents in the browser
+        + open spec/loadtest.html in the browser
+    - to simulate a client opening several documents in the console
+        + run: mocha headlessLoadTest.js
+
+
 Contributing
 ------------
 
commit 7e7e458b1ef8107f211cbdd76c9df453eeca1497
Author: Mihai Varga <[email protected]>
Date:   Tue Aug 18 14:27:17 2015 +0300

    loleaflet: headless load test

diff --git a/loleaflet/package.json b/loleaflet/package.json
index b6e4b12..9c423c4 100644
--- a/loleaflet/package.json
+++ b/loleaflet/package.json
@@ -38,5 +38,8 @@
       "type": "BSD-2-Clause",
       "url": "https://github.com/Leaflet/Leaflet/blob/master/LICENSE";
     }
-  ]
+  ],
+  "dependencies": {
+    "ws": "~0.7.2"
+  }
 }
diff --git a/loleaflet/spec/headlessLoadTest.js 
b/loleaflet/spec/headlessLoadTest.js
new file mode 100644
index 0000000..fa88982
--- /dev/null
+++ b/loleaflet/spec/headlessLoadTest.js
@@ -0,0 +1,179 @@
+var WebSocket = require('ws');
+var events = require('events');
+
+if (typeof String.prototype.startsWith != 'function') {
+       String.prototype.startsWith = function (str){
+               return this.indexOf(str) === 0;
+       };
+}
+
+describe('LoadTest', function () {
+       // 10s timeout
+       this.timeout(10000);
+       // set the slow time to 5ms knowing each test takes more than that,
+       // so the run time is always printed
+       this.slow(5);
+       var testsRan = 0,
+               tileSize = 256,
+               tileSizeTwips = 3000,
+               host = 'ws://localhost:9980';
+
+       var docPath = 'file:///PATH';
+       var docs = ['eval.odt', 'lorem.odt'];
+
+       var _parseServerCmd = function (msg) {
+               var tokens = msg.split(/[ \n]+/);
+               var command = {};
+               for (var i = 0; i < tokens.length; i++) {
+                       if (tokens[i].substring(0, 9) === 'tileposx=') {
+                               command.x = parseInt(tokens[i].substring(9));
+                       }
+                       else if (tokens[i].substring(0, 9) === 'tileposy=') {
+                               command.y = parseInt(tokens[i].substring(9));
+                       }
+                       else if (tokens[i].substring(0, 10) === 'tilewidth=') {
+                               command.tileWidth = 
parseInt(tokens[i].substring(10));
+                       }
+                       else if (tokens[i].substring(0, 11) === 'tileheight=') {
+                               command.tileHeight = 
parseInt(tokens[i].substring(11));
+                       }
+                       else if (tokens[i].substring(0, 6) === 'width=') {
+                               command.width = 
parseInt(tokens[i].substring(6));
+                       }
+                       else if (tokens[i].substring(0, 7) === 'height=') {
+                               command.height = 
parseInt(tokens[i].substring(7));
+                       }
+                       else if (tokens[i].substring(0, 5) === 'part=') {
+                               command.part = parseInt(tokens[i].substring(5));
+                       }
+                       else if (tokens[i].substring(0, 6) === 'parts=') {
+                               command.parts = 
parseInt(tokens[i].substring(6));
+                       }
+                       else if (tokens[i].substring(0, 8) === 'current=') {
+                               command.currentPart = 
parseInt(tokens[i].substring(8));
+                       }
+               }
+               return command;
+       };
+
+       before(function () {
+               if (docPath === 'file:///PATH') {
+                       throw new Error('Document file path not set');
+               }
+               else if (docPath[docPath.length - 1] !== '/') {
+                       docPath += '/';
+               }
+       });
+
+       docs.forEach(function (testDoc) {
+               testsRan += 1;
+               describe('Document #' + testsRan + ' (' + testDoc + ')', 
function () {
+                       var ws;
+                       var requestedTiles = 0;
+                       var docWidthTwips, docHeightTwips, midY, endY;
+                       var eventEmitter = new events.EventEmitter();
+
+                       var onMessage = function (evt) {
+                               var bytes, index, textMsg;
+
+                               if (typeof (evt.data) === 'string') {
+                                       textMsg = evt.data;
+                               }
+                               else if (typeof (evt.data) === 'object') {
+                                       bytes = new Uint8Array(evt.data);
+                                       index = 0;
+                                       // search for the first newline which 
marks the end of the message
+                                       while (index < bytes.length && 
bytes[index] !== 10) {
+                                               index++;
+                                       }
+                                       textMsg = 
String.fromCharCode.apply(null, bytes.subarray(0, index));
+                               }
+
+                               if (textMsg.startsWith('status:')) {
+                                       command = _parseServerCmd(textMsg);
+                                       docWidthTwips = command.width;
+                                       docHeightTwips = command.height;
+                                       endY = Math.floor(docHeightTwips / 
tileSizeTwips);
+                                       midY = Math.floor(endY / 2);
+                                       eventEmitter.emit('status');
+                               }
+                               else if (textMsg.startsWith('tile:')) {
+                                       requestedTiles -= 1;
+                                       if (requestedTiles <= 0) {
+                                               
eventEmitter.emit('alltilesloaded');
+                                       }
+                               }
+                               else if (textMsg.startsWith('error:')) {
+                                       console.log(textMsg);
+                                       throw new Error(textMsg);
+                               }
+                       };
+
+                       var requestTiles = function (x, y) {
+                               requestedTiles += 1;
+                               ws.send('tile ' +
+                                               'part=0 ' +
+                                               'width=' + tileSize + ' ' +
+                                               'height=' + tileSize + ' ' +
+                                               'tileposx=' + x * tileSizeTwips 
+ ' ' +
+                                               'tileposy=' + y * tileSizeTwips 
+ ' ' +
+                                               'tilewidth=' + tileSizeTwips + 
' ' +
+                                               'tileheight=' + tileSizeTwips);
+                       };
+
+                       var isValidTile = function (x, y) {
+                               return x >= 0 && y >= 0 && (x * tileSizeTwips < 
docWidthTwips) && (y * tileSizeTwips < docHeightTwips);
+                       };
+
+                       after(function () {
+                               ws.onmessage = function () {};
+                               ws.close();
+                       });
+
+                       it('Connect to the server', function (done) {
+                               eventEmitter.once('status', done);
+                               ws = new WebSocket(host);
+                               ws.onmessage = onMessage;
+                               ws.onerror = function (e) {console.log(e)};
+                               ws.binaryType = 'arraybuffer';
+                               ws.onopen = function () {
+                                       ws.send('load url=' + docPath + 
testDoc);
+                                       ws.send('status');
+                               };
+                       });
+
+                       it('Load the document', function (done) {
+                               eventEmitter.once('alltilesloaded', done);
+                               for (var i = 0; i < 3; i++) {
+                                       for (j = 0; j < 5; j++) {
+                                               if (isValidTile(j, i)) {
+                                                       requestTiles(j, i);
+                                               }
+                                       }
+                               }
+                       });
+
+                       it('Scroll to the middle', function (done) {
+                               eventEmitter.once('alltilesloaded', done);
+                               for (var i = midY; i < midY + 3; i++) {
+                                       for (j = 0; j < 5; j++) {
+                                               if (isValidTile(j, i)) {
+                                                       requestTiles(j, i);
+                                               }
+                                       }
+                               }
+                       });
+
+                       it('Scroll to the end', function (done) {
+                               eventEmitter.once('alltilesloaded', done);
+                               for (var i = endY; i > endY - 3; i--) {
+                                       for (j = 0; j < 5; j++) {
+                                               if (isValidTile(j, i)) {
+                                                       requestTiles(j, i);
+                                               }
+                                       }
+                               }
+                       });
+               });
+       });
+});
commit 6dc45b5825316ba9aeb1389acd9580fab7b4ad66
Author: Mihai Varga <[email protected]>
Date:   Tue Aug 18 13:52:00 2015 +0300

    loleaflet: load test check if doc path is set

diff --git a/loleaflet/spec/loadtest/LoadTestSpec.js 
b/loleaflet/spec/loadtest/LoadTestSpec.js
index e04350a..c339926 100644
--- a/loleaflet/spec/loadtest/LoadTestSpec.js
+++ b/loleaflet/spec/loadtest/LoadTestSpec.js
@@ -1,6 +1,9 @@
 describe('LoadTest', function () {
        // 25 s timeout
        this.timeout(25000);
+       // set the slow time to 5ms knowing each test takes more than that,
+       // so the run time is always printed
+       this.slow(5);
        var testsRan = 0,
                checkTimeOut = null,
                map = null,
@@ -9,6 +12,10 @@ describe('LoadTest', function () {
                y = 0;
 
        before(function() {
+               if (docPath === 'file:///PATH') {
+                       throw new Error('Document file path not set');
+               }
+
                map = L.map('map-test', {
                        center: [0, 0],
                        zoom: 10,
_______________________________________________
Libreoffice-commits mailing list
[email protected]
http://lists.freedesktop.org/mailman/listinfo/libreoffice-commits

Reply via email to