Skip to content

Commit

Permalink
test(portable-text-editor): refactor collab test to reliably wait for…
Browse files Browse the repository at this point in the history
… selection changes
  • Loading branch information
skogsmaskin committed Sep 8, 2021
1 parent 09ecadc commit 56cc8a4
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('selection adjustment', () => {
await editorA.pressKey('ArrowRight', 2)
let selectionA = await editorA.getSelection()
expect(selectionA).toEqual(expectedSelection)
await editorB.insertNewLine()
await editorB.pressKey('Enter')
selectionA = await editorA.getSelection()
expect(selectionA).toEqual(expectedSelection)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ describe('collaborate editing', () => {
anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11},
focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11},
})
expect(selectionB).toEqual(null)
expect(selectionB).toEqual({
anchor: {offset: 0, path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}]},
focus: {offset: 0, path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}]},
})
})

it('will update value in editor A when editor B writes something', async () => {
Expand Down Expand Up @@ -145,7 +148,7 @@ describe('collaborate editing', () => {
focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 18},
})
await editorA.setSelection(desiredSelectionA)
await editorB.insertNewLine()
await editorB.pressKey('Enter')
const valA = await editorA.getValue()
const valB = await editorB.getValue()
expect(valA).toEqual(valB)
Expand Down Expand Up @@ -372,7 +375,7 @@ describe('collaborate editing', () => {
anchor: {offset: 17, path: [{_key: 'B-3'}, 'children', {_key: 'B-2'}]},
focus: {offset: 17, path: [{_key: 'B-3'}, 'children', {_key: 'B-2'}]},
})
await editorA.insertNewLine()
await editorA.pressKey('Enter')
await editorA.insertText('A new line appears')
const valA = await editorA.getValue()
const valB = await editorB.getValue()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
jest.setTimeout(10 * 1000)
jest.setTimeout(20 * 1000)

// beforeAll(() => {
// console.log('beforeAll')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ ipc.config.silent = true

const WEB_SERVER_ROOT_URL = 'http://localhost:3000'

const SELECTION_EVENT_DELAY_MS = 50

// Forward debug info from the PTE in the browsers
// const DEBUG = 'sanity-pte:*'
const DEBUG = false
const DEBUG = process.env.DEBUG || false

let testId: string

Expand Down Expand Up @@ -109,11 +107,29 @@ export default class CollaborationEnvironment extends NodeEnvironment {
[this._pageA, this._pageB].map(async (page, index) => {
const editorId = ['A', 'B'][index]
const editableHandle = await page.waitForSelector('div[contentEditable="true"]')
const selectionHandle: puppeteer.ElementHandle<HTMLDivElement> = await page.waitForSelector(
'#pte-selection'
)
const waitForRevision = async () => {
const revId = (Math.random() + 1).toString(36).substring(7)
ipc.of.socketServer.emit('payload', JSON.stringify({type: 'revId', revId, testId}))
ipc.of.socketServer.emit(
'payload',
JSON.stringify({type: 'revId', revId, testId, editorId})
)
await page.waitForSelector(`code[data-rev-id="${revId}"]`)
}
const getSelection = async (): Promise<EditorSelection | null> => {
const selection = await selectionHandle.evaluate((node) =>
node.innerText ? JSON.parse(node.innerText) : null
)
return selection
}
const waitForSelection = async (selectionChangeFn: () => Promise<void>) => {
const selection = await getSelection()
await selectionChangeFn()
const dataVal = selection ? JSON.stringify(selection) : 'null'
await page.waitForSelector(`code[data-selection]:not([data-selection='${dataVal}'])`)
}
return {
testId,
editorId,
Expand All @@ -133,25 +149,43 @@ export default class CollaborationEnvironment extends NodeEnvironment {
)
await waitForRevision()
},
insertNewLine: async () => {
await editableHandle.press('Enter')
await waitForRevision()
},
pressKey: async (keyName: string, times?: number) => {
const pressKey = () => editableHandle.press(keyName)
for (let i = 0; i < (times || 1); i++) {
await editableHandle.press(keyName)
}
if (keyName.length === 1 || keyName === 'Backspace' || keyName === 'Delete') {
await waitForRevision()
} else {
await delay(SELECTION_EVENT_DELAY_MS)
// Value manipulation keys
if (
keyName.length === 1 ||
keyName === 'Backspace' ||
keyName === 'Delete' ||
keyName === 'Enter'
) {
await pressKey()
await waitForRevision()
} else if (
// Selection manipulation keys
[
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'PageUp',
'PageDown',
'Home',
'End',
].includes(keyName)
) {
await waitForSelection(() => pressKey())
} else {
// Unknown keys, test needs should be covered by the above cases.
console.warn(`Key ${keyName} not accounted for`)
await pressKey()
}
}
},
focus: async () => {
await editableHandle.focus()
},
setSelection: async (selection: EditorSelection | null) => {
await editableHandle.focus()
ipc.of.socketServer.emit(
'payload',
JSON.stringify({
Expand All @@ -161,7 +195,7 @@ export default class CollaborationEnvironment extends NodeEnvironment {
editorId,
})
)
await delay(SELECTION_EVENT_DELAY_MS)
await waitForSelection(() => delay(300)) // A little delay to let selection "manifest itself" via websocket.
},
async getValue(): Promise<PortableTextBlock[] | undefined> {
const valueHandle: puppeteer.ElementHandle<HTMLDivElement> = await page.waitForSelector(
Expand All @@ -172,15 +206,7 @@ export default class CollaborationEnvironment extends NodeEnvironment {
)
return value
},
async getSelection(): Promise<EditorSelection | null> {
const selectionHandle: puppeteer.ElementHandle<HTMLDivElement> = await page.waitForSelector(
'#pte-selection'
)
const selection = await selectionHandle.evaluate((node) =>
node.innerText ? JSON.parse(node.innerText) : null
)
return selection
},
getSelection,
}
})
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ type Editor = {
focus: () => Promise<void>
getSelection: () => Promise<EditorSelection | null>
getValue: () => Promise<Value>
insertNewLine: () => Promise<void>
insertText: (text: string) => Promise<void>
pressKey: (keyName: string, times?: number) => Promise<void>
setSelection: (selection: EditorSelection | null) => Promise<void>
Expand Down
31 changes: 22 additions & 9 deletions packages/@sanity/portable-text-editor/test/web-server/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,37 @@ export function App() {
}, [])
const webSocket = useMemo(() => {
const socket = new WebSocket(`ws://localhost:3001/?editorId=${editorId}&testId=${testId}`)
socket.addEventListener('open', () => {
socket.send(JSON.stringify({type: 'hello', editorId, testId}))
})
socket.addEventListener('message', (message) => {
if (message.data && typeof message.data === 'string') {
const data = JSON.parse(message.data)
if (data.testId === testId) {
if (data.type === 'value') {
setValue(data.value)
setRevId(data.revId)
}
if (data.type === 'selection' && data.editorId === editorId) {
setSelection(data.selection)
}
if (data.type === 'mutation' && data.editorId !== editorId) {
data.patches.map((patch) => incomingPatches$.next(patch))
switch (data.type) {
case 'value':
setValue(data.value)
setRevId(data.revId)
break
case 'selection':
if (data.editorId === editorId && data.testId === testId) {
setSelection(data.selection)
}
break
case 'mutation':
if (data.editorId !== editorId && data.testId === testId) {
data.patches.map((patch) => incomingPatches$.next(patch))
}
break
default:
// Nothing
}
}
}
})
return socket
}, [editorId, incomingPatches$, testId])

const handleMutation = useCallback(
(patches: Patch[]) => {
if (webSocket) {
Expand All @@ -50,6 +62,7 @@ export function App() {
},
[editorId, testId, webSocket]
)

return (
<ThemeProvider theme={studioTheme}>
<Stack>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable no-console */
import React, {useCallback, useMemo, useRef, useState} from 'react'
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {Text, Box, Card, Code} from '@sanity/ui'
import styled from 'styled-components'
import {Subject} from 'rxjs'
Expand Down Expand Up @@ -55,9 +55,17 @@ export const Editor = ({
selection: EditorSelection | null
}) => {
const [selectionValue, setSelectionValue] = useState<EditorSelection | null>(selection)
const selectionString = useMemo(() => JSON.stringify(selectionValue), [selectionValue])
const editor = useRef<PortableTextEditor>(null)
const keyGenFn = useMemo(() => createKeyGenerator(editorId), [editorId])

// Make sure tests always has focus or keyPress can become hard to test.
useEffect(() => {
if (editor.current && value) {
PortableTextEditor.focus(editor.current)
}
}, [editor, value])

const renderBlock: RenderBlockFunction = useCallback((block, type, attributes, defaultRender) => {
if (editor.current) {
const textType = PortableTextEditor.getPortableTextFeatures(editor.current).types.block
Expand Down Expand Up @@ -166,8 +174,14 @@ export const Editor = ({
/>
</Box>
<Box padding={4} style={{outline: '1px solid #999'}}>
<Code size={0} language="json" id="pte-selection">
{JSON.stringify(selectionValue)}
<Code
as="code"
size={0}
language="json"
id="pte-selection"
data-selection={selectionString}
>
{selectionString}
</Code>
</Box>
</PortableTextEditor>
Expand Down
25 changes: 15 additions & 10 deletions packages/@sanity/portable-text-editor/test/ws-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ const messages = new Subject()
const PORT = 3001
const valueMap: Record<string, PortableTextBlock[] | undefined> = {}
const revisionMap: Record<string, string> = {}
const editorToSocket = new WeakMap<{editorId: string; testId: string}, any>()

let sockets: any = []
const sub = messages.subscribe((next: any) => {
sockets.forEach((socket: any) => socket.send(next))
})

ipc.config.id = 'socketServer'
ipc.config.retry = 1500
Expand All @@ -32,25 +38,27 @@ ipc.serveNet(() => {
})
ipc.server.start()

let sockets: any = []

const sub = messages.subscribe((next: any) => {
sockets.forEach((socket: any) => socket.send(next))
})

app.ws('/', (s, req) => {
const testId = req.query.testId?.toString()
if (testId && !sockets.includes(s)) {
sockets.push(s)
s.send(
JSON.stringify({type: 'value', value: valueMap[testId], testId, revId: revisionMap[testId]})
JSON.stringify({
type: 'value',
value: valueMap[testId],
testId,
revId: revisionMap[testId] || 'first',
})
)
}
s.on('close', () => {
sockets = sockets.filter((socket: any) => socket !== s)
})
s.on('message', (msg: string) => {
const data = JSON.parse(msg)
if (data.type === 'hello') {
editorToSocket.set({editorId: data.editorId, testId: data.testId}, s)
}
if (data.type === 'mutation' && testId) {
valueMap[testId] = applyAll(valueMap[testId], data.patches)
messages.next(
Expand All @@ -63,9 +71,6 @@ app.ws('/', (s, req) => {
)
messages.next(JSON.stringify(data))
}
if (data.type === 'selection') {
messages.next(JSON.stringify(data))
}
})
})
const server = app.listen(PORT)
Expand Down

0 comments on commit 56cc8a4

Please sign in to comment.