cm-chessboard 8.12.16 → 8.12.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/package.json +3 -2
- package/src/view/VisualMoveInput.js +16 -11
- package/test/TestArrows.js +33 -0
- package/test/TestChessboard.js +44 -1
- package/test/TestMarkers.js +15 -0
- package/test/TestPosition.js +36 -0
- package/test/TestVisualMoveInput.js +194 -0
- package/test/headless.mjs +109 -0
- package/test/index.html +2 -0
package/README.md
CHANGED
|
@@ -207,6 +207,13 @@ The event has the following **`event.type`**:
|
|
|
207
207
|
Return `true` or `false` to validate the start square. `false` cancels the move.
|
|
208
208
|
- **`INPUT_EVENT_TYPE.validateMoveInput`**: To validate the users move input. `event.squareFrom` and `event.squareTo`
|
|
209
209
|
contain the coordinates. Return `true` or `false` to validate the move. `false` cancels the move.
|
|
210
|
+
The validator runs once at most per sequence of actions that describe a move (e.g. click starting piece, click destination),
|
|
211
|
+
so side effects like updating game position state or triggering sounds can run here.
|
|
212
|
+
One special case is moving a piece to a square occupied by another friendly piece. This kind of move can be useful
|
|
213
|
+
when you want to allow chess960 castling style (moving the king into the rook position) or when you want to allow
|
|
214
|
+
recapture premoves. Return `true` to commit it (e.g. castling by clicking the rook). When you return `false` here the
|
|
215
|
+
clicked own piece simply becomes the new selection, so if you show illegal-move feedback, skip it when `event.squareTo`
|
|
216
|
+
still holds one of the mover's own pieces.
|
|
210
217
|
- **`INPUT_EVENT_TYPE.moveInputCanceled`**: The user canceled the move with clicking again on the start square, clicking
|
|
211
218
|
outside the board or right click.
|
|
212
219
|
- **`INPUT_EVENT_TYPE.moveInputFinished`**: Fired after the move was made, also when canceled.
|
|
@@ -416,6 +423,17 @@ const chessboard = new Chessboard(document.getElementById("board"), {
|
|
|
416
423
|
|
|
417
424
|
When `movePieceForm` is enabled, press **Shift+Option+E** (Mac) or **Shift+Alt+E** (Windows/Linux) to focus the "Move from" input field.
|
|
418
425
|
|
|
426
|
+
## Testing
|
|
427
|
+
|
|
428
|
+
The unit tests use the tiny [Teevi](https://github.com/shaack/teevi) framework and run in a real browser.
|
|
429
|
+
|
|
430
|
+
- **In the browser (no install needed):** open [`test/index.html`](test/index.html) directly, or run `npm test`, which
|
|
431
|
+
just prints that hint.
|
|
432
|
+
- **Headless (optional):** `npm run test:headless` runs the same suite in headless Chrome and exits non-zero on failure,
|
|
433
|
+
which is handy for CI. To keep cm-chessboard dependency-free, [puppeteer](https://pptr.dev) is **not** a project
|
|
434
|
+
dependency; install it **globally** for this: `npm install -g puppeteer`. The runner starts its own static server, so
|
|
435
|
+
nothing else is required.
|
|
436
|
+
|
|
419
437
|
## Usage with JS Frameworks
|
|
420
438
|
|
|
421
439
|
- Works with **Vue** out of the box
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cm-chessboard",
|
|
3
|
-
"version": "8.12.
|
|
3
|
+
"version": "8.12.18",
|
|
4
4
|
"description": "A JavaScript chessboard which is lightweight, ES6 module based, responsive, SVG rendered and without dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chess",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"module": "./src/Chessboard.js",
|
|
17
17
|
"browser": "./src/Chessboard.js",
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "tput setaf 4;echo 'Run test/index.html in your browser for unit testing.'; echo ''; exit 0"
|
|
19
|
+
"test": "tput setaf 4;echo 'Run test/index.html in your browser for unit testing, or `npm run test:headless` (needs a global puppeteer).'; echo ''; exit 0",
|
|
20
|
+
"test:headless": "node test/headless.mjs"
|
|
20
21
|
},
|
|
21
22
|
"repository": {
|
|
22
23
|
"type": "git",
|
|
@@ -171,7 +171,10 @@ export class VisualMoveInput {
|
|
|
171
171
|
throw new Error("moveInputState")
|
|
172
172
|
}
|
|
173
173
|
this.toSquare = params.square
|
|
174
|
-
if
|
|
174
|
+
// if the move was already validated, don't trigger the validator again so possible user side effects run once at most
|
|
175
|
+
const validated = params.validated !== undefined ?
|
|
176
|
+
params.validated : this.validateMoveInputCallback(this.fromSquare, this.toSquare)
|
|
177
|
+
if (this.toSquare && validated) {
|
|
175
178
|
this.chessboard.movePiece(this.fromSquare, this.toSquare, prevState === MOVE_INPUT_STATE.clickTo).then(() => {
|
|
176
179
|
if (prevState === MOVE_INPUT_STATE.clickTo) {
|
|
177
180
|
this.view.setPieceVisibility(this.toSquare, true)
|
|
@@ -298,9 +301,11 @@ export class VisualMoveInput {
|
|
|
298
301
|
const startPieceName = this.chessboard.getPiece(this.fromSquare)
|
|
299
302
|
const startPieceColor = startPieceName ? startPieceName.substring(0, 1) : null
|
|
300
303
|
if (color && startPieceColor === pieceColor) {
|
|
301
|
-
// added to allow chess960
|
|
304
|
+
// added to allow moves into own pieces, useful for chess960 castle style or recapture premoves
|
|
305
|
+
// result holds false if the user legality checker deemed the move into another own piece as illegal
|
|
306
|
+
// in that case, we start a new move by selecting the target piece
|
|
302
307
|
const result = this.validateMoveInputCallback(this.fromSquare, square)
|
|
303
|
-
if(!result) {
|
|
308
|
+
if (!result) {
|
|
304
309
|
this.moveInputCanceledCallback(this.fromSquare, square, MOVE_CANCELED_REASON.clickedAnotherPiece)
|
|
305
310
|
if (this.moveInputStartedCallback(square)) {
|
|
306
311
|
this.setMoveInputState(MOVE_INPUT_STATE.pieceClickedThreshold, {
|
|
@@ -312,6 +317,10 @@ export class VisualMoveInput {
|
|
|
312
317
|
} else {
|
|
313
318
|
this.setMoveInputState(MOVE_INPUT_STATE.reset)
|
|
314
319
|
}
|
|
320
|
+
} else {
|
|
321
|
+
// if the user deemed the move into own piece legal, execute it
|
|
322
|
+
// but prevent validating the move again with the validated flag
|
|
323
|
+
this.setMoveInputState(MOVE_INPUT_STATE.moveDone, {square: square, validated: true})
|
|
315
324
|
}
|
|
316
325
|
} else {
|
|
317
326
|
this.setMoveInputState(MOVE_INPUT_STATE.moveDone, {square: square})
|
|
@@ -384,14 +393,10 @@ export class VisualMoveInput {
|
|
|
384
393
|
if (square) {
|
|
385
394
|
if (this.moveInputState === MOVE_INPUT_STATE.dragTo || this.moveInputState === MOVE_INPUT_STATE.clickDragTo) {
|
|
386
395
|
if (this.fromSquare === square) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
this.setMoveInputState(MOVE_INPUT_STATE.reset)
|
|
392
|
-
} else {
|
|
393
|
-
this.setMoveInputState(MOVE_INPUT_STATE.clickTo, {square: square})
|
|
394
|
-
}
|
|
396
|
+
this.chessboard.state.position.setPiece(this.fromSquare, this.movedPiece)
|
|
397
|
+
this.view.setPieceVisibility(this.fromSquare)
|
|
398
|
+
this.moveInputCanceledCallback(square, null, MOVE_CANCELED_REASON.draggedBack)
|
|
399
|
+
this.setMoveInputState(MOVE_INPUT_STATE.reset)
|
|
395
400
|
} else {
|
|
396
401
|
this.setMoveInputState(MOVE_INPUT_STATE.moveDone, {square: square})
|
|
397
402
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Author and copyright: Stefan Haack (https://shaack.com)
|
|
3
|
+
* Repository: https://github.com/shaack/cm-chessboard
|
|
4
|
+
* License: MIT, see file 'LICENSE'
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {describe, it, assert} from "../node_modules/teevi/src/teevi.js"
|
|
8
|
+
import {Chessboard} from "../src/Chessboard.js"
|
|
9
|
+
import {ARROW_TYPE, Arrows} from "../src/extensions/arrows/Arrows.js"
|
|
10
|
+
|
|
11
|
+
describe("TestArrows", () => {
|
|
12
|
+
|
|
13
|
+
it("should add, get and remove arrows", () => {
|
|
14
|
+
const chessboard = new Chessboard(document.getElementById("TestArrows"), {
|
|
15
|
+
assetsUrl: "../assets/",
|
|
16
|
+
extensions: [{class: Arrows}]
|
|
17
|
+
})
|
|
18
|
+
chessboard.addArrow(ARROW_TYPE.default, "e2", "e4")
|
|
19
|
+
chessboard.addArrow(ARROW_TYPE.danger, "d2", "d4")
|
|
20
|
+
assert.equal(chessboard.getArrows().length, 2)
|
|
21
|
+
assert.equal(chessboard.getArrows(ARROW_TYPE.danger).length, 1)
|
|
22
|
+
assert.equal(chessboard.getArrows(undefined, "e2").length, 1)
|
|
23
|
+
assert.equal(chessboard.getArrows(undefined, "e2", "e4").length, 1)
|
|
24
|
+
assert.equal(chessboard.getArrows(undefined, "a1").length, 0)
|
|
25
|
+
chessboard.removeArrows(undefined, "e2")
|
|
26
|
+
assert.equal(chessboard.getArrows().length, 1)
|
|
27
|
+
assert.equal(chessboard.getArrows()[0].from, "d2")
|
|
28
|
+
chessboard.removeArrows()
|
|
29
|
+
assert.equal(chessboard.getArrows().length, 0)
|
|
30
|
+
chessboard.destroy()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
})
|
package/test/TestChessboard.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {describe, it, assert} from "../node_modules/teevi/src/teevi.js"
|
|
8
|
-
import {PIECE, Chessboard} from "../src/Chessboard.js"
|
|
8
|
+
import {PIECE, COLOR, Chessboard} from "../src/Chessboard.js"
|
|
9
9
|
import {FEN} from "../src/model/Position.js"
|
|
10
10
|
|
|
11
11
|
describe("TestChessboard", () => {
|
|
@@ -69,6 +69,49 @@ describe("TestChessboard", () => {
|
|
|
69
69
|
chessboard.destroy()
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
+
it("should move a piece via the api", async () => {
|
|
73
|
+
const chessboard = new Chessboard(document.getElementById("TestBoard"), {
|
|
74
|
+
assetsUrl: "../assets/",
|
|
75
|
+
position: FEN.start,
|
|
76
|
+
style: {animationDuration: 0}
|
|
77
|
+
})
|
|
78
|
+
await chessboard.movePiece("e2", "e4", false)
|
|
79
|
+
assert.equal(chessboard.getPiece("e2"), null)
|
|
80
|
+
assert.equal(chessboard.getPiece("e4"), "wp")
|
|
81
|
+
chessboard.destroy()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("should remove a piece when set to null", () => {
|
|
85
|
+
const chessboard = new Chessboard(document.getElementById("TestBoard"), {
|
|
86
|
+
assetsUrl: "../assets/",
|
|
87
|
+
position: FEN.start
|
|
88
|
+
})
|
|
89
|
+
chessboard.setPiece("e2", null)
|
|
90
|
+
assert.equal(chessboard.getPiece("e2"), null)
|
|
91
|
+
chessboard.destroy()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("should create an empty board from FEN.empty", () => {
|
|
95
|
+
const chessboard = new Chessboard(document.getElementById("TestBoard"), {
|
|
96
|
+
assetsUrl: "../assets/",
|
|
97
|
+
position: FEN.empty
|
|
98
|
+
})
|
|
99
|
+
assert.equal("" + chessboard.getPosition(), "8/8/8/8/8/8/8/8")
|
|
100
|
+
chessboard.destroy()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("should set and get the orientation", async () => {
|
|
104
|
+
const chessboard = new Chessboard(document.getElementById("TestBoard"), {
|
|
105
|
+
assetsUrl: "../assets/",
|
|
106
|
+
position: FEN.start,
|
|
107
|
+
style: {animationDuration: 0}
|
|
108
|
+
})
|
|
109
|
+
assert.equal(chessboard.getOrientation(), COLOR.white)
|
|
110
|
+
await chessboard.setOrientation(COLOR.black)
|
|
111
|
+
assert.equal(chessboard.getOrientation(), COLOR.black)
|
|
112
|
+
chessboard.destroy()
|
|
113
|
+
})
|
|
114
|
+
|
|
72
115
|
// Regression for https://github.com/shaack/cm-chessboard/issues/154
|
|
73
116
|
//
|
|
74
117
|
// A setPosition() call with a null diff (positionFrom === positionTo)
|
package/test/TestMarkers.js
CHANGED
|
@@ -39,4 +39,19 @@ describe("TestMarkers", () => {
|
|
|
39
39
|
chessboard.destroy()
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
+
it("should add different marker types and remove all markers at once", () => {
|
|
43
|
+
const chessboard = new Chessboard(document.getElementById("TestMarkers"), {
|
|
44
|
+
assetsUrl: "../assets/",
|
|
45
|
+
extensions: [{class: Markers}]
|
|
46
|
+
})
|
|
47
|
+
chessboard.addMarker(MARKER_TYPE.dot, "e4")
|
|
48
|
+
chessboard.addMarker(MARKER_TYPE.circle, "d4")
|
|
49
|
+
chessboard.addMarker(MARKER_TYPE.circleDanger, "c4")
|
|
50
|
+
assert.equal(chessboard.getMarkers().length, 3)
|
|
51
|
+
assert.equal(chessboard.getMarkers(MARKER_TYPE.dot).length, 1)
|
|
52
|
+
chessboard.removeMarkers()
|
|
53
|
+
assert.equal(chessboard.getMarkers().length, 0)
|
|
54
|
+
chessboard.destroy()
|
|
55
|
+
})
|
|
56
|
+
|
|
42
57
|
})
|
package/test/TestPosition.js
CHANGED
|
@@ -39,5 +39,41 @@ describe("TestPosition", () => {
|
|
|
39
39
|
assert.equal(rooks.length, 1)
|
|
40
40
|
assert.equal(rooks[0].square, "f5")
|
|
41
41
|
})
|
|
42
|
+
it("should round-trip every square through index conversion", () => {
|
|
43
|
+
for (let i = 0; i < 64; i++) {
|
|
44
|
+
assert.equal(Position.squareToIndex(Position.indexToSquare(i)), i)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
it("should set, get and remove a piece", () => {
|
|
48
|
+
const position = new Position(FEN.empty)
|
|
49
|
+
assert.equal(position.getPiece("e4"), null)
|
|
50
|
+
position.setPiece("e4", "wq")
|
|
51
|
+
assert.equal(position.getPiece("e4"), "wq")
|
|
52
|
+
position.setPiece("e4", null)
|
|
53
|
+
assert.equal(position.getPiece("e4"), null)
|
|
54
|
+
})
|
|
55
|
+
it("should move a piece", () => {
|
|
56
|
+
const position = new Position("8/8/8/8/8/8/4P3/8")
|
|
57
|
+
position.movePiece("e2", "e4")
|
|
58
|
+
assert.equal(position.getPiece("e2"), null)
|
|
59
|
+
assert.equal(position.getPiece("e4"), "wp")
|
|
60
|
+
})
|
|
61
|
+
it("should represent an empty board as FEN", () => {
|
|
62
|
+
const position = new Position(FEN.empty)
|
|
63
|
+
assert.equal(position.getFen(), "8/8/8/8/8/8/8/8")
|
|
64
|
+
})
|
|
65
|
+
it("should store only the piece placement of a full FEN", () => {
|
|
66
|
+
const position = new Position("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
|
|
67
|
+
assert.equal(position.getFen(), "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR")
|
|
68
|
+
})
|
|
69
|
+
it("should count the pieces of the start position", () => {
|
|
70
|
+
const position = new Position(FEN.start)
|
|
71
|
+
assert.equal(position.getPieces().length, 32)
|
|
72
|
+
assert.equal(position.getPieces(COLOR.white).length, 16)
|
|
73
|
+
assert.equal(position.getPieces(COLOR.black, PIECE_TYPE.pawn).length, 8)
|
|
74
|
+
})
|
|
75
|
+
it("should return no pieces for an empty board", () => {
|
|
76
|
+
assert.equal(new Position(FEN.empty).getPieces().length, 0)
|
|
77
|
+
})
|
|
42
78
|
})
|
|
43
79
|
|
|
@@ -6,10 +6,35 @@
|
|
|
6
6
|
|
|
7
7
|
import {describe, it, assert} from "../node_modules/teevi/src/teevi.js"
|
|
8
8
|
import {Chessboard} from "../src/Chessboard.js"
|
|
9
|
+
import {COLOR, INPUT_EVENT_TYPE} from "../src/view/ChessboardView.js"
|
|
10
|
+
import {MOVE_CANCELED_REASON} from "../src/view/VisualMoveInput.js"
|
|
9
11
|
import {FEN} from "../src/model/Position.js"
|
|
10
12
|
|
|
11
13
|
const STATE_WAIT_FOR_INPUT_START = "waitForInputStart"
|
|
12
14
|
const STATE_PIECE_CLICKED_THRESHOLD = "pieceClickedThreshold"
|
|
15
|
+
const STATE_CLICK_TO = "clickTo"
|
|
16
|
+
const STATE_DRAG_TO = "dragTo"
|
|
17
|
+
|
|
18
|
+
// Synthetic pointer events. The mouse path reads e.target / e.clientX directly,
|
|
19
|
+
// so it drives the real state machine without needing document.elementFromPoint.
|
|
20
|
+
const squareTarget = (name) => ({getAttribute: (n) => (n === "data-square" ? name : null)})
|
|
21
|
+
const mouseDown = (name, x = 100, y = 100) => ({
|
|
22
|
+
type: "mousedown", button: 0, target: squareTarget(name),
|
|
23
|
+
clientX: x, clientY: y, preventDefault: () => {}
|
|
24
|
+
})
|
|
25
|
+
const mouseMove = (name, x, y) => ({
|
|
26
|
+
type: "mousemove", target: squareTarget(name), clientX: x, clientY: y, pageX: x, pageY: y
|
|
27
|
+
})
|
|
28
|
+
const mouseUp = (name) => ({type: "mouseup", target: squareTarget(name)})
|
|
29
|
+
const tick = () => new Promise((resolve) => setTimeout(resolve, 30))
|
|
30
|
+
|
|
31
|
+
function newBoard() {
|
|
32
|
+
return new Chessboard(document.getElementById("TestBoard"), {
|
|
33
|
+
assetsUrl: "../assets/",
|
|
34
|
+
position: FEN.start,
|
|
35
|
+
style: {animationDuration: 0}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
13
38
|
|
|
14
39
|
describe("TestVisualMoveInput", () => {
|
|
15
40
|
|
|
@@ -37,4 +62,173 @@ describe("TestVisualMoveInput", () => {
|
|
|
37
62
|
chessboard.destroy()
|
|
38
63
|
})
|
|
39
64
|
|
|
65
|
+
// Regression for https://github.com/shaack/cm-chessboard/pull/174
|
|
66
|
+
it("should validate once and finish the move when moving into own piece is marked as valid", async () => {
|
|
67
|
+
const chessboard = new Chessboard(document.getElementById("TestBoard"), {
|
|
68
|
+
assetsUrl: "../assets/",
|
|
69
|
+
position: FEN.start
|
|
70
|
+
})
|
|
71
|
+
let validateCallCount = 0
|
|
72
|
+
let moveInputFinishedCount = 0
|
|
73
|
+
chessboard.enableMoveInput((event) => {
|
|
74
|
+
if (event.type === INPUT_EVENT_TYPE.validateMoveInput) {
|
|
75
|
+
validateCallCount++
|
|
76
|
+
return true // accept the castling-by-clicking-the-rook move
|
|
77
|
+
} else if (event.type === INPUT_EVENT_TYPE.moveInputFinished) {
|
|
78
|
+
moveInputFinishedCount++
|
|
79
|
+
}
|
|
80
|
+
return true
|
|
81
|
+
}, COLOR.white)
|
|
82
|
+
const visualMoveInput = chessboard.view.visualMoveInput
|
|
83
|
+
|
|
84
|
+
// simulate an existing click-to-move selection of the white king on e1
|
|
85
|
+
visualMoveInput.moveInputStartedCallback("e1")
|
|
86
|
+
visualMoveInput.moveInputState = STATE_CLICK_TO
|
|
87
|
+
visualMoveInput.fromSquare = "e1"
|
|
88
|
+
|
|
89
|
+
// castle by clicking the rook on h1 -> same-color-click branch
|
|
90
|
+
visualMoveInput.onPointerDown({
|
|
91
|
+
type: "touchstart",
|
|
92
|
+
target: {getAttribute: (name) => (name === "data-square" ? "h1" : null)},
|
|
93
|
+
touches: [{clientX: 100, clientY: 100}],
|
|
94
|
+
preventDefault: () => {}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
assert.equal(validateCallCount, 1)
|
|
98
|
+
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve))
|
|
100
|
+
// the move must complete (not get stuck in `clickTo`), firing moveInputFinished exactly once
|
|
101
|
+
assert.equal(moveInputFinishedCount, 1)
|
|
102
|
+
|
|
103
|
+
chessboard.destroy()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("should complete a click-to-move (select, then click destination)", async () => {
|
|
107
|
+
const board = newBoard()
|
|
108
|
+
let finished = 0
|
|
109
|
+
board.enableMoveInput((e) => {
|
|
110
|
+
if (e.type === INPUT_EVENT_TYPE.moveInputFinished) finished++
|
|
111
|
+
return true
|
|
112
|
+
}, COLOR.white)
|
|
113
|
+
const vmi = board.view.visualMoveInput
|
|
114
|
+
|
|
115
|
+
vmi.onPointerDown(mouseDown("e2"))
|
|
116
|
+
vmi.onPointerUp(mouseUp("e2"))
|
|
117
|
+
assert.equal(vmi.moveInputState, STATE_CLICK_TO)
|
|
118
|
+
|
|
119
|
+
vmi.onPointerDown(mouseDown("e4"))
|
|
120
|
+
await tick()
|
|
121
|
+
assert.equal(board.getPiece("e4"), "wp")
|
|
122
|
+
assert.equal(board.getPiece("e2"), null)
|
|
123
|
+
assert.equal(finished, 1)
|
|
124
|
+
board.destroy()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("should complete a drag move to a new square", async () => {
|
|
128
|
+
const board = newBoard()
|
|
129
|
+
board.enableMoveInput(() => true, COLOR.white)
|
|
130
|
+
const vmi = board.view.visualMoveInput
|
|
131
|
+
|
|
132
|
+
vmi.onPointerDown(mouseDown("e2", 100, 100))
|
|
133
|
+
vmi.onPointerMove(mouseMove("e4", 100, 140)) // past DRAG_THRESHOLD -> dragTo
|
|
134
|
+
assert.equal(vmi.moveInputState, STATE_DRAG_TO)
|
|
135
|
+
vmi.onPointerUp(mouseUp("e4"))
|
|
136
|
+
await tick()
|
|
137
|
+
assert.equal(board.getPiece("e4"), "wp")
|
|
138
|
+
assert.equal(board.getPiece("e2"), null)
|
|
139
|
+
board.destroy()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Regression for https://github.com/shaack/cm-chessboard/issues/170 (PR #171)
|
|
143
|
+
it("should cancel when a piece is dragged back to its origin", async () => {
|
|
144
|
+
const board = newBoard()
|
|
145
|
+
let canceledReason
|
|
146
|
+
board.enableMoveInput((e) => {
|
|
147
|
+
if (e.type === INPUT_EVENT_TYPE.moveInputCanceled) canceledReason = e.reason
|
|
148
|
+
return true
|
|
149
|
+
}, COLOR.white)
|
|
150
|
+
const vmi = board.view.visualMoveInput
|
|
151
|
+
|
|
152
|
+
vmi.onPointerDown(mouseDown("e2", 100, 100))
|
|
153
|
+
vmi.onPointerMove(mouseMove("e2", 100, 140)) // -> dragTo
|
|
154
|
+
assert.equal(vmi.moveInputState, STATE_DRAG_TO)
|
|
155
|
+
vmi.onPointerUp(mouseUp("e2")) // dropped back on origin
|
|
156
|
+
await tick()
|
|
157
|
+
|
|
158
|
+
assert.equal(canceledReason, MOVE_CANCELED_REASON.draggedBack)
|
|
159
|
+
assert.equal(vmi.moveInputState, STATE_WAIT_FOR_INPUT_START)
|
|
160
|
+
assert.equal(board.getPiece("e2"), "wp") // piece stays put
|
|
161
|
+
board.destroy()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Regression for https://github.com/shaack/cm-chessboard/issues/170
|
|
165
|
+
it("should re-select when clicking another own piece", async () => {
|
|
166
|
+
const board = newBoard()
|
|
167
|
+
let canceledReason
|
|
168
|
+
board.enableMoveInput((e) => {
|
|
169
|
+
if (e.type === INPUT_EVENT_TYPE.validateMoveInput) return false // not a legal move onto own piece
|
|
170
|
+
if (e.type === INPUT_EVENT_TYPE.moveInputCanceled) canceledReason = e.reason
|
|
171
|
+
return true
|
|
172
|
+
}, COLOR.white)
|
|
173
|
+
const vmi = board.view.visualMoveInput
|
|
174
|
+
|
|
175
|
+
vmi.onPointerDown(mouseDown("e2"))
|
|
176
|
+
vmi.onPointerUp(mouseUp("e2"))
|
|
177
|
+
assert.equal(vmi.moveInputState, STATE_CLICK_TO)
|
|
178
|
+
|
|
179
|
+
vmi.onPointerDown(mouseDown("d1")) // own white queen
|
|
180
|
+
assert.equal(canceledReason, MOVE_CANCELED_REASON.clickedAnotherPiece)
|
|
181
|
+
assert.equal(vmi.fromSquare, "d1")
|
|
182
|
+
assert.equal(vmi.moveInputState, STATE_PIECE_CLICKED_THRESHOLD)
|
|
183
|
+
await tick()
|
|
184
|
+
board.destroy()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// Regression for https://github.com/shaack/cm-chessboard/pull/173
|
|
188
|
+
it("should cancel an in-progress move on right click", async () => {
|
|
189
|
+
const board = newBoard()
|
|
190
|
+
let canceledReason
|
|
191
|
+
board.enableMoveInput((e) => {
|
|
192
|
+
if (e.type === INPUT_EVENT_TYPE.moveInputCanceled) canceledReason = e.reason
|
|
193
|
+
return true
|
|
194
|
+
}, COLOR.white)
|
|
195
|
+
const vmi = board.view.visualMoveInput
|
|
196
|
+
|
|
197
|
+
vmi.onPointerDown(mouseDown("e2"))
|
|
198
|
+
vmi.onPointerUp(mouseUp("e2"))
|
|
199
|
+
assert.equal(vmi.moveInputState, STATE_CLICK_TO)
|
|
200
|
+
|
|
201
|
+
vmi.onContextMenu({preventDefault: () => {}})
|
|
202
|
+
assert.equal(canceledReason, MOVE_CANCELED_REASON.secondaryClick)
|
|
203
|
+
assert.equal(vmi.moveInputState, STATE_WAIT_FOR_INPUT_START)
|
|
204
|
+
await tick()
|
|
205
|
+
board.destroy()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it("should ignore input when move input is disabled", () => {
|
|
209
|
+
const board = newBoard()
|
|
210
|
+
const vmi = board.view.visualMoveInput
|
|
211
|
+
vmi.onPointerDown(mouseDown("e2"))
|
|
212
|
+
assert.equal(vmi.moveInputState, STATE_WAIT_FOR_INPUT_START)
|
|
213
|
+
board.destroy()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it("should ignore a piece of the disabled color", () => {
|
|
217
|
+
const board = newBoard()
|
|
218
|
+
board.enableMoveInput(() => true, COLOR.white)
|
|
219
|
+
const vmi = board.view.visualMoveInput
|
|
220
|
+
vmi.onPointerDown(mouseDown("e7")) // black pawn, black input disabled
|
|
221
|
+
assert.equal(vmi.moveInputState, STATE_WAIT_FOR_INPUT_START)
|
|
222
|
+
board.destroy()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it("should not start a move when moveInputStarted returns false", () => {
|
|
226
|
+
const board = newBoard()
|
|
227
|
+
board.enableMoveInput((e) => e.type !== INPUT_EVENT_TYPE.moveInputStarted, COLOR.white)
|
|
228
|
+
const vmi = board.view.visualMoveInput
|
|
229
|
+
vmi.onPointerDown(mouseDown("e2"))
|
|
230
|
+
assert.equal(vmi.moveInputState, STATE_WAIT_FOR_INPUT_START)
|
|
231
|
+
board.destroy()
|
|
232
|
+
})
|
|
233
|
+
|
|
40
234
|
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Author and copyright: Stefan Haack (https://shaack.com)
|
|
3
|
+
* Repository: https://github.com/shaack/cm-chessboard
|
|
4
|
+
* License: MIT, see file 'LICENSE'
|
|
5
|
+
*
|
|
6
|
+
* Optional headless test runner. Serves the project over a tiny static server
|
|
7
|
+
* and runs test/index.html in headless Chrome, then prints the Teevi summary
|
|
8
|
+
* and exits non-zero on failure.
|
|
9
|
+
*
|
|
10
|
+
* Puppeteer is intentionally NOT a project dependency (cm-chessboard ships
|
|
11
|
+
* without dependencies). Install it globally to use this runner:
|
|
12
|
+
*
|
|
13
|
+
* npm install -g puppeteer
|
|
14
|
+
* npm run test:headless
|
|
15
|
+
*
|
|
16
|
+
* The regular `npm test` just points you at test/index.html in a browser and
|
|
17
|
+
* needs nothing installed.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {createServer} from "http"
|
|
21
|
+
import {createRequire} from "module"
|
|
22
|
+
import {execSync} from "child_process"
|
|
23
|
+
import {readFile} from "fs/promises"
|
|
24
|
+
import {fileURLToPath} from "url"
|
|
25
|
+
import {dirname, join, normalize, extname} from "path"
|
|
26
|
+
|
|
27
|
+
const projectRoot = normalize(join(dirname(fileURLToPath(import.meta.url)), ".."))
|
|
28
|
+
|
|
29
|
+
// Resolve the globally installed puppeteer without adding a project dependency.
|
|
30
|
+
function loadPuppeteer() {
|
|
31
|
+
let globalRoot
|
|
32
|
+
try {
|
|
33
|
+
globalRoot = execSync("npm root -g", {encoding: "utf8"}).trim()
|
|
34
|
+
} catch {
|
|
35
|
+
globalRoot = ""
|
|
36
|
+
}
|
|
37
|
+
for (const base of [globalRoot + "/", projectRoot + "/"]) {
|
|
38
|
+
try {
|
|
39
|
+
return createRequire(base)("puppeteer")
|
|
40
|
+
} catch { /* try next */ }
|
|
41
|
+
}
|
|
42
|
+
console.error(
|
|
43
|
+
"\nCould not find puppeteer. This headless runner needs it installed globally:\n" +
|
|
44
|
+
" npm install -g puppeteer\n" +
|
|
45
|
+
"Or just open test/index.html in a browser (see `npm test`).\n")
|
|
46
|
+
process.exit(2)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MIME = {
|
|
50
|
+
".html": "text/html", ".js": "text/javascript", ".mjs": "text/javascript",
|
|
51
|
+
".css": "text/css", ".svg": "image/svg+xml", ".json": "application/json",
|
|
52
|
+
".png": "image/png", ".map": "application/json"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function startServer() {
|
|
56
|
+
const server = createServer(async (req, res) => {
|
|
57
|
+
const urlPath = decodeURIComponent(req.url.split("?")[0])
|
|
58
|
+
const filePath = normalize(join(projectRoot, urlPath))
|
|
59
|
+
if (!filePath.startsWith(projectRoot)) { // block path traversal
|
|
60
|
+
res.writeHead(403).end()
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const body = await readFile(filePath)
|
|
65
|
+
res.writeHead(200, {"Content-Type": MIME[extname(filePath)] || "application/octet-stream"})
|
|
66
|
+
res.end(body)
|
|
67
|
+
} catch {
|
|
68
|
+
res.writeHead(404).end()
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
server.listen(0, "127.0.0.1", () => resolve({server, port: server.address().port}))
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const puppeteer = loadPuppeteer()
|
|
77
|
+
const {server, port} = await startServer()
|
|
78
|
+
const url = `http://127.0.0.1:${port}/test/index.html`
|
|
79
|
+
|
|
80
|
+
let exitCode = 1
|
|
81
|
+
const browser = await puppeteer.launch({headless: "new"})
|
|
82
|
+
try {
|
|
83
|
+
const page = await browser.newPage()
|
|
84
|
+
const errors = []
|
|
85
|
+
page.on("pageerror", (e) => errors.push(e.message))
|
|
86
|
+
await page.goto(url, {waitUntil: "networkidle0", timeout: 30000})
|
|
87
|
+
await page.waitForFunction(
|
|
88
|
+
() => /All \d+ tests passed|\d+ tests, \d+ passed, \d+ failed/.test(document.body.innerText),
|
|
89
|
+
{timeout: 30000})
|
|
90
|
+
const {summary, fails} = await page.evaluate(() => {
|
|
91
|
+
const text = document.body.innerText
|
|
92
|
+
const m = text.match(/All \d+ tests passed|\d+ tests, \d+ passed, \d+ failed/)
|
|
93
|
+
const fails = []
|
|
94
|
+
document.querySelectorAll("div").forEach((d) => {
|
|
95
|
+
if (d.innerText && d.innerText.includes("→ fail")) {
|
|
96
|
+
fails.push(d.innerText.replace(/\s+/g, " ").trim().slice(0, 500))
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
return {summary: m ? m[0] : "NO SUMMARY", fails}
|
|
100
|
+
})
|
|
101
|
+
console.log(summary)
|
|
102
|
+
for (const f of fails) console.log(" FAIL: " + f)
|
|
103
|
+
for (const e of errors.slice(0, 10)) console.log(" pageerror: " + e)
|
|
104
|
+
exitCode = /failed|NO SUMMARY/.test(summary) ? 1 : 0
|
|
105
|
+
} finally {
|
|
106
|
+
await browser.close()
|
|
107
|
+
server.close()
|
|
108
|
+
}
|
|
109
|
+
process.exit(exitCode)
|
package/test/index.html
CHANGED
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
<div class="board" id="TestBoard"></div>
|
|
21
21
|
<div class="board" id="TestPosition"></div>
|
|
22
22
|
<div class="board" id="TestMarkers"></div>
|
|
23
|
+
<div class="board" id="TestArrows"></div>
|
|
23
24
|
<script type="module">
|
|
24
25
|
import {teevi} from "../node_modules/teevi/src/teevi.js"
|
|
25
26
|
import "./TestChessboard.js"
|
|
26
27
|
import "./TestPiecesAnimation.js"
|
|
27
28
|
import "./TestMarkers.js"
|
|
29
|
+
import "./TestArrows.js"
|
|
28
30
|
import "./TestPosition.js"
|
|
29
31
|
import "./TestVisualMoveInput.js"
|
|
30
32
|
teevi.run()
|