Skip to content

Commit eb71a9b

Browse files
authored
Merge pull request #299 from SeleniumHQ/test-dnd
Rearrange test cases inside suites
2 parents 3ff7c79 + 99371a8 commit eb71a9b

File tree

9 files changed

+141
-44
lines changed

9 files changed

+141
-44
lines changed

packages/selenium-ide/src/neo/__test__/models/Suite.spec.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,26 @@ describe("Suite model", () => {
120120
suite.removeTestCase(new TestCase());
121121
expect(suite.tests.length).toBe(1);
122122
});
123+
it("shoul tell if a test exist in the suite", () => {
124+
const suite = new Suite();
125+
const exists = new TestCase();
126+
const nonExistent = new TestCase();
127+
suite.addTestCase(exists);
128+
expect(suite.containsTest(exists)).toBeTruthy();
129+
expect(suite.containsTest(nonExistent)).toBeFalsy();
130+
});
131+
it("should swap the test cases", () => {
132+
const suite = new Suite();
133+
const test1 = new TestCase();
134+
const test2 = new TestCase();
135+
suite.addTestCase(test1);
136+
suite.addTestCase(test2);
137+
expect(suite.tests[0]).toBe(test1);
138+
expect(suite.tests[1]).toBe(test2);
139+
suite.swapTestCases(0, 1);
140+
expect(suite.tests[1]).toBe(test1);
141+
expect(suite.tests[0]).toBe(test2);
142+
});
123143
it("should replace the tests in the suite", () => {
124144
const store = new ProjectStore();
125145
const suite = new Suite();

packages/selenium-ide/src/neo/components/Suite/index.jsx

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,28 @@ import ListMenu, { ListMenuItem } from "../ListMenu";
3030
import MoreButton from "../ActionButtons/More";
3131
import "./style.css";
3232

33-
function containsTest(tests, test) {
34-
return tests.find((currTest) => (currTest.id === test.id));
35-
}
36-
3733
const testTarget = {
3834
canDrop(props, monitor) {
39-
const test = monitor.getItem();
40-
return !containsTest(props.suite.tests, test);
35+
const test = monitor.getItem().test;
36+
return !props.suite.containsTest(test);
4137
},
42-
drop(props, monitor) {
43-
if (monitor.didDrop()) {
38+
hover(props, monitor) {
39+
// check if they are different suites
40+
const dragged = monitor.getItem();
41+
if (monitor.canDrop() && props.suite !== dragged.suite) {
42+
dragged.suite.removeTestCase(dragged.test);
43+
props.suite.insertTestCaseAt(dragged.test, 0);
44+
dragged.suite = props.suite;
45+
dragged.index = 0;
4446
return;
4547
}
46-
47-
props.moveTest(monitor.getItem(), props.suite);
4848
}
4949
};
5050

5151
function collect(connect, monitor) {
5252
return {
5353
connectDropTarget: connect.dropTarget(),
54-
isOver: monitor.isOver(),
54+
isOver: monitor.isOver({ shallow: true }),
5555
canDrop: monitor.canDrop()
5656
};
5757
}
@@ -70,7 +70,6 @@ class Suite extends React.Component {
7070
rename: PropTypes.func.isRequired,
7171
editSettings: PropTypes.func.isRequired,
7272
remove: PropTypes.func.isRequired,
73-
moveTest: PropTypes.func.isRequired,
7473
isOver: PropTypes.bool,
7574
canDrop: PropTypes.bool,
7675
onContextMenu: PropTypes.func,
@@ -101,13 +100,15 @@ class Suite extends React.Component {
101100
//setting component of context menu.
102101
this.props.setContextMenu(listMenu);
103102

104-
return this.props.connectDropTarget(
103+
return (
105104
<div onKeyDown={this.handleKeyDown.bind(this)} >
106105
<div className="project" onContextMenu={this.props.onContextMenu} >
107-
<a href="#" tabIndex="-1" className={classNames(PlaybackState.suiteState.get(this.props.suite.id), { "hover": (this.props.isOver && this.props.canDrop) }, { "active": this.store.isOpen })} onClick={this.handleClick} >
108-
<span className="si-caret"></span>
109-
<span className="title">{this.props.suite.name}</span>
110-
</a>
106+
{this.props.connectDropTarget(
107+
<a href="#" tabIndex="-1" className={classNames(PlaybackState.suiteState.get(this.props.suite.id), { "active": this.store.isOpen })} onClick={this.handleClick} >
108+
<span className="si-caret"></span>
109+
<span className="title">{this.props.suite.name}</span>
110+
</a>
111+
)}
111112
{listMenu}
112113
</div>
113114
<TestList collapsed={!this.store.isOpen} suite={this.props.suite} tests={this.store.filteredTests.get()} removeTest={(test) => {

packages/selenium-ide/src/neo/components/Suite/style.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@
1313
min-width: 0;
1414
}
1515

16-
.project > a.hover {
17-
border-bottom: 1px #40A6FF solid;
18-
}
19-
2016
.project > a .title {
2117
padding: 0 6px;
2218
font-size: 14px;

packages/selenium-ide/src/neo/components/SuiteList/index.jsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ import "./style.css";
2828
selectTests: PropTypes.func.isRequired,
2929
rename: PropTypes.func.isRequired,
3030
editSettings: PropTypes.func.isRequired,
31-
removeSuite: PropTypes.func.isRequired,
32-
moveTest: PropTypes.func.isRequired
31+
removeSuite: PropTypes.func.isRequired
3332
};
3433
render() {
3534
return (
@@ -42,7 +41,6 @@ import "./style.css";
4241
rename={this.props.rename}
4342
editSettings={() => {this.props.editSettings(suite);}}
4443
remove={() => {this.props.removeSuite(suite);}}
45-
moveTest={this.props.moveTest}
4644
/>
4745
</li>
4846
))}

packages/selenium-ide/src/neo/components/Test/index.jsx

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import React from "react";
1919
import PropTypes from "prop-types";
20-
import { DragSource } from "react-dnd";
20+
import { DragSource, DropTarget } from "react-dnd";
2121
import classNames from "classnames";
2222
import { modifier } from "modifier-keys";
2323
import Callstack from "../Callstack";
@@ -28,20 +28,92 @@ import MoreButton from "../ActionButtons/More";
2828
import "./style.css";
2929

3030
export const Type = "test";
31+
3132
const testSource = {
3233
beginDrag(props) {
3334
return {
3435
id: props.test.id,
35-
suite: props.suite.id
36+
index: props.index,
37+
test: props.test,
38+
suite: props.suite
3639
};
40+
},
41+
isDragging(props, monitor) {
42+
return (props.test.id === monitor.getItem().id && props.suite.id === monitor.getItem().suite.id);
3743
}
3844
};
39-
function collect(connect, monitor) {
45+
46+
function collectSource(connect, monitor) {
4047
return {
4148
connectDragSource: connect.dragSource(),
4249
isDragging: monitor.isDragging()
4350
};
4451
}
52+
53+
const testTarget = {
54+
canDrop(props, monitor) {
55+
const test = monitor.getItem().test;
56+
const suite = props.suite;
57+
return !suite.containsTest(test);
58+
},
59+
hover(props, monitor, component) {
60+
// check if they are different suites
61+
const dragged = monitor.getItem();
62+
if (monitor.canDrop() && props.suite !== dragged.suite) {
63+
dragged.suite.removeTestCase(dragged.test);
64+
props.suite.addTestCase(dragged.test);
65+
dragged.suite = props.suite;
66+
dragged.index = props.suite.tests.length - 1;
67+
return;
68+
} else if (!monitor.canDrop() && props.suite !== dragged.suite) {
69+
// trying to move a duplicate
70+
return;
71+
}
72+
const dragIndex = dragged.index;
73+
const hoverIndex = props.index;
74+
75+
// Don't replace items with themselves
76+
if (dragIndex === hoverIndex) {
77+
return;
78+
}
79+
80+
// Determine rectangle on screen
81+
const hoverBoundingRect = component.decoratedComponentInstance.node.getBoundingClientRect();
82+
83+
// Get vertical middle
84+
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
85+
86+
// Determine mouse position
87+
const clientOffset = monitor.getClientOffset();
88+
89+
// Get pixels to the top
90+
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
91+
92+
// Only perform the move when the mouse has crossed half of the items height
93+
// When dragging downwards, only move when the cursor is below 50%
94+
// When dragging upwards, only move when the cursor is above 50%
95+
96+
// Dragging downwards
97+
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
98+
return;
99+
}
100+
101+
// Dragging upwards
102+
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
103+
return;
104+
}
105+
106+
props.swapTests(dragIndex, hoverIndex);
107+
108+
// save time on index lookups
109+
monitor.getItem().index = hoverIndex;
110+
}
111+
};
112+
113+
const collectTarget = (connect) => ({
114+
connectDropTarget: connect.dropTarget()
115+
});
116+
45117
export default class Test extends React.Component {
46118
static propTypes = {
47119
className: PropTypes.string,
@@ -58,6 +130,7 @@ export default class Test extends React.Component {
58130
selectTest: PropTypes.func.isRequired,
59131
renameTest: PropTypes.func,
60132
removeTest: PropTypes.func,
133+
connectDropTarget: PropTypes.func,
61134
connectDragSource: PropTypes.func,
62135
moveSelectionUp: PropTypes.func,
63136
moveSelectionDown: PropTypes.func,
@@ -127,7 +200,7 @@ export default class Test extends React.Component {
127200
tabIndex={this.props.selected ? "0" : "-1"}
128201
onContextMenu={this.props.onContextMenu}
129202
style={{
130-
display: this.props.isDragging ? "none" : "block"
203+
opacity: this.props.isDragging ? "0" : "1"
131204
}}>
132205
<a
133206
ref={(button) => { this.button = button; }}
@@ -144,7 +217,7 @@ export default class Test extends React.Component {
144217
onClick={this.handleCallstackClick.bind(this, this.props.test, this.props.suite)}
145218
/> : undefined}
146219
</div>;
147-
return (this.props.connectDragSource ? this.props.connectDragSource(rendered) : rendered);
220+
return (this.props.connectDragSource ? this.props.connectDropTarget(this.props.connectDragSource(rendered)) : rendered);
148221
}
149222
}
150223

@@ -164,4 +237,5 @@ export class MenuTest extends React.Component {
164237
}
165238
}
166239

167-
export const DraggableTest = DragSource(Type, testSource, collect)(Test);
240+
241+
export const DraggableTest = DropTarget(Type, testTarget, collectTarget)(DragSource(Type, testSource, collectSource)(Test));

packages/selenium-ide/src/neo/components/TestList/index.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export default class TestList extends Component {
6161
this.props.suite ?
6262
<DraggableTest
6363
className={PlaybackState.testState.get(test.id)}
64+
index={index}
6465
test={test}
6566
suite={this.props.suite}
6667
selected={UiState.selectedTest.test && test.id === UiState.selectedTest.test.id && this.props.suite.id === (UiState.selectedTest.suite ? UiState.selectedTest.suite.id : undefined)}
@@ -69,6 +70,7 @@ export default class TestList extends Component {
6970
changed={UiState.getTestState(test).modified}
7071
selectTest={UiState.selectTest}
7172
removeTest={this.props.removeTest ? () => { this.props.removeTest(test); } : undefined}
73+
swapTests={this.props.suite.swapTestCases}
7274
moveSelectionUp={() => { UiState.selectTestByIndex(index - 1, this.props.suite); }}
7375
moveSelectionDown={() => { UiState.selectTestByIndex(index + 1, this.props.suite); }}
7476
setSectionFocus={UiState.setSectionFocus}

packages/selenium-ide/src/neo/containers/Navigation/index.jsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ import "./style.css";
3939
}
4040
static propTypes = {
4141
suites: MobxPropTypes.arrayOrObservableArray.isRequired,
42-
tests: MobxPropTypes.arrayOrObservableArray.isRequired,
43-
moveTest: PropTypes.func.isRequired
42+
tests: MobxPropTypes.arrayOrObservableArray.isRequired
4443
};
4544
handleChangedTab(tab) {
4645
if (PlaybackState.isPlaying && !PlaybackState.paused) {
@@ -97,7 +96,6 @@ import "./style.css";
9796
editSettings={ModalState.editSuiteSettings}
9897
selectTests={ModalState.editSuite}
9998
removeSuite={ModalState.deleteSuite}
100-
moveTest={this.props.moveTest}
10199
/>
102100
</React.Fragment>
103101
}

packages/selenium-ide/src/neo/containers/Panel/index.jsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ if (browser.windows) {
100100
constructor(props) {
101101
super(props);
102102
this.state = { project };
103-
this.moveTest = this.moveTest.bind(this);
104103
this.keyDownHandler = window.document.body.onkeydown = this.handleKeyDown.bind(this);
105104
this.resizeHandler = window.addEventListener("resize", this.handleResize.bind(this, window));
106105
this.quitHandler = window.addEventListener("beforeunload", (e) => {
@@ -112,13 +111,6 @@ if (browser.windows) {
112111
}
113112
});
114113
}
115-
moveTest(testItem, destination) {
116-
const origin = this.state.project.suites.find((suite) => (suite.id === testItem.suite));
117-
const test = origin.tests.find(test => (test.id === testItem.id));
118-
119-
destination.addTestCase(test);
120-
origin.removeTestCase(test);
121-
}
122114
handleResize(currWindow) {
123115
UiState.setWindowHeight(currWindow.innerHeight);
124116
storage.set({
@@ -192,7 +184,6 @@ if (browser.windows) {
192184
createSuite={this.createSuite}
193185
removeSuite={this.state.project.deleteSuite}
194186
createTest={this.createTest}
195-
moveTest={this.moveTest}
196187
deleteTest={this.deleteTest}
197188
/>
198189
<Editor

packages/selenium-ide/src/neo/models/Suite.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ export default class Suite {
3636
}
3737

3838
@computed get tests() {
39-
return this._tests.sort((t1, t2) => (
39+
return this.isParallel ? this._tests.sort((t1, t2) => (
4040
naturalCompare(t1.name, t2.name)
41-
));
41+
)) : this._tests;
4242
}
4343

4444
isTest(test) {
@@ -71,6 +71,10 @@ export default class Suite {
7171
}
7272
}
7373

74+
@action.bound containsTest(test) {
75+
return this._tests.includes(test);
76+
}
77+
7478
@action.bound addTestCase(test) {
7579
if (!this.isTest(test)) {
7680
throw new Error(`Expected to receive TestCase instead received ${test ? test.constructor.name : test}`);
@@ -79,6 +83,14 @@ export default class Suite {
7983
}
8084
}
8185

86+
@action.bound insertTestCaseAt(test, index) {
87+
if (!this.isTest(test)) {
88+
throw new Error(`Expected to receive TestCase instead received ${test ? test.constructor.name : test}`);
89+
} else {
90+
this._tests.splice(index, 0, test);
91+
}
92+
}
93+
8294
@action.bound removeTestCase(test) {
8395
if (!this.isTest(test)) {
8496
throw new Error(`Expected to receive TestCase instead received ${test ? test.constructor.name : test}`);
@@ -87,6 +99,11 @@ export default class Suite {
8799
}
88100
}
89101

102+
@action.bound swapTestCases(from, to) {
103+
const test = this._tests.splice(from, 1)[0];
104+
this.insertTestCaseAt(test, to);
105+
}
106+
90107
@action.bound replaceTestCases(tests) {
91108
if (tests.filter(test => !this.isTest(test)).length) {
92109
throw new Error("Expected to receive array of TestCase");

0 commit comments

Comments
 (0)