mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Moving client and common tests to core
Summary: - Moved /test/client and /test/common to core. - Moved two files (CircularArray and RecentItems) from app/common to core/app/common. - Moved resetOrg test to gen-server. - `testrun.sh` is now invoking common and client test from core. - Added missing packages to core's package.json (and revealed underscore as it is used in the main app). - Removed Coord.js as it is not used anywhere. Test Plan: Existing tests Reviewers: paulfitz Reviewed By: paulfitz Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D3590
This commit is contained in:
170
test/client/components/Layout.js
Normal file
170
test/client/components/Layout.js
Normal file
@@ -0,0 +1,170 @@
|
||||
/* global describe, beforeEach, afterEach, it */
|
||||
|
||||
var assert = require('chai').assert;
|
||||
var clientUtil = require('../clientUtil');
|
||||
var dom = require('app/client/lib/dom');
|
||||
var Layout = require('app/client/components/Layout');
|
||||
|
||||
describe('Layout', function() {
|
||||
|
||||
clientUtil.setTmpMochaGlobals();
|
||||
|
||||
var layout;
|
||||
|
||||
var sampleData = {
|
||||
children: [{
|
||||
children: [{
|
||||
children: [{
|
||||
leaf: 1
|
||||
}, {
|
||||
leaf: 2
|
||||
}]
|
||||
}, {
|
||||
children: [{
|
||||
children: [{
|
||||
leaf: 3
|
||||
}, {
|
||||
leaf: 4
|
||||
}]
|
||||
}, {
|
||||
leaf: 5
|
||||
}]
|
||||
}]
|
||||
}, {
|
||||
leaf: 6
|
||||
}]
|
||||
};
|
||||
|
||||
function createLeaf(leafId) {
|
||||
return dom('div.layout_leaf_test', "#" + leafId);
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
layout = Layout.Layout.create(sampleData, createLeaf);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
layout.dispose();
|
||||
layout = null;
|
||||
});
|
||||
|
||||
function getClasses(node) {
|
||||
return Array.prototype.slice.call(node.classList, 0).sort();
|
||||
}
|
||||
|
||||
it("should generate same layout spec as it was built with", function() {
|
||||
assert.deepEqual(layout.getLayoutSpec(), sampleData);
|
||||
assert.deepEqual(layout.getAllLeafIds().sort(), [1, 2, 3, 4, 5, 6]);
|
||||
});
|
||||
|
||||
it("should generate nested DOM structure", function() {
|
||||
var rootBox = layout.rootElem.querySelector('.layout_box');
|
||||
assert(rootBox);
|
||||
assert.strictEqual(rootBox, layout.rootBox().dom);
|
||||
assert.deepEqual(getClasses(rootBox), ["layout_box", "layout_last_child",
|
||||
"layout_vbox"]);
|
||||
|
||||
var rows = rootBox.children;
|
||||
assert.equal(rows.length, 2);
|
||||
assert.equal(rows[0].children.length, 2);
|
||||
assert.deepEqual(getClasses(rows[0]), ["layout_box", "layout_hbox"]);
|
||||
assert.deepEqual(getClasses(rows[0].children[0]), ["layout_box", "layout_vbox"]);
|
||||
assert.deepEqual(getClasses(rows[0].children[1]), ["layout_box", "layout_last_child",
|
||||
"layout_vbox"]);
|
||||
assert.equal(rows[1].children.length, 1);
|
||||
assert.includeMembers(getClasses(rows[1]), ["layout_box", "layout_hbox",
|
||||
"layout_last_child", "layout_leaf"]);
|
||||
});
|
||||
|
||||
it("should correctly handle removing boxes", function() {
|
||||
layout.getLeafBox(4).removeFromParent();
|
||||
layout.getLeafBox(1).removeFromParent();
|
||||
assert.deepEqual(layout.getAllLeafIds().sort(), [2, 3, 5, 6]);
|
||||
|
||||
assert.deepEqual(layout.getLayoutSpec(), {
|
||||
children: [{
|
||||
children: [{
|
||||
leaf: 2
|
||||
}, {
|
||||
children: [{
|
||||
leaf: 3
|
||||
}, {
|
||||
leaf: 5
|
||||
}]
|
||||
}]
|
||||
}, {
|
||||
leaf: 6
|
||||
}]
|
||||
});
|
||||
|
||||
// Here we get into a rare situation with a single child (to allow root box to be split
|
||||
// vertically).
|
||||
layout.getLeafBox(6).removeFromParent();
|
||||
assert.deepEqual(layout.getLayoutSpec(), {
|
||||
children: [{
|
||||
children: [{
|
||||
leaf: 2
|
||||
}, {
|
||||
children: [{
|
||||
leaf: 3
|
||||
}, {
|
||||
leaf: 5
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
});
|
||||
assert.deepEqual(layout.getAllLeafIds().sort(), [2, 3, 5]);
|
||||
|
||||
// Here the special single-child box should collapse
|
||||
layout.getLeafBox(2).removeFromParent();
|
||||
assert.deepEqual(layout.getLayoutSpec(), {
|
||||
children: [{
|
||||
leaf: 3
|
||||
}, {
|
||||
leaf: 5
|
||||
}]
|
||||
});
|
||||
|
||||
layout.getLeafBox(3).removeFromParent();
|
||||
assert.deepEqual(layout.getLayoutSpec(), {
|
||||
leaf: 5
|
||||
});
|
||||
assert.deepEqual(layout.getAllLeafIds().sort(), [5]);
|
||||
});
|
||||
|
||||
it("should correctly handle adding child and sibling boxes", function() {
|
||||
// In this test, we'll build up the sample layout from scratch, trying to exercise all code
|
||||
// paths.
|
||||
layout = Layout.Layout.create({ leaf: 1 }, createLeaf);
|
||||
assert.deepEqual(layout.getLayoutSpec(), { leaf: 1 });
|
||||
assert.deepEqual(layout.getAllLeafIds().sort(), [1]);
|
||||
|
||||
function makeBox(leafId) {
|
||||
return layout.buildLayoutBox({leaf: leafId});
|
||||
}
|
||||
|
||||
assert.strictEqual(layout.rootBox(), layout.getLeafBox(1));
|
||||
layout.getLeafBox(1).addSibling(makeBox(5), true);
|
||||
assert.deepEqual(layout.getLayoutSpec(), {children: [{
|
||||
children: [{ leaf: 1 }, { leaf: 5 }]
|
||||
}]});
|
||||
assert.notStrictEqual(layout.rootBox(), layout.getLeafBox(1));
|
||||
|
||||
// An extra little check to add a sibling to a vertically-split root (in which case the split
|
||||
// is really a level lower, and that's where the sibling should be added).
|
||||
layout.rootBox().addSibling(makeBox("foo"), true);
|
||||
assert.deepEqual(layout.getLayoutSpec(), {children: [{
|
||||
children: [{ leaf: 1 }, { leaf: 5 }, { leaf: "foo" }]
|
||||
}]});
|
||||
assert.deepEqual(layout.getAllLeafIds().sort(), [1, 5, "foo"]);
|
||||
layout.getLeafBox("foo").dispose();
|
||||
assert.deepEqual(layout.getAllLeafIds().sort(), [1, 5]);
|
||||
|
||||
layout.getLeafBox(1).parentBox().addSibling(makeBox(6), true);
|
||||
layout.getLeafBox(5).addChild(makeBox(3), false);
|
||||
layout.getLeafBox(3).addChild(makeBox(4), true);
|
||||
layout.getLeafBox(1).addChild(makeBox(2), true);
|
||||
assert.deepEqual(layout.getLayoutSpec(), sampleData);
|
||||
assert.deepEqual(layout.getAllLeafIds().sort(), [1, 2, 3, 4, 5, 6]);
|
||||
});
|
||||
});
|
||||
63
test/client/components/WidgetFrame.ts
Normal file
63
test/client/components/WidgetFrame.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {assert} from 'chai';
|
||||
import {MethodAccess} from 'app/client/components/WidgetFrame';
|
||||
import {AccessLevel} from 'app/common/CustomWidget';
|
||||
|
||||
describe('WidgetFrame', function () {
|
||||
it('should define access level per method', function () {
|
||||
class SampleApi {
|
||||
public none() {
|
||||
return true;
|
||||
}
|
||||
public read_table() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public full() {}
|
||||
public notMentioned() {}
|
||||
}
|
||||
const checker = new MethodAccess<SampleApi>()
|
||||
.require(AccessLevel.none, 'none')
|
||||
.require(AccessLevel.read_table, 'read_table')
|
||||
.require(AccessLevel.full, 'full');
|
||||
|
||||
const directTest = () => {
|
||||
assert.isTrue(checker.check(AccessLevel.none, 'none'));
|
||||
assert.isFalse(checker.check(AccessLevel.none, 'read_table'));
|
||||
assert.isFalse(checker.check(AccessLevel.none, 'full'));
|
||||
|
||||
assert.isTrue(checker.check(AccessLevel.read_table, 'none'));
|
||||
assert.isTrue(checker.check(AccessLevel.read_table, 'read_table'));
|
||||
assert.isFalse(checker.check(AccessLevel.read_table, 'full'));
|
||||
|
||||
assert.isTrue(checker.check(AccessLevel.full, 'none'));
|
||||
assert.isTrue(checker.check(AccessLevel.full, 'read_table'));
|
||||
assert.isTrue(checker.check(AccessLevel.full, 'full'));
|
||||
};
|
||||
directTest();
|
||||
|
||||
// Check that for any other method, access is denied.
|
||||
assert.isFalse(checker.check(AccessLevel.none, 'notMentioned'));
|
||||
assert.isFalse(checker.check(AccessLevel.read_table, 'notMentioned'));
|
||||
// Even though access is full, the method was not mentioned, so it should be denied.
|
||||
assert.isFalse(checker.check(AccessLevel.full, 'notMentioned'));
|
||||
|
||||
// Now add a default rule.
|
||||
checker.require(AccessLevel.none, '*');
|
||||
assert.isTrue(checker.check(AccessLevel.none, 'notMentioned'));
|
||||
assert.isTrue(checker.check(AccessLevel.read_table, 'notMentioned'));
|
||||
assert.isTrue(checker.check(AccessLevel.full, 'notMentioned'));
|
||||
directTest();
|
||||
|
||||
checker.require(AccessLevel.read_table, '*');
|
||||
assert.isFalse(checker.check(AccessLevel.none, 'notMentioned'));
|
||||
assert.isTrue(checker.check(AccessLevel.read_table, 'notMentioned'));
|
||||
assert.isTrue(checker.check(AccessLevel.full, 'notMentioned'));
|
||||
directTest();
|
||||
|
||||
checker.require(AccessLevel.full, '*');
|
||||
assert.isFalse(checker.check(AccessLevel.none, 'notMentioned'));
|
||||
assert.isFalse(checker.check(AccessLevel.read_table, 'notMentioned'));
|
||||
assert.isTrue(checker.check(AccessLevel.full, 'notMentioned'));
|
||||
directTest();
|
||||
});
|
||||
});
|
||||
218
test/client/components/commands.js
Normal file
218
test/client/components/commands.js
Normal file
@@ -0,0 +1,218 @@
|
||||
/* global describe, beforeEach, before, after, it */
|
||||
|
||||
var _ = require('underscore');
|
||||
var sinon = require('sinon');
|
||||
var assert = require('chai').assert;
|
||||
var ko = require('knockout');
|
||||
var Mousetrap = require('app/client/lib/Mousetrap');
|
||||
var commands = require('app/client/components/commands');
|
||||
var clientUtil = require('../clientUtil');
|
||||
|
||||
describe('commands', function() {
|
||||
|
||||
clientUtil.setTmpMochaGlobals();
|
||||
|
||||
before(function() {
|
||||
sinon.stub(Mousetrap, "bind");
|
||||
sinon.stub(Mousetrap, "unbind");
|
||||
});
|
||||
|
||||
after(function() {
|
||||
Mousetrap.bind.restore();
|
||||
Mousetrap.unbind.restore();
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
commands.init([{
|
||||
group: "Foo",
|
||||
commands: [{
|
||||
name: "cmd1",
|
||||
keys: ["Ctrl+a", "Ctrl+b"],
|
||||
desc: "Command 1"
|
||||
}, {
|
||||
name: "cmd2",
|
||||
keys: ["Ctrl+c"],
|
||||
desc: "Command 2"
|
||||
}, {
|
||||
name: "cmd3",
|
||||
keys: ["Ctrl+a"],
|
||||
desc: "Command 1B"
|
||||
}]
|
||||
}]);
|
||||
});
|
||||
|
||||
describe("activate", function() {
|
||||
it("should invoke Mousetrap.bind/unbind", function() {
|
||||
var obj = {};
|
||||
var spy = sinon.spy();
|
||||
var cmdGroup = commands.createGroup({ cmd1: spy }, obj, true);
|
||||
sinon.assert.callCount(Mousetrap.bind, 2);
|
||||
sinon.assert.calledWith(Mousetrap.bind, "ctrl+a");
|
||||
sinon.assert.calledWith(Mousetrap.bind, "ctrl+b");
|
||||
Mousetrap.bind.reset();
|
||||
Mousetrap.unbind.reset();
|
||||
|
||||
commands.allCommands.cmd1.run();
|
||||
sinon.assert.callCount(spy, 1);
|
||||
sinon.assert.calledOn(spy, obj);
|
||||
|
||||
cmdGroup.activate(false);
|
||||
sinon.assert.callCount(Mousetrap.bind, 0);
|
||||
sinon.assert.callCount(Mousetrap.unbind, 2);
|
||||
sinon.assert.calledWith(Mousetrap.unbind, "ctrl+a");
|
||||
sinon.assert.calledWith(Mousetrap.unbind, "ctrl+b");
|
||||
Mousetrap.bind.reset();
|
||||
Mousetrap.unbind.reset();
|
||||
|
||||
commands.allCommands.cmd1.run();
|
||||
sinon.assert.callCount(spy, 1);
|
||||
|
||||
cmdGroup.activate(true);
|
||||
sinon.assert.callCount(Mousetrap.bind, 2);
|
||||
sinon.assert.calledWith(Mousetrap.bind, "ctrl+a");
|
||||
sinon.assert.calledWith(Mousetrap.bind, "ctrl+b");
|
||||
sinon.assert.callCount(Mousetrap.unbind, 0);
|
||||
|
||||
commands.allCommands.cmd1.run();
|
||||
sinon.assert.callCount(spy, 2);
|
||||
|
||||
cmdGroup.dispose();
|
||||
sinon.assert.callCount(Mousetrap.unbind, 2);
|
||||
sinon.assert.calledWith(Mousetrap.unbind, "ctrl+a");
|
||||
sinon.assert.calledWith(Mousetrap.unbind, "ctrl+b");
|
||||
|
||||
commands.allCommands.cmd1.run();
|
||||
sinon.assert.callCount(spy, 2);
|
||||
});
|
||||
|
||||
/**
|
||||
* For an object of the form { group1: { cmd1: sinon.spy() } }, goes through all spys, and
|
||||
* returns a mapping of call counts: {'group1:cmd1': spyCallCount}.
|
||||
*/
|
||||
function getCallCounts(groups) {
|
||||
var counts = {};
|
||||
_.each(groups, function(group, grpName) {
|
||||
_.each(group, function(cmdSpy, cmdName) {
|
||||
counts[grpName + ":" + cmdName] = cmdSpy.callCount;
|
||||
});
|
||||
});
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diffs two sets of call counts as produced by getCallCounts and returns the difference.
|
||||
*/
|
||||
function diffCallCounts(callCounts1, callCounts2) {
|
||||
return _.chain(callCounts2).mapObject(function(count, name) {
|
||||
return count - callCounts1[name];
|
||||
})
|
||||
.pick(function(count, name) {
|
||||
return count > 0;
|
||||
})
|
||||
.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the given command, and makes sure the difference of call counts before and after is
|
||||
* as expected.
|
||||
*/
|
||||
function assertCallCounts(groups, cmdOrFunc, expectedCounts) {
|
||||
var before = getCallCounts(groups);
|
||||
if (typeof cmdOrFunc === 'string') {
|
||||
commands.allCommands[cmdOrFunc].run();
|
||||
} else if (cmdOrFunc === null) {
|
||||
// nothing
|
||||
} else {
|
||||
cmdOrFunc();
|
||||
}
|
||||
var after = getCallCounts(groups);
|
||||
assert.deepEqual(diffCallCounts(before, after), expectedCounts);
|
||||
}
|
||||
|
||||
it("should respect order of CommandGroups", function() {
|
||||
var groups = {
|
||||
group1: { cmd1: sinon.spy(), cmd3: sinon.spy() },
|
||||
group2: { cmd1: sinon.spy(), cmd2: sinon.spy() },
|
||||
group3: { cmd3: sinon.spy() },
|
||||
};
|
||||
var cmdGroup1 = commands.createGroup(groups.group1, null, true);
|
||||
var cmdGroup2 = commands.createGroup(groups.group2, null, true);
|
||||
var cmdGroup3 = commands.createGroup(groups.group3, null, false);
|
||||
|
||||
assertCallCounts(groups, 'cmd1', {'group2:cmd1': 1});
|
||||
assertCallCounts(groups, 'cmd2', {'group2:cmd2': 1});
|
||||
assertCallCounts(groups, 'cmd3', {'group1:cmd3': 1});
|
||||
|
||||
cmdGroup2.activate(false);
|
||||
assertCallCounts(groups, 'cmd1', {'group1:cmd1': 1});
|
||||
assertCallCounts(groups, 'cmd2', {});
|
||||
assertCallCounts(groups, 'cmd3', {'group1:cmd3': 1});
|
||||
|
||||
cmdGroup3.activate(true);
|
||||
cmdGroup1.activate(false);
|
||||
assertCallCounts(groups, 'cmd1', {});
|
||||
assertCallCounts(groups, 'cmd2', {});
|
||||
assertCallCounts(groups, 'cmd3', {'group3:cmd3': 1});
|
||||
|
||||
cmdGroup2.activate(true);
|
||||
assertCallCounts(groups, 'cmd1', {'group2:cmd1': 1});
|
||||
assertCallCounts(groups, 'cmd2', {'group2:cmd2': 1});
|
||||
assertCallCounts(groups, 'cmd3', {'group3:cmd3': 1});
|
||||
});
|
||||
|
||||
it("should allow use of observable for activation flag", function() {
|
||||
var groups = {
|
||||
groupFoo: { cmd1: sinon.spy() },
|
||||
};
|
||||
var isActive = ko.observable(false);
|
||||
commands.createGroup(groups.groupFoo, null, isActive);
|
||||
assertCallCounts(groups, 'cmd1', {});
|
||||
isActive(true);
|
||||
assertCallCounts(groups, 'cmd1', {'groupFoo:cmd1': 1});
|
||||
// Check that subsequent calls continue working.
|
||||
assertCallCounts(groups, 'cmd1', {'groupFoo:cmd1': 1});
|
||||
isActive(false);
|
||||
assertCallCounts(groups, 'cmd1', {});
|
||||
});
|
||||
|
||||
function getFuncForShortcut(shortcut) {
|
||||
function argsIncludeShortcut(args) {
|
||||
return Array.isArray(args[0]) ? _.contains(args[0], shortcut) : (args[0] === shortcut);
|
||||
}
|
||||
var b = _.findLastIndex(Mousetrap.bind.args, argsIncludeShortcut);
|
||||
var u = _.findLastIndex(Mousetrap.unbind.args, argsIncludeShortcut);
|
||||
if (b < 0) {
|
||||
return null;
|
||||
} else if (u < 0) {
|
||||
return Mousetrap.bind.args[b][1];
|
||||
} else if (Mousetrap.bind.getCall(b).calledBefore(Mousetrap.unbind.getCall(u))) {
|
||||
return null;
|
||||
} else {
|
||||
return Mousetrap.bind.args[b][1];
|
||||
}
|
||||
}
|
||||
|
||||
it("should allow same keys used for different commands", function() {
|
||||
// Both cmd1 and cmd3 use "Ctrl+a" shortcut, so cmd3 should win when group3 is active.
|
||||
Mousetrap.bind.reset();
|
||||
Mousetrap.unbind.reset();
|
||||
var groups = {
|
||||
group1: { cmd1: sinon.spy() },
|
||||
group3: { cmd3: sinon.spy() },
|
||||
};
|
||||
var cmdGroup1 = commands.createGroup(groups.group1, null, true);
|
||||
var cmdGroup3 = commands.createGroup(groups.group3, null, true);
|
||||
assertCallCounts(groups, getFuncForShortcut('ctrl+a'), {'group3:cmd3': 1});
|
||||
assertCallCounts(groups, getFuncForShortcut('ctrl+b'), {'group1:cmd1': 1});
|
||||
cmdGroup3.activate(false);
|
||||
assertCallCounts(groups, getFuncForShortcut('ctrl+a'), {'group1:cmd1': 1});
|
||||
assertCallCounts(groups, getFuncForShortcut('ctrl+b'), {'group1:cmd1': 1});
|
||||
cmdGroup1.activate(false);
|
||||
assertCallCounts(groups, getFuncForShortcut('ctrl+a'), {});
|
||||
assertCallCounts(groups, getFuncForShortcut('ctrl+b'), {});
|
||||
cmdGroup3.activate(true);
|
||||
assertCallCounts(groups, getFuncForShortcut('ctrl+a'), {'group3:cmd3': 1});
|
||||
assertCallCounts(groups, getFuncForShortcut('ctrl+b'), {});
|
||||
});
|
||||
});
|
||||
});
|
||||
80
test/client/components/sampleLayout.js
Normal file
80
test/client/components/sampleLayout.js
Normal file
@@ -0,0 +1,80 @@
|
||||
var dom = require('app/client/lib/dom');
|
||||
var kd = require('app/client/lib/koDom');
|
||||
var kf = require('app/client/lib/koForm');
|
||||
var Layout = require('app/client/components/Layout');
|
||||
var LayoutEditor = require('app/client/components/LayoutEditor');
|
||||
|
||||
function createTestTab() {
|
||||
return kf.topTab('Layout',
|
||||
kf.label("Layout Editor")
|
||||
);
|
||||
}
|
||||
exports.createTestTab = createTestTab;
|
||||
|
||||
var sampleData = {
|
||||
children: [{
|
||||
children: [{
|
||||
children: [{
|
||||
leaf: 1
|
||||
}, {
|
||||
leaf: 2
|
||||
}, {
|
||||
leaf: 7
|
||||
}, {
|
||||
leaf: 8
|
||||
}]
|
||||
}, {
|
||||
children: [{
|
||||
children: [{
|
||||
leaf: 3
|
||||
}, {
|
||||
leaf: 4
|
||||
}, {
|
||||
leaf: 9
|
||||
}, {
|
||||
leaf: 10
|
||||
}]
|
||||
}, {
|
||||
leaf: 5
|
||||
}]
|
||||
}]
|
||||
}, {
|
||||
leaf: 6
|
||||
}]
|
||||
};
|
||||
|
||||
function getMaxLeaf(spec) {
|
||||
var maxChild = spec.children ? Math.max.apply(Math, spec.children.map(getMaxLeaf)) : -Infinity;
|
||||
return Math.max(maxChild, spec.leaf || -Infinity);
|
||||
}
|
||||
|
||||
function createLeaf(leafId) {
|
||||
return dom('div.layout_leaf_test', "#" + leafId,
|
||||
kd.toggleClass('layout_leaf_test_big', leafId % 2 === 0)
|
||||
);
|
||||
}
|
||||
|
||||
function createTestPane() {
|
||||
var layout = Layout.Layout.create(sampleData, createLeaf);
|
||||
var layoutEditor = LayoutEditor.LayoutEditor.create(layout);
|
||||
var maxLeaf = getMaxLeaf(sampleData);
|
||||
return dom('div',
|
||||
dom.autoDispose(layoutEditor),
|
||||
dom.autoDispose(layout),
|
||||
dom('div',
|
||||
dom('div.layout_new.pull-left', '+ Add New',
|
||||
dom.on('mousedown', function(event) {
|
||||
layoutEditor.dragInNewBox(event, ++maxLeaf);
|
||||
return false;
|
||||
})
|
||||
),
|
||||
dom('div.layout_trash.pull-right',
|
||||
dom('span.glyphicon.glyphicon-trash')
|
||||
),
|
||||
dom('div.clearfix')
|
||||
),
|
||||
layout.rootElem
|
||||
);
|
||||
}
|
||||
|
||||
exports.createTestPane = createTestPane;
|
||||
Reference in New Issue
Block a user