Much more thorough tests! (#1053)
This commit is contained in:
parent
d4ff5cb926
commit
bd7856adf3
@ -2,3 +2,4 @@ node_modules/
|
|||||||
build/
|
build/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.vscode/
|
.vscode/
|
||||||
|
firebase/
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { AppState, FlooredNumber } from "./types";
|
import { AppState, FlooredNumber } from "./types";
|
||||||
import { getDateTime } from "./utils";
|
import { getDateTime } from "./utils";
|
||||||
|
|
||||||
const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`;
|
|
||||||
export const DEFAULT_FONT = "20px Virgil";
|
export const DEFAULT_FONT = "20px Virgil";
|
||||||
|
|
||||||
export function getDefaultAppState(): AppState {
|
export function getDefaultAppState(): AppState {
|
||||||
@ -26,7 +25,7 @@ export function getDefaultAppState(): AppState {
|
|||||||
cursorX: 0,
|
cursorX: 0,
|
||||||
cursorY: 0,
|
cursorY: 0,
|
||||||
scrolledOutside: false,
|
scrolledOutside: false,
|
||||||
name: DEFAULT_PROJECT_NAME,
|
name: `excalidraw-${getDateTime()}`,
|
||||||
isCollaborating: false,
|
isCollaborating: false,
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
|
@ -4,8 +4,8 @@ import { ExcalidrawElement } from "../element/types";
|
|||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { DataState } from "./types";
|
import { DataState } from "./types";
|
||||||
import { isInvisiblySmallElement, normalizeDimensions } from "../element";
|
import { isInvisiblySmallElement, normalizeDimensions } from "../element";
|
||||||
import nanoid from "nanoid";
|
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
|
import { randomId } from "../random";
|
||||||
|
|
||||||
export function restore(
|
export function restore(
|
||||||
// we're making the elements mutable for this API because we want to
|
// we're making the elements mutable for this API because we want to
|
||||||
@ -62,7 +62,7 @@ export function restore(
|
|||||||
...element,
|
...element,
|
||||||
// all elements must have version > 0 so getDrawingVersion() will pick up newly added elements
|
// all elements must have version > 0 so getDrawingVersion() will pick up newly added elements
|
||||||
version: element.version || 1,
|
version: element.version || 1,
|
||||||
id: element.id || nanoid(),
|
id: element.id || randomId(),
|
||||||
fillStyle: element.fillStyle || "hachure",
|
fillStyle: element.fillStyle || "hachure",
|
||||||
strokeWidth: element.strokeWidth || 1,
|
strokeWidth: element.strokeWidth || 1,
|
||||||
roughness: element.roughness ?? 1,
|
roughness: element.roughness ?? 1,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { ExcalidrawElement } from "./types";
|
import { ExcalidrawElement } from "./types";
|
||||||
import { randomSeed } from "roughjs/bin/math";
|
|
||||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||||
import { globalSceneState } from "../scene";
|
import { globalSceneState } from "../scene";
|
||||||
import { getSizeFromPoints } from "../points";
|
import { getSizeFromPoints } from "../points";
|
||||||
|
import { randomInteger } from "../random";
|
||||||
|
|
||||||
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||||
Partial<TElement>,
|
Partial<TElement>,
|
||||||
@ -42,7 +42,7 @@ export function mutateElement<TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
element.version++;
|
element.version++;
|
||||||
element.versionNonce = randomSeed();
|
element.versionNonce = randomInteger();
|
||||||
|
|
||||||
globalSceneState.informMutation();
|
globalSceneState.informMutation();
|
||||||
}
|
}
|
||||||
@ -54,7 +54,7 @@ export function newElementWith<TElement extends ExcalidrawElement>(
|
|||||||
return {
|
return {
|
||||||
...element,
|
...element,
|
||||||
version: element.version + 1,
|
version: element.version + 1,
|
||||||
versionNonce: randomSeed(),
|
versionNonce: randomInteger(),
|
||||||
...updates,
|
...updates,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
import { randomSeed } from "roughjs/bin/math";
|
|
||||||
import nanoid from "nanoid";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@ -8,6 +5,7 @@ import {
|
|||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { measureText } from "../utils";
|
import { measureText } from "../utils";
|
||||||
|
import { randomInteger, randomId } from "../random";
|
||||||
|
|
||||||
type ElementConstructorOpts = {
|
type ElementConstructorOpts = {
|
||||||
x: ExcalidrawGenericElement["x"];
|
x: ExcalidrawGenericElement["x"];
|
||||||
@ -39,7 +37,7 @@ function _newElementBase<T extends ExcalidrawElement>(
|
|||||||
}: ElementConstructorOpts & Partial<ExcalidrawGenericElement>,
|
}: ElementConstructorOpts & Partial<ExcalidrawGenericElement>,
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
id: rest.id || nanoid(),
|
id: rest.id || randomId(),
|
||||||
type,
|
type,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
@ -51,7 +49,7 @@ function _newElementBase<T extends ExcalidrawElement>(
|
|||||||
strokeWidth,
|
strokeWidth,
|
||||||
roughness,
|
roughness,
|
||||||
opacity,
|
opacity,
|
||||||
seed: rest.seed ?? randomSeed(),
|
seed: rest.seed ?? randomInteger(),
|
||||||
version: rest.version || 1,
|
version: rest.version || 1,
|
||||||
versionNonce: rest.versionNonce ?? 0,
|
versionNonce: rest.versionNonce ?? 0,
|
||||||
isDeleted: rest.isDeleted ?? false,
|
isDeleted: rest.isDeleted ?? false,
|
||||||
@ -145,8 +143,8 @@ export function duplicateElement<TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
overrides?: Partial<TElement>,
|
overrides?: Partial<TElement>,
|
||||||
): TElement {
|
): TElement {
|
||||||
let copy: TElement = _duplicateElement(element);
|
let copy: TElement = _duplicateElement(element);
|
||||||
copy.id = nanoid();
|
copy.id = randomId();
|
||||||
copy.seed = randomSeed();
|
copy.seed = randomInteger();
|
||||||
if (overrides) {
|
if (overrides) {
|
||||||
copy = Object.assign(copy, overrides);
|
copy = Object.assign(copy, overrides);
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,14 @@ export class SceneHistory {
|
|||||||
private stateHistory: string[] = [];
|
private stateHistory: string[] = [];
|
||||||
private redoStack: string[] = [];
|
private redoStack: string[] = [];
|
||||||
|
|
||||||
|
getSnapshotForTest() {
|
||||||
|
return {
|
||||||
|
recording: this.recording,
|
||||||
|
stateHistory: this.stateHistory.map((s) => JSON.parse(s)),
|
||||||
|
redoStack: this.redoStack.map((s) => JSON.parse(s)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.stateHistory.length = 0;
|
this.stateHistory.length = 0;
|
||||||
this.redoStack.length = 0;
|
this.redoStack.length = 0;
|
||||||
|
@ -14,6 +14,8 @@ export const KEYS = {
|
|||||||
SPACE: " ",
|
SPACE: " ",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export type Key = keyof typeof KEYS;
|
||||||
|
|
||||||
export function isArrowKey(keyCode: string) {
|
export function isArrowKey(keyCode: string) {
|
||||||
return (
|
return (
|
||||||
keyCode === KEYS.ARROW_LEFT ||
|
keyCode === KEYS.ARROW_LEFT ||
|
||||||
|
18
src/random.ts
Normal file
18
src/random.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Random } from "roughjs/bin/math";
|
||||||
|
import nanoid from "nanoid";
|
||||||
|
|
||||||
|
let random = new Random(Date.now());
|
||||||
|
let testIdBase = 0;
|
||||||
|
|
||||||
|
export function randomInteger() {
|
||||||
|
return Math.floor(random.next() * 2 ** 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reseed(seed: number) {
|
||||||
|
random = new Random(seed);
|
||||||
|
testIdBase = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function randomId() {
|
||||||
|
return process.env.NODE_ENV === "test" ? `id${testIdBase++}` : nanoid();
|
||||||
|
}
|
136
src/tests/__snapshots__/dragCreate.test.tsx.snap
Normal file
136
src/tests/__snapshots__/dragCreate.test.tsx.snap
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`add element to the scene when pointer dragging long enough arrow 1`] = `1`;
|
||||||
|
|
||||||
|
exports[`add element to the scene when pointer dragging long enough arrow 2`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 50,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"lastCommittedPoint": null,
|
||||||
|
"opacity": 100,
|
||||||
|
"points": Array [
|
||||||
|
Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
30,
|
||||||
|
50,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "arrow",
|
||||||
|
"version": 3,
|
||||||
|
"versionNonce": 449462985,
|
||||||
|
"width": 30,
|
||||||
|
"x": 30,
|
||||||
|
"y": 20,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`add element to the scene when pointer dragging long enough diamond 1`] = `1`;
|
||||||
|
|
||||||
|
exports[`add element to the scene when pointer dragging long enough diamond 2`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 50,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "diamond",
|
||||||
|
"version": 2,
|
||||||
|
"versionNonce": 1278240551,
|
||||||
|
"width": 30,
|
||||||
|
"x": 30,
|
||||||
|
"y": 20,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`add element to the scene when pointer dragging long enough ellipse 1`] = `1`;
|
||||||
|
|
||||||
|
exports[`add element to the scene when pointer dragging long enough ellipse 2`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 50,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "ellipse",
|
||||||
|
"version": 2,
|
||||||
|
"versionNonce": 1278240551,
|
||||||
|
"width": 30,
|
||||||
|
"x": 30,
|
||||||
|
"y": 20,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`add element to the scene when pointer dragging long enough line 1`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 50,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"lastCommittedPoint": null,
|
||||||
|
"opacity": 100,
|
||||||
|
"points": Array [
|
||||||
|
Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
30,
|
||||||
|
50,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "line",
|
||||||
|
"version": 3,
|
||||||
|
"versionNonce": 449462985,
|
||||||
|
"width": 30,
|
||||||
|
"x": 30,
|
||||||
|
"y": 20,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`add element to the scene when pointer dragging long enough rectangle 1`] = `1`;
|
||||||
|
|
||||||
|
exports[`add element to the scene when pointer dragging long enough rectangle 2`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 50,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "rectangle",
|
||||||
|
"version": 2,
|
||||||
|
"versionNonce": 1278240551,
|
||||||
|
"width": 30,
|
||||||
|
"x": 30,
|
||||||
|
"y": 20,
|
||||||
|
}
|
||||||
|
`;
|
64
src/tests/__snapshots__/move.test.tsx.snap
Normal file
64
src/tests/__snapshots__/move.test.tsx.snap
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`duplicate element on move when ALT is clicked rectangle 1`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 50,
|
||||||
|
"id": "id1",
|
||||||
|
"isDeleted": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 453191,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "rectangle",
|
||||||
|
"version": 2,
|
||||||
|
"versionNonce": 1278240551,
|
||||||
|
"width": 30,
|
||||||
|
"x": 30,
|
||||||
|
"y": 20,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`duplicate element on move when ALT is clicked rectangle 2`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 50,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
|
"versionNonce": 2019559783,
|
||||||
|
"width": 30,
|
||||||
|
"x": 0,
|
||||||
|
"y": 40,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`move element rectangle 1`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 50,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
|
"versionNonce": 401146281,
|
||||||
|
"width": 30,
|
||||||
|
"x": 0,
|
||||||
|
"y": 40,
|
||||||
|
}
|
||||||
|
`;
|
79
src/tests/__snapshots__/multiPointCreate.test.tsx.snap
Normal file
79
src/tests/__snapshots__/multiPointCreate.test.tsx.snap
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`multi point mode in linear elements arrow 1`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 110,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"lastCommittedPoint": Array [
|
||||||
|
70,
|
||||||
|
110,
|
||||||
|
],
|
||||||
|
"opacity": 100,
|
||||||
|
"points": Array [
|
||||||
|
Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
20,
|
||||||
|
30,
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
70,
|
||||||
|
110,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "arrow",
|
||||||
|
"version": 7,
|
||||||
|
"versionNonce": 1116226695,
|
||||||
|
"width": 70,
|
||||||
|
"x": 30,
|
||||||
|
"y": 30,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`multi point mode in linear elements line 1`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 110,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"lastCommittedPoint": Array [
|
||||||
|
70,
|
||||||
|
110,
|
||||||
|
],
|
||||||
|
"opacity": 100,
|
||||||
|
"points": Array [
|
||||||
|
Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
20,
|
||||||
|
30,
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
70,
|
||||||
|
110,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "line",
|
||||||
|
"version": 7,
|
||||||
|
"versionNonce": 1116226695,
|
||||||
|
"width": 70,
|
||||||
|
"x": 30,
|
||||||
|
"y": 30,
|
||||||
|
}
|
||||||
|
`;
|
10762
src/tests/__snapshots__/regressionTests.test.tsx.snap
Normal file
10762
src/tests/__snapshots__/regressionTests.test.tsx.snap
Normal file
File diff suppressed because it is too large
Load Diff
43
src/tests/__snapshots__/resize.test.tsx.snap
Normal file
43
src/tests/__snapshots__/resize.test.tsx.snap
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`resize element rectangle 1`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 50,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
|
"versionNonce": 1150084233,
|
||||||
|
"width": 30,
|
||||||
|
"x": 29,
|
||||||
|
"y": 47,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`resize element with aspect ratio when SHIFT is clicked rectangle 1`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 50,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "rectangle",
|
||||||
|
"version": 3,
|
||||||
|
"versionNonce": 1150084233,
|
||||||
|
"width": 30,
|
||||||
|
"x": 29,
|
||||||
|
"y": 47,
|
||||||
|
}
|
||||||
|
`;
|
128
src/tests/__snapshots__/selection.test.tsx.snap
Normal file
128
src/tests/__snapshots__/selection.test.tsx.snap
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`select single element on the scene arrow 1`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 50,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"lastCommittedPoint": null,
|
||||||
|
"opacity": 100,
|
||||||
|
"points": Array [
|
||||||
|
Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
30,
|
||||||
|
50,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "arrow",
|
||||||
|
"version": 3,
|
||||||
|
"versionNonce": 449462985,
|
||||||
|
"width": 30,
|
||||||
|
"x": 30,
|
||||||
|
"y": 20,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`select single element on the scene arrow escape 1`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 50,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"lastCommittedPoint": null,
|
||||||
|
"opacity": 100,
|
||||||
|
"points": Array [
|
||||||
|
Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
30,
|
||||||
|
50,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "line",
|
||||||
|
"version": 3,
|
||||||
|
"versionNonce": 449462985,
|
||||||
|
"width": 30,
|
||||||
|
"x": 30,
|
||||||
|
"y": 20,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`select single element on the scene diamond 1`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 50,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "diamond",
|
||||||
|
"version": 2,
|
||||||
|
"versionNonce": 1278240551,
|
||||||
|
"width": 30,
|
||||||
|
"x": 30,
|
||||||
|
"y": 20,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`select single element on the scene ellipse 1`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 50,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "ellipse",
|
||||||
|
"version": 2,
|
||||||
|
"versionNonce": 1278240551,
|
||||||
|
"width": 30,
|
||||||
|
"x": 30,
|
||||||
|
"y": 20,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`select single element on the scene rectangle 1`] = `
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fillStyle": "hachure",
|
||||||
|
"height": 50,
|
||||||
|
"id": "id0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"seed": 337897,
|
||||||
|
"strokeColor": "#000000",
|
||||||
|
"strokeWidth": 1,
|
||||||
|
"type": "rectangle",
|
||||||
|
"version": 2,
|
||||||
|
"versionNonce": 1278240551,
|
||||||
|
"width": 30,
|
||||||
|
"x": 30,
|
||||||
|
"y": 20,
|
||||||
|
}
|
||||||
|
`;
|
@ -5,6 +5,7 @@ import * as Renderer from "../renderer/renderScene";
|
|||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { render, fireEvent } from "./test-utils";
|
import { render, fireEvent } from "./test-utils";
|
||||||
import { ExcalidrawLinearElement } from "../element/types";
|
import { ExcalidrawLinearElement } from "../element/types";
|
||||||
|
import { reseed } from "../random";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
@ -13,6 +14,7 @@ const renderScene = jest.spyOn(Renderer, "renderScene");
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderScene.mockClear();
|
||||||
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
@ -44,6 +46,9 @@ describe("add element to the scene when pointer dragging long enough", () => {
|
|||||||
expect(h.elements[0].y).toEqual(20);
|
expect(h.elements[0].y).toEqual(20);
|
||||||
expect(h.elements[0].width).toEqual(30); // 60 - 30
|
expect(h.elements[0].width).toEqual(30); // 60 - 30
|
||||||
expect(h.elements[0].height).toEqual(50); // 70 - 20
|
expect(h.elements[0].height).toEqual(50); // 70 - 20
|
||||||
|
|
||||||
|
expect(h.elements.length).toMatchSnapshot();
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ellipse", () => {
|
it("ellipse", () => {
|
||||||
@ -72,6 +77,9 @@ describe("add element to the scene when pointer dragging long enough", () => {
|
|||||||
expect(h.elements[0].y).toEqual(20);
|
expect(h.elements[0].y).toEqual(20);
|
||||||
expect(h.elements[0].width).toEqual(30); // 60 - 30
|
expect(h.elements[0].width).toEqual(30); // 60 - 30
|
||||||
expect(h.elements[0].height).toEqual(50); // 70 - 20
|
expect(h.elements[0].height).toEqual(50); // 70 - 20
|
||||||
|
|
||||||
|
expect(h.elements.length).toMatchSnapshot();
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("diamond", () => {
|
it("diamond", () => {
|
||||||
@ -100,6 +108,9 @@ describe("add element to the scene when pointer dragging long enough", () => {
|
|||||||
expect(h.elements[0].y).toEqual(20);
|
expect(h.elements[0].y).toEqual(20);
|
||||||
expect(h.elements[0].width).toEqual(30); // 60 - 30
|
expect(h.elements[0].width).toEqual(30); // 60 - 30
|
||||||
expect(h.elements[0].height).toEqual(50); // 70 - 20
|
expect(h.elements[0].height).toEqual(50); // 70 - 20
|
||||||
|
|
||||||
|
expect(h.elements.length).toMatchSnapshot();
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("arrow", () => {
|
it("arrow", () => {
|
||||||
@ -132,6 +143,9 @@ describe("add element to the scene when pointer dragging long enough", () => {
|
|||||||
expect(element.points.length).toEqual(2);
|
expect(element.points.length).toEqual(2);
|
||||||
expect(element.points[0]).toEqual([0, 0]);
|
expect(element.points[0]).toEqual([0, 0]);
|
||||||
expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20)
|
expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20)
|
||||||
|
|
||||||
|
expect(h.elements.length).toMatchSnapshot();
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("line", () => {
|
it("line", () => {
|
||||||
@ -164,6 +178,8 @@ describe("add element to the scene when pointer dragging long enough", () => {
|
|||||||
expect(element.points.length).toEqual(2);
|
expect(element.points.length).toEqual(2);
|
||||||
expect(element.points[0]).toEqual([0, 0]);
|
expect(element.points[0]).toEqual([0, 0]);
|
||||||
expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20)
|
expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20)
|
||||||
|
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import ReactDOM from "react-dom";
|
|||||||
import { render, fireEvent } from "./test-utils";
|
import { render, fireEvent } from "./test-utils";
|
||||||
import { App } from "../components/App";
|
import { App } from "../components/App";
|
||||||
import * as Renderer from "../renderer/renderScene";
|
import * as Renderer from "../renderer/renderScene";
|
||||||
|
import { reseed } from "../random";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
@ -11,6 +12,7 @@ const renderScene = jest.spyOn(Renderer, "renderScene");
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderScene.mockClear();
|
||||||
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
@ -45,6 +47,8 @@ describe("move element", () => {
|
|||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([0, 40]);
|
expect([h.elements[0].x, h.elements[0].y]).toEqual([0, 40]);
|
||||||
|
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -81,5 +85,7 @@ describe("duplicate element on move when ALT is clicked", () => {
|
|||||||
// previous element should stay intact
|
// previous element should stay intact
|
||||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
|
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toEqual([0, 40]);
|
expect([h.elements[1].x, h.elements[1].y]).toEqual([0, 40]);
|
||||||
|
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -5,6 +5,7 @@ import { App } from "../components/App";
|
|||||||
import * as Renderer from "../renderer/renderScene";
|
import * as Renderer from "../renderer/renderScene";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { ExcalidrawLinearElement } from "../element/types";
|
import { ExcalidrawLinearElement } from "../element/types";
|
||||||
|
import { reseed } from "../random";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
@ -13,6 +14,7 @@ const renderScene = jest.spyOn(Renderer, "renderScene");
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderScene.mockClear();
|
||||||
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
@ -99,6 +101,8 @@ describe("multi point mode in linear elements", () => {
|
|||||||
[20, 30],
|
[20, 30],
|
||||||
[70, 110],
|
[70, 110],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("line", () => {
|
it("line", () => {
|
||||||
@ -138,5 +142,7 @@ describe("multi point mode in linear elements", () => {
|
|||||||
[20, 30],
|
[20, 30],
|
||||||
[70, 110],
|
[70, 110],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import { queries, buildQueries } from "@testing-library/react";
|
import { queries, buildQueries } from "@testing-library/react";
|
||||||
|
|
||||||
const _getAllByToolName = (container: HTMLElement, tool: string) => {
|
const toolMap = {
|
||||||
const toolMap: { [propKey: string]: string } = {
|
selection: "Selection — S, 1",
|
||||||
selection: "Selection — S, 1",
|
rectangle: "Rectangle — R, 2",
|
||||||
rectangle: "Rectangle — R, 2",
|
diamond: "Diamond — D, 3",
|
||||||
diamond: "Diamond — D, 3",
|
ellipse: "Ellipse — E, 4",
|
||||||
ellipse: "Ellipse — E, 4",
|
arrow: "Arrow — A, 5",
|
||||||
arrow: "Arrow — A, 5",
|
line: "Line — L, 6",
|
||||||
line: "Line — L, 6",
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const toolTitle = toolMap[tool as string];
|
export type ToolName = keyof typeof toolMap;
|
||||||
|
|
||||||
|
const _getAllByToolName = (container: HTMLElement, tool: string) => {
|
||||||
|
const toolTitle = toolMap[tool as ToolName];
|
||||||
return queries.getAllByTitle(container, toolTitle);
|
return queries.getAllByTitle(container, toolTitle);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
564
src/tests/regressionTests.test.tsx
Normal file
564
src/tests/regressionTests.test.tsx
Normal file
@ -0,0 +1,564 @@
|
|||||||
|
import { reseed } from "../random";
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import * as Renderer from "../renderer/renderScene";
|
||||||
|
import { render, fireEvent } from "./test-utils";
|
||||||
|
import { App } from "../components/App";
|
||||||
|
import { ToolName } from "./queries/toolQueries";
|
||||||
|
import { KEYS, Key } from "../keys";
|
||||||
|
import { setDateTimeForTests } from "../utils";
|
||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
import { handlerRectangles } from "../element";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
const renderScene = jest.spyOn(Renderer, "renderScene");
|
||||||
|
let getByToolName: (name: string) => HTMLElement = null!;
|
||||||
|
let canvas: HTMLCanvasElement = null!;
|
||||||
|
|
||||||
|
function clickTool(toolName: ToolName) {
|
||||||
|
fireEvent.click(getByToolName(toolName));
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastClientX = 0;
|
||||||
|
let lastClientY = 0;
|
||||||
|
let pointerType: "mouse" | "pen" | "touch" = "mouse";
|
||||||
|
|
||||||
|
function pointerDown(
|
||||||
|
clientX: number = lastClientX,
|
||||||
|
clientY: number = lastClientY,
|
||||||
|
altKey: boolean = false,
|
||||||
|
shiftKey: boolean = false,
|
||||||
|
) {
|
||||||
|
lastClientX = clientX;
|
||||||
|
lastClientY = clientY;
|
||||||
|
fireEvent.pointerDown(canvas, {
|
||||||
|
clientX,
|
||||||
|
clientY,
|
||||||
|
altKey,
|
||||||
|
shiftKey,
|
||||||
|
pointerId: 1,
|
||||||
|
pointerType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointer2Down(clientX: number, clientY: number) {
|
||||||
|
fireEvent.pointerDown(canvas, {
|
||||||
|
clientX,
|
||||||
|
clientY,
|
||||||
|
pointerId: 2,
|
||||||
|
pointerType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointer2Move(clientX: number, clientY: number) {
|
||||||
|
fireEvent.pointerMove(canvas, {
|
||||||
|
clientX,
|
||||||
|
clientY,
|
||||||
|
pointerId: 2,
|
||||||
|
pointerType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointer2Up(clientX: number, clientY: number) {
|
||||||
|
fireEvent.pointerUp(canvas, {
|
||||||
|
clientX,
|
||||||
|
clientY,
|
||||||
|
pointerId: 2,
|
||||||
|
pointerType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointerMove(
|
||||||
|
clientX: number = lastClientX,
|
||||||
|
clientY: number = lastClientY,
|
||||||
|
altKey: boolean = false,
|
||||||
|
shiftKey: boolean = false,
|
||||||
|
) {
|
||||||
|
lastClientX = clientX;
|
||||||
|
lastClientY = clientY;
|
||||||
|
fireEvent.pointerMove(canvas, {
|
||||||
|
clientX,
|
||||||
|
clientY,
|
||||||
|
altKey,
|
||||||
|
shiftKey,
|
||||||
|
pointerId: 1,
|
||||||
|
pointerType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointerUp(
|
||||||
|
clientX: number = lastClientX,
|
||||||
|
clientY: number = lastClientY,
|
||||||
|
altKey: boolean = false,
|
||||||
|
shiftKey: boolean = false,
|
||||||
|
) {
|
||||||
|
lastClientX = clientX;
|
||||||
|
lastClientY = clientY;
|
||||||
|
fireEvent.pointerUp(canvas, { pointerId: 1, pointerType, shiftKey, altKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
function hotkeyDown(key: Key) {
|
||||||
|
fireEvent.keyDown(document, { key: KEYS[key] });
|
||||||
|
}
|
||||||
|
|
||||||
|
function hotkeyUp(key: Key) {
|
||||||
|
fireEvent.keyUp(document, {
|
||||||
|
key: KEYS[key],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyDown(
|
||||||
|
key: string,
|
||||||
|
ctrlKey: boolean = false,
|
||||||
|
shiftKey: boolean = false,
|
||||||
|
) {
|
||||||
|
fireEvent.keyDown(document, { key, ctrlKey, shiftKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyUp(
|
||||||
|
key: string,
|
||||||
|
ctrlKey: boolean = false,
|
||||||
|
shiftKey: boolean = false,
|
||||||
|
) {
|
||||||
|
fireEvent.keyUp(document, {
|
||||||
|
key,
|
||||||
|
ctrlKey,
|
||||||
|
shiftKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hotkeyPress(key: Key) {
|
||||||
|
hotkeyDown(key);
|
||||||
|
hotkeyUp(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyPress(
|
||||||
|
key: string,
|
||||||
|
ctrlKey: boolean = false,
|
||||||
|
shiftKey: boolean = false,
|
||||||
|
) {
|
||||||
|
keyDown(key, ctrlKey, shiftKey);
|
||||||
|
keyUp(key, ctrlKey, shiftKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickLabeledElement(label: string) {
|
||||||
|
const element = document.querySelector(`[aria-label='${label}']`);
|
||||||
|
if (!element) {
|
||||||
|
throw new Error(`No labeled element found: ${label}`);
|
||||||
|
}
|
||||||
|
fireEvent.click(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedElement(): ExcalidrawElement {
|
||||||
|
const selectedElements = h.elements.filter(
|
||||||
|
(element) => h.state.selectedElementIds[element.id],
|
||||||
|
);
|
||||||
|
if (selectedElements.length !== 1) {
|
||||||
|
throw new Error(
|
||||||
|
`expected 1 selected element; got ${selectedElements.length}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return selectedElements[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResizeHandles() {
|
||||||
|
const rects = handlerRectangles(
|
||||||
|
getSelectedElement(),
|
||||||
|
h.state.zoom,
|
||||||
|
pointerType,
|
||||||
|
);
|
||||||
|
|
||||||
|
const rv: { [K in keyof typeof rects]: [number, number] } = {} as any;
|
||||||
|
|
||||||
|
for (const handlePos in rects) {
|
||||||
|
const [x, y, width, height] = rects[handlePos as keyof typeof rects];
|
||||||
|
|
||||||
|
rv[handlePos as keyof typeof rects] = [x + width / 2, y + height / 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is always called at the end of your test, so usually you don't need to call it.
|
||||||
|
* However, if you have a long test, you might want to call it during the test so it's easier
|
||||||
|
* to debug where a test failure came from.
|
||||||
|
*/
|
||||||
|
function checkpoint(name: string) {
|
||||||
|
expect(renderScene.mock.calls.length).toMatchSnapshot(
|
||||||
|
`[${name}] number of renders`,
|
||||||
|
);
|
||||||
|
expect(h.state).toMatchSnapshot(`[${name}] appState`);
|
||||||
|
expect(h.history.getSnapshotForTest()).toMatchSnapshot(`[${name}] history`);
|
||||||
|
expect(h.elements.length).toMatchSnapshot(`[${name}] number of elements`);
|
||||||
|
h.elements.forEach((element, i) =>
|
||||||
|
expect(element).toMatchSnapshot(`[${name}] element ${i}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Unmount ReactDOM from root
|
||||||
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
|
localStorage.clear();
|
||||||
|
renderScene.mockClear();
|
||||||
|
h.history.clear();
|
||||||
|
reseed(7);
|
||||||
|
setDateTimeForTests("201933152653");
|
||||||
|
pointerType = "mouse";
|
||||||
|
|
||||||
|
const renderResult = render(<App />);
|
||||||
|
|
||||||
|
getByToolName = renderResult.getByToolName;
|
||||||
|
canvas = renderResult.container.querySelector("canvas")!;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
checkpoint("end of test");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("regression tests", () => {
|
||||||
|
it("draw every type of shape", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(10, 10);
|
||||||
|
pointerMove(20, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
clickTool("diamond");
|
||||||
|
pointerDown(30, 10);
|
||||||
|
pointerMove(40, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
clickTool("ellipse");
|
||||||
|
pointerDown(50, 10);
|
||||||
|
pointerMove(60, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
clickTool("arrow");
|
||||||
|
pointerDown(70, 10);
|
||||||
|
pointerMove(80, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
clickTool("line");
|
||||||
|
pointerDown(90, 10);
|
||||||
|
pointerMove(100, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
clickTool("arrow");
|
||||||
|
pointerDown(10, 30);
|
||||||
|
pointerUp();
|
||||||
|
pointerMove(20, 40);
|
||||||
|
pointerUp();
|
||||||
|
pointerMove(10, 50);
|
||||||
|
pointerUp();
|
||||||
|
hotkeyPress("ENTER");
|
||||||
|
|
||||||
|
clickTool("line");
|
||||||
|
pointerDown(30, 30);
|
||||||
|
pointerUp();
|
||||||
|
pointerMove(40, 40);
|
||||||
|
pointerUp();
|
||||||
|
pointerMove(30, 50);
|
||||||
|
pointerUp();
|
||||||
|
hotkeyPress("ENTER");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("click to select a shape", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(10, 10);
|
||||||
|
pointerMove(20, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(30, 10);
|
||||||
|
pointerMove(40, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
const prevSelectedId = getSelectedElement().id;
|
||||||
|
pointerDown(10, 10);
|
||||||
|
pointerUp();
|
||||||
|
expect(getSelectedElement().id).not.toEqual(prevSelectedId);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [keys, shape] of [
|
||||||
|
["2r", "rectangle"],
|
||||||
|
["3d", "diamond"],
|
||||||
|
["4e", "ellipse"],
|
||||||
|
["5a", "arrow"],
|
||||||
|
["6l", "line"],
|
||||||
|
] as [string, ExcalidrawElement["type"]][]) {
|
||||||
|
for (const key of keys) {
|
||||||
|
it(`hotkey ${key} selects ${shape} tool`, () => {
|
||||||
|
keyPress(key);
|
||||||
|
|
||||||
|
pointerDown(10, 10);
|
||||||
|
pointerMove(20, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
expect(getSelectedElement().type).toBe(shape);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("change the properties of a shape", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(10, 10);
|
||||||
|
pointerMove(20, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
clickLabeledElement("Background");
|
||||||
|
clickLabeledElement("#fa5252");
|
||||||
|
clickLabeledElement("Stroke");
|
||||||
|
clickLabeledElement("#5f3dc4");
|
||||||
|
expect(getSelectedElement().backgroundColor).toBe("#fa5252");
|
||||||
|
expect(getSelectedElement().strokeColor).toBe("#5f3dc4");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resize an element, trying every resize handle", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(10, 10);
|
||||||
|
pointerMove(20, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
const resizeHandles = getResizeHandles();
|
||||||
|
for (const handlePos in resizeHandles) {
|
||||||
|
const [x, y] = resizeHandles[handlePos as keyof typeof resizeHandles];
|
||||||
|
const { width: prevWidth, height: prevHeight } = getSelectedElement();
|
||||||
|
pointerDown(x, y);
|
||||||
|
pointerMove(x - 5, y - 5);
|
||||||
|
pointerUp();
|
||||||
|
const {
|
||||||
|
width: nextWidthNegative,
|
||||||
|
height: nextHeightNegative,
|
||||||
|
} = getSelectedElement();
|
||||||
|
expect(
|
||||||
|
prevWidth !== nextWidthNegative || prevHeight !== nextHeightNegative,
|
||||||
|
).toBeTruthy();
|
||||||
|
checkpoint(`resize handle ${handlePos} (-5, -5)`);
|
||||||
|
|
||||||
|
pointerDown();
|
||||||
|
pointerMove(x, y);
|
||||||
|
pointerUp();
|
||||||
|
const { width, height } = getSelectedElement();
|
||||||
|
expect(width).toBe(prevWidth);
|
||||||
|
expect(height).toBe(prevHeight);
|
||||||
|
checkpoint(`unresize handle ${handlePos} (-5, -5)`);
|
||||||
|
|
||||||
|
pointerDown(x, y);
|
||||||
|
pointerMove(x + 5, y + 5);
|
||||||
|
pointerUp();
|
||||||
|
const {
|
||||||
|
width: nextWidthPositive,
|
||||||
|
height: nextHeightPositive,
|
||||||
|
} = getSelectedElement();
|
||||||
|
expect(
|
||||||
|
prevWidth !== nextWidthPositive || prevHeight !== nextHeightPositive,
|
||||||
|
).toBeTruthy();
|
||||||
|
checkpoint(`resize handle ${handlePos} (+5, +5)`);
|
||||||
|
|
||||||
|
pointerDown();
|
||||||
|
pointerMove(x, y);
|
||||||
|
pointerUp();
|
||||||
|
const { width: finalWidth, height: finalHeight } = getSelectedElement();
|
||||||
|
expect(finalWidth).toBe(prevWidth);
|
||||||
|
expect(finalHeight).toBe(prevHeight);
|
||||||
|
|
||||||
|
checkpoint(`unresize handle ${handlePos} (+5, +5)`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("click on an element and drag it", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(10, 10);
|
||||||
|
pointerMove(20, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
const { x: prevX, y: prevY } = getSelectedElement();
|
||||||
|
pointerDown(10, 10);
|
||||||
|
pointerMove(20, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
const { x: nextX, y: nextY } = getSelectedElement();
|
||||||
|
expect(nextX).toBeGreaterThan(prevX);
|
||||||
|
expect(nextY).toBeGreaterThan(prevY);
|
||||||
|
|
||||||
|
checkpoint("dragged");
|
||||||
|
|
||||||
|
pointerDown();
|
||||||
|
pointerMove(10, 10);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
const { x, y } = getSelectedElement();
|
||||||
|
expect(x).toBe(prevX);
|
||||||
|
expect(y).toBe(prevY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("alt-drag duplicates an element", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(10, 10);
|
||||||
|
pointerMove(20, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
h.elements.filter((element) => element.type === "rectangle").length,
|
||||||
|
).toBe(1);
|
||||||
|
pointerDown(10, 10, true);
|
||||||
|
pointerMove(20, 20, true);
|
||||||
|
pointerUp(20, 20, true);
|
||||||
|
expect(
|
||||||
|
h.elements.filter((element) => element.type === "rectangle").length,
|
||||||
|
).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("click-drag to select a group", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(10, 10);
|
||||||
|
pointerMove(20, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(30, 10);
|
||||||
|
pointerMove(40, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(50, 10);
|
||||||
|
pointerMove(60, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
pointerDown(0, 0);
|
||||||
|
pointerMove(45, 25);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
h.elements.filter((element) => h.state.selectedElementIds[element.id])
|
||||||
|
.length,
|
||||||
|
).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shift-click to select a group, then drag", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(10, 10);
|
||||||
|
pointerMove(20, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(30, 10);
|
||||||
|
pointerMove(40, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
const prevRectsXY = h.elements
|
||||||
|
.filter((element) => element.type === "rectangle")
|
||||||
|
.map((element) => ({ x: element.x, y: element.y }));
|
||||||
|
pointerDown(10, 10);
|
||||||
|
pointerUp();
|
||||||
|
pointerDown(30, 10, false, true);
|
||||||
|
pointerUp();
|
||||||
|
pointerDown(30, 10);
|
||||||
|
pointerMove(40, 20);
|
||||||
|
pointerUp();
|
||||||
|
h.elements
|
||||||
|
.filter((element) => element.type === "rectangle")
|
||||||
|
.forEach((element, i) => {
|
||||||
|
expect(element.x).toBeGreaterThan(prevRectsXY[i].x);
|
||||||
|
expect(element.y).toBeGreaterThan(prevRectsXY[i].y);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pinch-to-zoom works", () => {
|
||||||
|
expect(h.state.zoom).toBe(1);
|
||||||
|
pointerType = "touch";
|
||||||
|
pointerDown(50, 50);
|
||||||
|
pointer2Down(60, 50);
|
||||||
|
pointerMove(40, 50);
|
||||||
|
pointer2Move(60, 50);
|
||||||
|
expect(h.state.zoom).toBeGreaterThan(1);
|
||||||
|
const zoomed = h.state.zoom;
|
||||||
|
pointerMove(45, 50);
|
||||||
|
pointer2Move(55, 50);
|
||||||
|
expect(h.state.zoom).toBeLessThan(zoomed);
|
||||||
|
pointerUp(45, 50);
|
||||||
|
pointer2Up(55, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("two-finger scroll works", () => {
|
||||||
|
const startScrollY = h.state.scrollY;
|
||||||
|
pointerDown(50, 50);
|
||||||
|
pointer2Down(60, 50);
|
||||||
|
pointerMove(50, 40);
|
||||||
|
pointer2Move(60, 40);
|
||||||
|
pointerUp(50, 40);
|
||||||
|
pointer2Up(60, 40);
|
||||||
|
expect(h.state.scrollY).toBeLessThan(startScrollY);
|
||||||
|
|
||||||
|
const startScrollX = h.state.scrollX;
|
||||||
|
pointerDown(50, 50);
|
||||||
|
pointer2Down(50, 60);
|
||||||
|
pointerMove(60, 50);
|
||||||
|
pointer2Move(60, 60);
|
||||||
|
pointerUp(60, 50);
|
||||||
|
pointer2Up(60, 60);
|
||||||
|
expect(h.state.scrollX).toBeGreaterThan(startScrollX);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("spacebar + drag scrolls the canvas", () => {
|
||||||
|
const { scrollX: startScrollX, scrollY: startScrollY } = h.state;
|
||||||
|
hotkeyDown("SPACE");
|
||||||
|
pointerDown(50, 50);
|
||||||
|
pointerMove(60, 60);
|
||||||
|
pointerUp();
|
||||||
|
hotkeyUp("SPACE");
|
||||||
|
const { scrollX, scrollY } = h.state;
|
||||||
|
expect(scrollX).not.toEqual(startScrollX);
|
||||||
|
expect(scrollY).not.toEqual(startScrollY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("arrow keys", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(10, 10);
|
||||||
|
pointerMove(20, 20);
|
||||||
|
pointerUp();
|
||||||
|
hotkeyPress("ARROW_LEFT");
|
||||||
|
hotkeyPress("ARROW_LEFT");
|
||||||
|
hotkeyPress("ARROW_RIGHT");
|
||||||
|
hotkeyPress("ARROW_UP");
|
||||||
|
hotkeyPress("ARROW_UP");
|
||||||
|
hotkeyPress("ARROW_DOWN");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("undo/redo drawing an element", () => {
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(10, 10);
|
||||||
|
pointerMove(20, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(30, 10);
|
||||||
|
pointerMove(40, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
clickTool("rectangle");
|
||||||
|
pointerDown(50, 10);
|
||||||
|
pointerMove(60, 20);
|
||||||
|
pointerUp();
|
||||||
|
|
||||||
|
expect(h.elements.filter((element) => !element.isDeleted).length).toBe(3);
|
||||||
|
keyPress("z", true);
|
||||||
|
expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
|
||||||
|
keyPress("z", true);
|
||||||
|
expect(h.elements.filter((element) => !element.isDeleted).length).toBe(1);
|
||||||
|
keyPress("z", true, true);
|
||||||
|
expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("zoom hotkeys", () => {
|
||||||
|
expect(h.state.zoom).toBe(1);
|
||||||
|
fireEvent.keyDown(document, { code: "Equal", ctrlKey: true });
|
||||||
|
fireEvent.keyUp(document, { code: "Equal", ctrlKey: true });
|
||||||
|
expect(h.state.zoom).toBeGreaterThan(1);
|
||||||
|
fireEvent.keyDown(document, { code: "Minus", ctrlKey: true });
|
||||||
|
fireEvent.keyUp(document, { code: "Minus", ctrlKey: true });
|
||||||
|
expect(h.state.zoom).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
@ -3,6 +3,7 @@ import ReactDOM from "react-dom";
|
|||||||
import { render, fireEvent } from "./test-utils";
|
import { render, fireEvent } from "./test-utils";
|
||||||
import { App } from "../components/App";
|
import { App } from "../components/App";
|
||||||
import * as Renderer from "../renderer/renderScene";
|
import * as Renderer from "../renderer/renderScene";
|
||||||
|
import { reseed } from "../random";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
@ -11,6 +12,7 @@ const renderScene = jest.spyOn(Renderer, "renderScene");
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderScene.mockClear();
|
||||||
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
@ -53,6 +55,8 @@ describe("resize element", () => {
|
|||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([29, 47]);
|
expect([h.elements[0].x, h.elements[0].y]).toEqual([29, 47]);
|
||||||
expect([h.elements[0].width, h.elements[0].height]).toEqual([30, 50]);
|
expect([h.elements[0].width, h.elements[0].height]).toEqual([30, 50]);
|
||||||
|
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -94,5 +98,7 @@ describe("resize element with aspect ratio when SHIFT is clicked", () => {
|
|||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([29, 47]);
|
expect([h.elements[0].x, h.elements[0].y]).toEqual([29, 47]);
|
||||||
expect([h.elements[0].width, h.elements[0].height]).toEqual([30, 50]);
|
expect([h.elements[0].width, h.elements[0].height]).toEqual([30, 50]);
|
||||||
|
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -4,6 +4,7 @@ import { render, fireEvent } from "./test-utils";
|
|||||||
import { App } from "../components/App";
|
import { App } from "../components/App";
|
||||||
import * as Renderer from "../renderer/renderScene";
|
import * as Renderer from "../renderer/renderScene";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
import { reseed } from "../random";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
@ -12,6 +13,7 @@ const renderScene = jest.spyOn(Renderer, "renderScene");
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderScene.mockClear();
|
||||||
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
@ -98,6 +100,8 @@ describe("select single element on the scene", () => {
|
|||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
|
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("diamond", () => {
|
it("diamond", () => {
|
||||||
@ -123,6 +127,8 @@ describe("select single element on the scene", () => {
|
|||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
|
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ellipse", () => {
|
it("ellipse", () => {
|
||||||
@ -148,6 +154,8 @@ describe("select single element on the scene", () => {
|
|||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
|
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("arrow", () => {
|
it("arrow", () => {
|
||||||
@ -186,6 +194,7 @@ describe("select single element on the scene", () => {
|
|||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("arrow escape", () => {
|
it("arrow escape", () => {
|
||||||
@ -224,5 +233,7 @@ describe("select single element on the scene", () => {
|
|||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
|
|
||||||
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
10
src/utils.ts
10
src/utils.ts
@ -3,7 +3,17 @@ import { getZoomOrigin } from "./scene";
|
|||||||
|
|
||||||
export const SVG_NS = "http://www.w3.org/2000/svg";
|
export const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
|
let mockDateTime: string | null = null;
|
||||||
|
|
||||||
|
export function setDateTimeForTests(dateTime: string) {
|
||||||
|
mockDateTime = dateTime;
|
||||||
|
}
|
||||||
|
|
||||||
export function getDateTime() {
|
export function getDateTime() {
|
||||||
|
if (mockDateTime) {
|
||||||
|
return mockDateTime;
|
||||||
|
}
|
||||||
|
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = date.getMonth() + 1;
|
const month = date.getMonth() + 1;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user