tldraw/apps/examples/e2e/tests/test-text.spec.ts
Steve Ruiz 41601ac61e
Stickies: release candidate (#3249)
This PR is the target for the stickies PRs that are moving forward. It
should collect changes.

- [x] New icon
- [x] Improved shadows
- [x] Shadow LOD
- [x] New colors / theme options
- [x] Shrink text size to avoid word breaks on the x axis
- [x] Hide indicator whilst typing (reverted)
- [x] Adjacent note positions
  - [x] buttons / clone handles
  - [x] position helpers for creating / translating (pits)
- [x] keyboard shortcuts: (Tab, Shift+tab (RTL aware), Cmd-Enter,
Shift+Cmd+enter)
  - [x] multiple shape translating 
- [x] Text editing
  - [x] Edit on type (feature flagged)
  - [x] click goes in correct place
- [x] Notes as parents (reverted)
- [x] Update colors
- [x] Update SVG appearance

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `feature` — New feature

### Test Plan

Todo: fold in test plans for child PRs

### Unit tests:

- [ ] Shrink text size to avoid word breaks on the x axis
- [x] Adjacent notes
  - [x] buttons (clone handles)
  - [x] position helpers (pits)
- [x] keyboard shortcuts: (Tab, Shift+tab (RTL aware), Cmd-Enter,
Shift+Cmd+enter)
- [ ] Text editing
  - [ ] Edit on type
  - [ ] click goes in correct place

### Release Notes

- Improves sticky notes (see list)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Mime Čuvalo <mimecuvalo@gmail.com>
Co-authored-by: alex <alex@dytry.ch>
Co-authored-by: Mitja Bezenšek <mitja.bezensek@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Lu[ke] Wilson <l2wilson94@gmail.com>
Co-authored-by: huppy-bot[bot] <128400622+huppy-bot[bot]@users.noreply.github.com>
2024-04-14 18:40:02 +00:00

333 lines
8.7 KiB
TypeScript

import test, { Page, expect } from '@playwright/test'
import { BoxModel, Editor, TLNoteShape, TLShapeId } from 'tldraw'
import { setupPage } from '../shared-e2e'
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
const measureTextOptions = {
maxWidth: null,
fontFamily: 'var(--tl-font-draw)',
fontSize: 24,
lineHeight: 1.35,
fontWeight: 'normal',
fontStyle: 'normal',
padding: '0px',
}
const measureTextSpansOptions = {
width: 100,
height: 1000,
overflow: 'wrap' as const,
padding: 0,
fontSize: 24,
fontWeight: 'normal',
fontFamily: 'var(--tl-font-draw)',
fontStyle: 'normal',
lineHeight: 1.35,
textAlign: 'start' as 'start' | 'middle' | 'end',
}
function formatLines(spans: { box: BoxModel; text: string }[]) {
const lines = []
let currentLine: string[] | null = null
let currentLineTop = null
for (const span of spans) {
if (currentLineTop !== span.box.y) {
if (currentLine !== null) {
lines.push(currentLine)
}
currentLine = []
currentLineTop = span.box.y
}
currentLine!.push(span.text)
}
if (currentLine !== null) {
lines.push(currentLine)
}
return lines
}
declare const editor: Editor
let page: Page
test.describe('text measurement', () => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
await setupPage(page)
})
test('measures text', async () => {
const { w, h } = await page.evaluate<{ w: number; h: number }, typeof measureTextOptions>(
async (options) => editor.textMeasure.measureText('testing', options),
measureTextOptions
)
expect(w).toBeCloseTo(87, 0)
expect(h).toBeCloseTo(32.3984375, 0)
})
// The text-measurement tests below this point aren't super useful any
// more. They were added when we had a different approach to text SVG
// exports (trying to replicate browser decisions with our own code) to
// what we do now (letting the browser make those decisions then
// measuring the results).
//
// It's hard to write better tests here (e.g. ones where we actually
// look at the measured values) because the specifics of text layout
// vary from browser to browser. The ideal thing would be to replace
// these with visual regression tests for text SVG exports, but we don't
// have a way of doing visual regression testing right now.
test('should get a single text span', async () => {
const spans = await page.evaluate<
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) => editor.textMeasure.measureTextSpans('testing', options),
measureTextSpansOptions
)
expect(formatLines(spans)).toEqual([['testing']])
})
test('should wrap a word when it has to', async () => {
const spans = await page.evaluate<
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) => editor.textMeasure.measureTextSpans('testing', { ...options, width: 50 }),
measureTextSpansOptions
)
expect(formatLines(spans)).toEqual([['tes'], ['ting']])
})
test('should preserve whitespace at line breaks', async () => {
const spans = await page.evaluate<
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) => editor.textMeasure.measureTextSpans('testing testing', options),
measureTextSpansOptions
)
expect(formatLines(spans)).toEqual([['testing', ' '], ['testing']])
})
test('should preserve whitespace at the end of wrapped lines', async () => {
const spans = await page.evaluate<
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) => editor.textMeasure.measureTextSpans('testing testing ', options),
measureTextSpansOptions
)
expect(formatLines(spans)).toEqual([
['testing', ' '],
['testing', ' '],
])
})
test('preserves whitespace at the end of unwrapped lines', async () => {
const spans = await page.evaluate<
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) =>
editor.textMeasure.measureTextSpans('testing testing ', { ...options, width: 200 }),
measureTextSpansOptions
)
expect(formatLines(spans)).toEqual([['testing', ' ', 'testing', ' ']])
})
test('preserves whitespace at the start of an unwrapped line', async () => {
const spans = await page.evaluate<
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) =>
editor.textMeasure.measureTextSpans(' testing testing', { ...options, width: 200 }),
measureTextSpansOptions
)
expect(formatLines(spans)).toEqual([[' ', 'testing', ' ', 'testing']])
})
test('should place starting whitespace on its own line if it has to', async () => {
const spans = await page.evaluate<
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) => editor.textMeasure.measureTextSpans(' testing testing', options),
measureTextSpansOptions
)
expect(formatLines(spans)).toEqual([[' '], ['testing', ' '], ['testing']])
})
test('should handle multiline text', async () => {
const spans = await page.evaluate<
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) =>
editor.textMeasure.measureTextSpans(' test\ning testing \n t', options),
measureTextSpansOptions
)
expect(formatLines(spans)).toEqual([
[' ', 'test', '\n'],
['ing', ' '],
['testing', ' \n'],
[' ', 't'],
])
})
test('should break long strings of text', async () => {
const spans = await page.evaluate<
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) =>
editor.textMeasure.measureTextSpans('testingtestingtestingtestingtestingtesting', options),
measureTextSpansOptions
)
expect(formatLines(spans)).toEqual([
['testingt'],
['estingt'],
['estingt'],
['estingt'],
['estingt'],
['esting'],
])
})
test('should return an empty array if the text is empty', async () => {
const spans = await page.evaluate<
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(async (options) => editor.textMeasure.measureTextSpans('', options), measureTextSpansOptions)
expect(formatLines(spans)).toEqual([])
})
test('should handle trailing newlines', async () => {
const spans = await page.evaluate<
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) => editor.textMeasure.measureTextSpans('hi\n\n\n', options),
measureTextSpansOptions
)
expect(formatLines(spans)).toEqual([['hi', '\n'], [' \n'], [' \n'], [' ']])
})
test('should handle only newlines', async () => {
const spans = await page.evaluate<
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) => editor.textMeasure.measureTextSpans('\n\n\n', options),
measureTextSpansOptions
)
expect(formatLines(spans)).toEqual([[' \n'], [' \n'], [' \n'], [' ']])
})
test('for auto-font-sizing shapes, should do normal font size for text that does not have long words', async () => {
const shape = await page.evaluate(() => {
const id = 'shape:testShape' as TLShapeId
editor.createShapes([
{
id,
type: 'note',
x: 0,
y: 0,
props: {
text: 'this is just some regular text',
size: 'xl',
},
},
])
return editor.getShape(id) as TLNoteShape
})
expect(shape.props.fontSizeAdjustment).toEqual(32)
})
test('for auto-font-sizing shapes, should auto-size text that have slightly long words', async () => {
const shape = await page.evaluate(() => {
const id = 'shape:testShape' as TLShapeId
editor.createShapes([
{
id,
type: 'note',
x: 0,
y: 0,
props: {
text: 'Amsterdam',
size: 'xl',
},
},
])
return editor.getShape(id) as TLNoteShape
})
expect(shape.props.fontSizeAdjustment).toEqual(27)
})
test('for auto-font-sizing shapes, should auto-size text that have long words', async () => {
const shape = await page.evaluate(() => {
const id = 'shape:testShape' as TLShapeId
editor.createShapes([
{
id,
type: 'note',
x: 0,
y: 0,
props: {
text: 'this is a tentoonstelling',
size: 'xl',
},
},
])
return editor.getShape(id) as TLNoteShape
})
expect(shape.props.fontSizeAdjustment).toEqual(20)
})
test('for auto-font-sizing shapes, should wrap text that has words that are way too long', async () => {
const shape = await page.evaluate(() => {
const id = 'shape:testShape' as TLShapeId
editor.createShapes([
{
id,
type: 'note',
x: 0,
y: 0,
props: {
text: 'a very long dutch word like ziekenhuisinrichtingsmaatschappij',
size: 'xl',
},
},
])
return editor.getShape(id) as TLNoteShape
})
expect(shape.props.fontSizeAdjustment).toEqual(14)
})
})