Adds shape tests (some stubs), fixes bugs

This commit is contained in:
Steve Ruiz 2021-07-13 20:34:43 +01:00
parent 322ce81fb6
commit 70c8c84790
15 changed files with 550 additions and 43 deletions

View file

@ -1,17 +1,61 @@
import { ArrowShape, ShapeType } from 'types'
import TestState from '../test-utils' import TestState from '../test-utils'
describe('arrow shape', () => { describe('arrow shape', () => {
const tt = new TestState() const tt = new TestState()
tt.resetDocumentState().send('SELECTED_ARROW_TOOL').save() tt.resetDocumentState().save()
it('creates shape', () => { describe('creating arrows', () => {
// TODO it('creates shape', () => {
null tt.reset().restore().send('SELECTED_ARROW_TOOL')
})
it('cancels shape while creating', () => { expect(tt.state.isIn('arrow.creating')).toBe(true)
// TODO
null tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
const id = tt.getSortedPageShapeIds()[0]
const shape = tt.getShape<ArrowShape>(id)
tt.assertShapeType(id, ShapeType.Arrow)
expect(shape.handles.start.point).toEqual([0, 0])
expect(shape.handles.bend.point).toEqual([50.5, 50.5])
expect(shape.handles.end.point).toEqual([101, 101])
})
it('creates shapes when pointing a shape', () => {
tt.reset().restore().send('SELECTED_ARROW_TOOL').send('TOGGLED_TOOL_LOCK')
tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
tt.startClick('canvas').movePointerBy([-200, 100]).stopClick('canvas')
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
})
it('creates shapes when shape locked', () => {
tt.reset()
.restore()
.createShape(
{
type: ShapeType.Rectangle,
point: [0, 0],
size: [100, 100],
childIndex: 1,
},
'rect1'
)
.send('SELECTED_ARROW_TOOL')
tt.startClick('rect1').movePointerBy([100, 100]).stopClick('canvas')
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
})
it('cancels shape while creating', () => {
// TODO
null
})
}) })
it('moves shape', () => { it('moves shape', () => {

View file

@ -0,0 +1,101 @@
import { ShapeType } from 'types'
import TestState from '../test-utils'
describe('draw shape', () => {
const tt = new TestState()
tt.resetDocumentState().save()
describe('creating draws', () => {
it('creates shape', () => {
tt.reset().restore().send('SELECTED_DRAW_TOOL')
expect(tt.state.isIn('draw.creating')).toBe(true)
tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
const id = tt.getSortedPageShapeIds()[0]
tt.assertShapeType(id, ShapeType.Draw)
})
it('creates shapes when pointing a shape', () => {
tt.reset().restore().send('SELECTED_DRAW_TOOL').send('TOGGLED_TOOL_LOCK')
tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
tt.startClick('canvas').movePointerBy([-200, 100]).stopClick('canvas')
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
})
it('creates shapes when shape locked', () => {
tt.reset()
.restore()
.createShape(
{
type: ShapeType.Rectangle,
point: [0, 0],
size: [100, 100],
childIndex: 1,
},
'rect1'
)
.send('SELECTED_DRAW_TOOL')
tt.startClick('rect1').movePointerBy([100, 100]).stopClick('canvas')
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
})
it('cancels shape while creating', () => {
// TODO
null
})
})
it('moves shape', () => {
// TODO
null
})
it('rotates shape', () => {
// TODO
null
})
it('rotates shape in a group', () => {
// TODO
null
})
it('measures shape bounds', () => {
// TODO
null
})
it('measures shape rotated bounds', () => {
// TODO
null
})
it('transforms single shape', () => {
// TODO
null
})
it('transforms in a group', () => {
// TODO
null
})
/* -------------------- Specific -------------------- */
it('closes the shape when the start and end points are near enough', () => {
// TODO
null
})
it('remains closed after resizing up', () => {
// TODO
null
})
})

View file

@ -0,0 +1,104 @@
import { ShapeType } from 'types'
import TestState from '../test-utils'
describe('ellipse shape', () => {
const tt = new TestState()
tt.resetDocumentState().save()
describe('creating ellipses', () => {
it('creates shape', () => {
tt.reset().restore().send('SELECTED_ELLIPSE_TOOL')
expect(tt.state.isIn('ellipse.creating')).toBe(true)
tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
const id = tt.getSortedPageShapeIds()[0]
tt.assertShapeType(id, ShapeType.Ellipse)
})
it('creates shapes when pointing a shape', () => {
tt.reset()
.restore()
.send('SELECTED_ELLIPSE_TOOL')
.send('TOGGLED_TOOL_LOCK')
tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
tt.startClick('canvas').movePointerBy([-200, 100]).stopClick('canvas')
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
})
it('creates shapes when shape locked', () => {
tt.reset()
.restore()
.createShape(
{
type: ShapeType.Rectangle,
point: [0, 0],
size: [100, 100],
childIndex: 1,
},
'rect1'
)
.send('SELECTED_ELLIPSE_TOOL')
tt.startClick('rect1').movePointerBy([100, 100]).stopClick('canvas')
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
})
it('cancels shape while creating', () => {
// TODO
null
})
})
it('moves shape', () => {
// TODO
null
})
it('rotates shape', () => {
// TODO
null
})
it('rotates shape in a group', () => {
// TODO
null
})
it('measures shape bounds', () => {
// TODO
null
})
it('measures shape rotated bounds', () => {
// TODO
null
})
it('transforms single shape', () => {
// TODO
null
})
it('transforms in a group', () => {
// TODO
null
})
/* -------------------- Specific -------------------- */
it('creates aspect-ratio-locked shape with shift key', () => {
// TODO
null
})
it('resizes aspect-ratio-locked shape with shift key', () => {
// TODO
null
})
})

View file

@ -0,0 +1,104 @@
import { ShapeType } from 'types'
import TestState from '../test-utils'
describe('rectangle shape', () => {
const tt = new TestState()
tt.resetDocumentState().save()
describe('creating rectangles', () => {
it('creates shape', () => {
tt.reset().restore().send('SELECTED_RECTANGLE_TOOL')
expect(tt.state.isIn('rectangle.creating')).toBe(true)
tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
const id = tt.getSortedPageShapeIds()[0]
tt.assertShapeType(id, ShapeType.Rectangle)
})
it('creates shapes when pointing a shape', () => {
tt.reset()
.restore()
.send('SELECTED_RECTANGLE_TOOL')
.send('TOGGLED_TOOL_LOCK')
tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
tt.startClick('canvas').movePointerBy([-200, 100]).stopClick('canvas')
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
})
it('creates shapes when shape locked', () => {
tt.reset()
.restore()
.createShape(
{
type: ShapeType.Rectangle,
point: [0, 0],
size: [100, 100],
childIndex: 1,
},
'rect1'
)
.send('SELECTED_RECTANGLE_TOOL')
tt.startClick('rect1').movePointerBy([100, 100]).stopClick('canvas')
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
})
it('cancels shape while creating', () => {
// TODO
null
})
})
it('moves shape', () => {
// TODO
null
})
it('rotates shape', () => {
// TODO
null
})
it('rotates shape in a group', () => {
// TODO
null
})
it('measures shape bounds', () => {
// TODO
null
})
it('measures shape rotated bounds', () => {
// TODO
null
})
it('transforms single shape', () => {
// TODO
null
})
it('transforms in a group', () => {
// TODO
null
})
/* -------------------- Specific -------------------- */
it('creates aspect-ratio-locked shape with shift key', () => {
// TODO
null
})
it('resizes aspect-ratio-locked shape with shift key', () => {
// TODO
null
})
})

View file

@ -0,0 +1,79 @@
import TestState from '../test-utils'
describe('arrow shape', () => {
const tt = new TestState()
tt.resetDocumentState()
it('creates shape', () => {
tt.send('SELECTED_TEXT_TOOL')
expect(tt.state.isIn('text.creating')).toBe(true)
const id = tt.getSortedPageShapeIds()[0]
tt.clickCanvas()
expect(tt.state.isIn('editingShape')).toBe(true)
tt.send('EDITED_SHAPE', {
id,
change: { text: 'Hello world' },
})
tt.send('BLURRED_EDITING_SHAPE', { id: id })
expect(tt.state.isIn('selecting')).toBe(true)
})
it('cancels shape while creating', () => {
// TODO
null
})
it('moves shape', () => {
// TODO
null
})
it('rotates shape', () => {
// TODO
null
})
it('rotates shape in a group', () => {
// TODO
null
})
it('measures shape bounds', () => {
// TODO
null
})
it('measures shape rotated bounds', () => {
// TODO
null
})
it('transforms single shape', () => {
// TODO
null
})
it('transforms in a group', () => {
// TODO
null
})
/* -------------------- Specific -------------------- */
it('scales', () => {
// TODO
null
})
it('selects different text on tap while editing', () => {
// TODO
null
})
})

View file

@ -106,7 +106,7 @@ class TestState {
*/ */
getSortedPageShapeIds(): string[] { getSortedPageShapeIds(): string[] {
return Object.values( return Object.values(
this.data.document.pages[this.data.currentParentId].shapes this.data.document.pages[this.data.currentPageId].shapes
) )
.sort((a, b) => a.childIndex - b.childIndex) .sort((a, b) => a.childIndex - b.childIndex)
.map((shape) => shape.id) .map((shape) => shape.id)
@ -257,6 +257,14 @@ class TestState {
const shape = tld.getShape(this.data, id) const shape = tld.getShape(this.data, id)
const [x, y] = shape ? vec.add(shape.point, [1, 1]) : [0, 0] const [x, y] = shape ? vec.add(shape.point, [1, 1]) : [0, 0]
if (id === 'canvas') {
this.state.send(
'POINTED_CANVAS',
inputs.pointerDown(TestState.point({ x, y, ...options }), id)
)
return this
}
this.state.send( this.state.send(
'POINTED_SHAPE', 'POINTED_SHAPE',
inputs.pointerDown(TestState.point({ x, y, ...options }), id) inputs.pointerDown(TestState.point({ x, y, ...options }), id)

38
__tests__/tools.test.ts Normal file
View file

@ -0,0 +1,38 @@
import TestState from './test-utils'
const TOOLS = [
'draw',
'rectangle',
'ellipse',
'arrow',
'text',
'line',
'ray',
'dot',
]
describe('when selecting tools', () => {
const tt = new TestState()
TOOLS.forEach((tool) => {
it(`selects ${tool} tool`, () => {
tt.reset().send(`SELECTED_${tool.toUpperCase()}_TOOL`)
expect(tt.data.activeTool).toBe(tool)
expect(tt.state.isIn(tool)).toBe(true)
})
TOOLS.forEach((otherTool) => {
if (otherTool === tool) return
it(`selects ${tool} tool from ${otherTool} tool`, () => {
tt.reset()
.send(`SELECTED_${tool.toUpperCase()}_TOOL`)
.send(`SELECTED_${otherTool.toUpperCase()}_TOOL`)
expect(tt.data.activeTool).toBe(otherTool)
expect(tt.state.isIn(otherTool)).toBe(true)
})
})
})
})

View file

@ -1,9 +0,0 @@
// This is the code library.
export default `
class Circle {
greet(): string {
return "Hello!"
}
}
`

View file

@ -1,8 +0,0 @@
export default `new Circle({
point: [200, 200],
})
new Rectangle({
point: [400, 300],
})
`

View file

@ -62,6 +62,8 @@ enum FontSize {
ExtraLarge = 'ExtraLarge', ExtraLarge = 'ExtraLarge',
} }
type Theme = 'dark' | 'light'
type ShapeStyles = { type ShapeStyles = {
color: ColorStyle color: ColorStyle
size: SizeStyle size: SizeStyle
@ -214,6 +216,16 @@ interface CodeResult {
error: CodeError error: CodeError
} }
interface ShapeTreeNode {
shape: Shape
children: ShapeTreeNode[]
isEditing: boolean
isHovered: boolean
isSelected: boolean
isDarkMode: boolean
isCurrentParent: boolean
}
/* -------------------------------------------------- */ /* -------------------------------------------------- */
/* Editor UI */ /* Editor UI */
/* -------------------------------------------------- */ /* -------------------------------------------------- */
@ -545,8 +557,12 @@ interface ShapeUtility<K extends Shape> {
render( render(
this: ShapeUtility<K>, this: ShapeUtility<K>,
shape: K, shape: K,
info: { info?: {
isEditing: boolean isEditing?: boolean
isHovered?: boolean
isSelected?: boolean
isCurrentParent?: boolean
isDarkMode?: boolean
ref?: React.MutableRefObject<HTMLTextAreaElement> ref?: React.MutableRefObject<HTMLTextAreaElement>
} }
): JSX.Element ): JSX.Element
@ -636,6 +652,8 @@ enum FontSize {
ExtraLarge = 'ExtraLarge', ExtraLarge = 'ExtraLarge',
} }
type Theme = 'dark' | 'light'
type ShapeStyles = { type ShapeStyles = {
color: ColorStyle color: ColorStyle
size: SizeStyle size: SizeStyle
@ -788,6 +806,16 @@ interface CodeResult {
error: CodeError error: CodeError
} }
interface ShapeTreeNode {
shape: Shape
children: ShapeTreeNode[]
isEditing: boolean
isHovered: boolean
isSelected: boolean
isDarkMode: boolean
isCurrentParent: boolean
}
/* -------------------------------------------------- */ /* -------------------------------------------------- */
/* Editor UI */ /* Editor UI */
/* -------------------------------------------------- */ /* -------------------------------------------------- */
@ -1119,8 +1147,12 @@ interface ShapeUtility<K extends Shape> {
render( render(
this: ShapeUtility<K>, this: ShapeUtility<K>,
shape: K, shape: K,
info: { info?: {
isEditing: boolean isEditing?: boolean
isHovered?: boolean
isSelected?: boolean
isCurrentParent?: boolean
isDarkMode?: boolean
ref?: React.MutableRefObject<HTMLTextAreaElement> ref?: React.MutableRefObject<HTMLTextAreaElement>
} }
): JSX.Element ): JSX.Element
@ -1840,6 +1872,14 @@ type RequiredKeys<T> = {
return Array.from(new Set(items).values()) return Array.from(new Set(items).values())
} }
/**
* Convert a set to an array.
* @param set
*/
static setToArray<T>(set: Set<T>): T[] {
return Array.from(set.values())
}
/** /**
* Get the outer of between a circle and a point. * Get the outer of between a circle and a point.
* @param C The circle's center. * @param C The circle's center.

View file

@ -9,10 +9,13 @@ export default function StatusBar(): JSX.Element {
const shapesInView = state.values.shapesToRender.length const shapesInView = state.values.shapesToRender.length
const active = local.active.slice(1).map((s) => { const active = local.active
const states = s.split('.') .slice(1)
return states[states.length - 1] .map((s) => {
}) const states = s.split('.')
return states[states.length - 1]
})
.join(' | ')
const log = local.log[0] const log = local.log[0]
@ -21,7 +24,7 @@ export default function StatusBar(): JSX.Element {
return ( return (
<StatusBarContainer size={size}> <StatusBarContainer size={size}>
<Section> <Section>
{active.join(' | ')} - {log} {active} - {log}
</Section> </Section>
<Section>{shapesInView || '0'} Shapes</Section> <Section>{shapesInView || '0'} Shapes</Section>
</StatusBarContainer> </StatusBarContainer>

View file

@ -21,7 +21,7 @@ export default function IsFilledPicker(): JSX.Element {
return ( return (
selectedShapes.length === 0 || selectedShapes.length === 0 ||
selectedShapes.every((shape) => getShapeUtils(shape).canStyleFill) selectedShapes.some((shape) => getShapeUtils(shape).canStyleFill)
) )
}) })

View file

@ -135,7 +135,7 @@ export function PrimaryButton({
children, children,
}: PrimaryToolButtonProps): JSX.Element { }: PrimaryToolButtonProps): JSX.Element {
return ( return (
<Tooltip label={label}> <Tooltip label={label[0].toUpperCase() + label.slice(1)}>
<PrimaryToolButton <PrimaryToolButton
name={label} name={label}
bp={{ bp={{

View file

@ -1,4 +1,4 @@
import isMobile from 'ismobilejs' import { isMobile } from 'utils'
import { useEffect } from 'react' import { useEffect } from 'react'
import state from 'state' import state from 'state'
@ -11,7 +11,7 @@ function handleFocusOut() {
export default function useSafariFocusOutFix(): void { export default function useSafariFocusOutFix(): void {
useEffect(() => { useEffect(() => {
if (isMobile().apple) { if (isMobile()) {
document.addEventListener('focusout', handleFocusOut) document.addEventListener('focusout', handleFocusOut)
return () => { return () => {

View file

@ -93,8 +93,8 @@ const initialData: Data = {
const draw = new Draw({ const draw = new Draw({
points: [ points: [
...Utils.getPointsBetween([0, 0], [20, 50]), ...Utils.getPointsBetween([0, 0], [20, 50]),
...Utils.getPointsBetween([20, 50], [100, 20], 3), ...Utils.getPointsBetween([20, 50], [100, 20], { steps: 3 }),
...Utils.getPointsBetween([100, 20], [100, 100], 10), ...Utils.getPointsBetween([100, 20], [100, 100], { steps: 10 }),
[100, 100], [100, 100],
], ],
}) })
@ -884,9 +884,9 @@ const state = createState({
}, },
arrow: { arrow: {
onEnter: 'setActiveToolArrow', onEnter: 'setActiveToolArrow',
initial: 'idle', initial: 'creating',
states: { states: {
idle: { creating: {
on: { on: {
CANCELLED: { to: 'selecting' }, CANCELLED: { to: 'selecting' },
POINTED_SHAPE: { POINTED_SHAPE: {
@ -937,6 +937,9 @@ const state = createState({
POINTED_CANVAS: { POINTED_CANVAS: {
to: 'ellipse.editing', to: 'ellipse.editing',
}, },
POINTED_SHAPE: {
to: 'ellipse.editing',
},
}, },
}, },
editing: { editing: {