E2e tests for the toolbar (#2709)

This PR adds some e2e tests for the toolbar.

Fixtures have been set up for the toolbar and style panel, and are
fairly barebones at the moment. Eventually each menu should have a
fixture associated with it, and all tests will use the class defined in
the fixtures file.

### Change Type

- [x] `tests` — Changes to any test code only[^2]

### Release Notes

- Add e2e tests for the toolbar
This commit is contained in:
Taha 2024-02-16 14:15:00 +00:00 committed by GitHub
parent c039d44f72
commit 7e673b5e37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 216 additions and 48 deletions

View file

@ -46,6 +46,7 @@ const config: PlaywrightTestConfig = {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry', trace: 'on-first-retry',
headless: true, // !process.env.CI, headless: true, // !process.env.CI,
video: 'retain-on-failure',
}, },
/* Configure projects for major browsers */ /* Configure projects for major browsers */

View file

@ -0,0 +1,21 @@
import { test as base } from '@playwright/test'
import { StylePanel } from './menus/StylePanel'
import { Toolbar } from './menus/Toolbar'
type Fixtures = {
toolbar: Toolbar
stylePanel: StylePanel
}
const test = base.extend<Fixtures>({
toolbar: async ({ page }, use) => {
const toolbar = new Toolbar(page)
await use(toolbar)
},
stylePanel: async ({ page }, use) => {
const stylePanel = new StylePanel(page)
await use(stylePanel)
},
})
export default test

View file

@ -0,0 +1,21 @@
import { Page } from '@playwright/test'
export class StylePanel {
readonly stylesArray: string[]
constructor(public readonly page: Page) {
this.page = page
this.stylesArray = [
'style.color',
'style.opacity',
'style.fill',
'style.dash',
'style.size',
'style.font',
'style.align',
]
}
getElement() {
return this.page.locator('[data-testid="style.panel"]')
}
}

View file

@ -0,0 +1,37 @@
import { Locator, Page, expect } from '@playwright/test'
export class Toolbar {
readonly toolLock: Locator
readonly moreToolsButton: Locator
readonly moreToolsPopover: Locator
readonly mobileStylesButton: Locator
readonly tools: { [key: string]: Locator }
readonly popOverTools: { [key: string]: Locator }
constructor(public readonly page: Page) {
this.page = page
this.toolLock = this.page.locator('[data-testid="tool-lock"]')
this.moreToolsButton = this.page.locator('[data-testid="tools.more-button"]')
this.moreToolsPopover = this.page.locator('[data-testid="tools.more-content"]')
this.mobileStylesButton = this.page.locator('[data-testid="mobile-styles.button"]')
this.tools = {
select: this.page.locator('[data-testid="tools.select"]'),
draw: this.page.locator('[data-testid="tools.draw"]'),
arrow: this.page.locator('[data-testid="tools.arrow"]'),
cloud: this.page.locator('[data-testid="tools.cloud"]'),
eraser: this.page.locator('[data-testid="tools.eraser"]'),
}
this.popOverTools = {
popoverCloud: this.page.locator('[data-testid="tools.more.cloud"]'),
popoverFrame: this.page.locator('[data-testid="tools.more.frame"]'),
}
}
async clickTool(tool: Locator) {
await tool.click()
}
async isSelected(tool: Locator, isSelected: boolean) {
// pseudo elements aren't exposed to the DOM, but we can check the color as a proxy
const expectedColor = isSelected ? 'rgb(255, 255, 255)' : 'rgb(46, 46, 46)'
await expect(tool).toHaveCSS('color', expectedColor)
}
}

View file

@ -0,0 +1,28 @@
import { expect } from '@playwright/test'
import { setup } from '../shared-e2e'
import test from './fixtures/fixtures'
test.describe('mobile ui', () => {
test.beforeEach(setup)
test('style panel opens and closes as expected', async ({
isMobile,
page,
toolbar,
stylePanel,
}) => {
test.skip(!isMobile, 'only run on mobile')
await expect(stylePanel.getElement()).toBeHidden()
await toolbar.mobileStylesButton.click()
await expect(stylePanel.getElement()).toBeVisible()
// clicking off the style panel should close it
page.mouse.click(200, 200)
await expect(stylePanel.getElement()).toBeHidden()
})
test('style menu button is disabled for the eraser tool', async ({ isMobile, toolbar }) => {
test.skip(!isMobile, 'only run on mobile')
const { eraser } = toolbar.tools
await eraser.click()
await expect(toolbar.mobileStylesButton).toBeDisabled()
})
})

View file

@ -1,5 +1,6 @@
import test, { Page, expect } from '@playwright/test' import { expect } from '@playwright/test'
import { getAllShapeTypes, setupPage } from '../shared-e2e' import { getAllShapeTypes, setup } from '../shared-e2e'
import test from './fixtures/fixtures'
export function sleep(ms: number) { export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms)) return new Promise((resolve) => setTimeout(resolve, ms))
@ -57,15 +58,9 @@ const draggableShapeCreators = [
const otherTools = [{ tool: 'select' }, { tool: 'eraser' }, { tool: 'laser' }] const otherTools = [{ tool: 'select' }, { tool: 'eraser' }, { tool: 'laser' }]
let page: Page
test.describe('Shape Tools', () => { test.describe('Shape Tools', () => {
test.beforeAll(async ({ browser }) => { test.beforeEach(setup)
page = await browser.newPage() test('creates shapes with other tools', async ({ toolbar, page }) => {
await setupPage(page)
})
test('creates shapes with other tools', async () => {
await page.keyboard.press('Control+a') await page.keyboard.press('Control+a')
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
expect(await getAllShapeTypes(page)).toEqual([]) expect(await getAllShapeTypes(page)).toEqual([])
@ -73,17 +68,17 @@ test.describe('Shape Tools', () => {
for (const { tool } of otherTools) { for (const { tool } of otherTools) {
// Find and click the button // Find and click the button
if (!(await page.getByTestId(`tools.${tool}`).isVisible())) { if (!(await page.getByTestId(`tools.${tool}`).isVisible())) {
if (!(await page.getByTestId(`tools.more`).isVisible())) { if (!(await toolbar.moreToolsButton.isVisible())) {
throw Error(`Tool more is not visible`) throw Error(`Tool more is not visible`)
} }
await page.getByTestId('tools.more').click() await toolbar.moreToolsButton.click()
if (!(await page.getByTestId(`tools.more.${tool}`).isVisible())) { if (!(await page.getByTestId(`tools.more.${tool}`).isVisible())) {
throw Error(`Tool in more panel is not visible`) throw Error(`Tool in more panel is not visible`)
} }
await page.getByTestId(`tools.more.${tool}`).click() await page.getByTestId(`tools.more.${tool}`).click()
await page.getByTestId(`tools.more`).click() await toolbar.moreToolsButton.click()
} }
if (!(await page.getByTestId(`tools.${tool}`).isVisible())) { if (!(await page.getByTestId(`tools.${tool}`).isVisible())) {
@ -102,7 +97,7 @@ test.describe('Shape Tools', () => {
} }
}) })
test('creates shapes clickable tools', async () => { test('creates shapes clickable tools', async ({ page, toolbar }) => {
await page.keyboard.press('v') await page.keyboard.press('v')
await page.keyboard.press('Control+a') await page.keyboard.press('Control+a')
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
@ -111,9 +106,9 @@ test.describe('Shape Tools', () => {
for (const { tool, shape } of clickableShapeCreators) { for (const { tool, shape } of clickableShapeCreators) {
// Find and click the button // Find and click the button
if (!(await page.getByTestId(`tools.${tool}`).isVisible())) { if (!(await page.getByTestId(`tools.${tool}`).isVisible())) {
await page.getByTestId('tools.more').click() await toolbar.moreToolsButton.click()
await page.getByTestId(`tools.more.${tool}`).click() await page.getByTestId(`tools.more.${tool}`).click()
await page.getByTestId('tools.more').click() await toolbar.moreToolsButton.click()
} }
await page.getByTestId(`tools.${tool}`).click() await page.getByTestId(`tools.${tool}`).click()
@ -141,7 +136,7 @@ test.describe('Shape Tools', () => {
expect(await getAllShapeTypes(page)).toEqual([]) expect(await getAllShapeTypes(page)).toEqual([])
}) })
test('creates shapes with draggable tools', async () => { test('creates shapes with draggable tools', async ({ page, toolbar }) => {
await page.keyboard.press('Control+a') await page.keyboard.press('Control+a')
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
expect(await getAllShapeTypes(page)).toEqual([]) expect(await getAllShapeTypes(page)).toEqual([])
@ -149,9 +144,9 @@ test.describe('Shape Tools', () => {
for (const { tool, shape } of draggableShapeCreators) { for (const { tool, shape } of draggableShapeCreators) {
// Find and click the button // Find and click the button
if (!(await page.getByTestId(`tools.${tool}`).isVisible())) { if (!(await page.getByTestId(`tools.${tool}`).isVisible())) {
await page.getByTestId('tools.more').click() await toolbar.moreToolsButton.click()
await page.getByTestId(`tools.more.${tool}`).click() await page.getByTestId(`tools.more.${tool}`).click()
await page.getByTestId('tools.more').click() await toolbar.moreToolsButton.click()
} }
await page.getByTestId(`tools.${tool}`).click() await page.getByTestId(`tools.${tool}`).click()

View file

@ -1,6 +1,7 @@
import test, { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { Editor, TLGeoShape } from '@tldraw/tldraw' import { Editor, TLGeoShape } from '@tldraw/tldraw'
import { getAllShapeTypes, setup } from '../shared-e2e' import { getAllShapeTypes, setup } from '../shared-e2e'
import test from './fixtures/fixtures'
export function sleep(ms: number) { export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms)) return new Promise((resolve) => setTimeout(resolve, ms))
@ -57,7 +58,7 @@ test.describe('smoke tests', () => {
expect(await page.getByTestId('quick-actions.redo').isDisabled()).toBe(true) expect(await page.getByTestId('quick-actions.redo').isDisabled()).toBe(true)
}) })
test('style panel + undo and redo squashing', async ({ page }) => { test('style panel + undo and redo squashing', async ({ page, toolbar }) => {
await page.keyboard.press('r') await page.keyboard.press('r')
await page.mouse.move(100, 100) await page.mouse.move(100, 100)
await page.mouse.down() await page.mouse.down()
@ -71,12 +72,11 @@ test.describe('smoke tests', () => {
expect(await getSelectedShapeColor()).toBe('black') expect(await getSelectedShapeColor()).toBe('black')
// when on a mobile device... // when on a mobile device...
const mobileStylesButton = page.getByTestId('mobile.styles') const hasMobileMenu = await toolbar.mobileStylesButton.isVisible()
const hasMobileMenu = await mobileStylesButton.isVisible()
if (hasMobileMenu) { if (hasMobileMenu) {
// open the style menu // open the style menu
await page.getByTestId('mobile.styles').click() await toolbar.mobileStylesButton.click()
} }
// Click the light-blue color // Click the light-blue color

View file

@ -0,0 +1,76 @@
import { expect } from '@playwright/test'
import { setup } from '../shared-e2e'
import test from './fixtures/fixtures'
test.describe('when selecting a tool from the toolbar', () => {
test.beforeEach(setup)
test('tool selection behaviors', async ({ toolbar }) => {
const { select, draw, arrow, cloud } = toolbar.tools
const { popoverCloud } = toolbar.popOverTools
await test.step('selecting a tool changes the button color', async () => {
await select.click()
await toolbar.isSelected(select, true)
await toolbar.isSelected(draw, false)
await draw.click()
await toolbar.isSelected(select, false)
await toolbar.isSelected(draw, true)
})
await test.step('selecting certain tools exposes the tool-lock button', async () => {
await draw.click()
expect(toolbar.toolLock).toBeHidden()
await arrow.click()
expect(toolbar.toolLock).toBeVisible()
})
await test.step('selecting a tool from the popover makes it appear on toolbar', async () => {
await expect(cloud).toBeHidden()
await expect(toolbar.moreToolsPopover).toBeHidden()
await toolbar.moreToolsButton.click()
await expect(toolbar.moreToolsPopover).toBeVisible()
await popoverCloud.click()
await expect(toolbar.moreToolsPopover).toBeHidden()
await expect(cloud).toBeVisible()
})
})
test('the correct styles are exposed for the selected tool', async ({
isMobile,
page,
toolbar,
stylePanel,
}) => {
const toolsStylesArr = [
{
name: 'tools.select',
styles: ['style.color', 'style.opacity', 'style.fill', 'style.dash', 'style.size'],
},
{ name: 'tools.more.frame', styles: ['style.opacity'] },
{
name: 'tools.text',
styles: ['style.size', 'style.color', 'style.opacity', 'style.font', 'style.align'],
},
]
for (const tool of toolsStylesArr) {
await test.step(`Check tool ${tool.name}`, async () => {
if (tool.name === 'tools.more.frame') {
await toolbar.moreToolsButton.click()
}
await page.getByTestId(tool.name).click()
if (isMobile) {
await toolbar.mobileStylesButton.click()
}
for (const style of stylePanel.stylesArray) {
const styleElement = page.getByTestId(style)
const isVisible = await styleElement.isVisible()
const isExpected = tool.styles.includes(style)
expect(isVisible).toBe(isExpected)
}
})
}
})
})

View file

@ -1,10 +0,0 @@
import test from '@playwright/test'
import { setup } from '../shared-e2e'
test.describe('ui', () => {
test.beforeEach(setup)
test('mobile style panel opens and closes when tapped', async ({ isMobile }) => {
test.skip(!isMobile, 'only run on mobile')
})
})

View file

@ -1,7 +1,5 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"packages": [ "packages": ["packages/*"],
"packages/*"
],
"version": "2.0.0-beta.3" "version": "2.0.0-beta.3"
} }

View file

@ -51,10 +51,12 @@ export function MobileStylePanel() {
<TldrawUiPopoverTrigger> <TldrawUiPopoverTrigger>
<TldrawUiButton <TldrawUiButton
type="tool" type="tool"
data-testid="mobile-styles.button"
style={{
color: disableStylePanel ? 'var(--color-muted-1)' : currentColor,
}}
title={msg('style-panel.title')} title={msg('style-panel.title')}
data-testid="mobile.styles"
disabled={disableStylePanel} disabled={disableStylePanel}
style={{ color: disableStylePanel ? 'var(--color-muted-1)' : currentColor }}
> >
<TldrawUiButtonIcon <TldrawUiButtonIcon
icon={disableStylePanel ? 'blob' : color?.type === 'mixed' ? 'mixed' : 'blob'} icon={disableStylePanel ? 'blob' : color?.type === 'mixed' ? 'mixed' : 'blob'}

View file

@ -299,6 +299,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
)} )}
</div> </div>
<div <div
data-testid="page-menu.list"
className="tlui-page-menu__list tlui-menu__group" className="tlui-page-menu__list tlui-menu__group"
style={{ height: ITEM_HEIGHT * pages.length + 4 }} style={{ height: ITEM_HEIGHT * pages.length + 4 }}
ref={rSortableContainer} ref={rSortableContainer}
@ -312,7 +313,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
return isEditing ? ( return isEditing ? (
<div <div
key={page.id + '_editing'} key={page.id + '_editing'}
data-testid={`page-menu-item-${page.id}`} data-testid="page-menu.item"
className="tlui-page_menu__item__sortable" className="tlui-page_menu__item__sortable"
style={{ style={{
zIndex: page.id === currentPage.id ? 888 : index, zIndex: page.id === currentPage.id ? 888 : index,
@ -370,11 +371,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
)} )}
</div> </div>
) : ( ) : (
<div <div key={page.id} data-testid="page-menu.item" className="tlui-page-menu__item">
key={page.id}
data-testid={`page-menu-item-${page.id}`}
className="tlui-page-menu__item"
>
<TldrawUiButton <TldrawUiButton
type="normal" type="normal"
className="tlui-page-menu__item__button" className="tlui-page-menu__item__button"

View file

@ -136,6 +136,7 @@ function CommonStylePickerSet({
tabIndex={-1} tabIndex={-1}
className="tlui-style-panel__section__common" className="tlui-style-panel__section__common"
aria-label="style panel styles" aria-label="style panel styles"
data-testid="style.panel"
> >
{color === undefined ? null : ( {color === undefined ? null : (
<TldrawUiButtonPicker <TldrawUiButtonPicker

View file

@ -150,7 +150,7 @@ export const DefaultToolbar = memo(function DefaultToolbar() {
title={msg('tool-panel.more')} title={msg('tool-panel.more')}
type="tool" type="tool"
className="tlui-toolbar__overflow" className="tlui-toolbar__overflow"
data-testid="tools.more" data-testid="tools.more-button"
> >
<TldrawUiButtonIcon icon="chevron-up" /> <TldrawUiButtonIcon icon="chevron-up" />
</TldrawUiButton> </TldrawUiButton>
@ -181,7 +181,7 @@ const OverflowToolsContent = track(function OverflowToolsContent({
const msg = useTranslation() const msg = useTranslation()
return ( return (
<div className="tlui-buttons__grid"> <div className="tlui-buttons__grid" data-testid="tools.more-content">
{toolbarItems.map(({ toolItem: { id, meta, kbd, label, onSelect, icon } }) => { {toolbarItems.map(({ toolItem: { id, meta, kbd, label, onSelect, icon } }) => {
return ( return (
<TldrawUiDropdownMenuItem <TldrawUiDropdownMenuItem

View file

@ -36,6 +36,7 @@ export function ToggleToolLockedButton({ activeToolId }: ToggleToolLockedButtonP
<TldrawUiButton <TldrawUiButton
type="normal" type="normal"
title={msg('action.toggle-tool-lock')} title={msg('action.toggle-tool-lock')}
data-testid="tool-lock"
className={classNames('tlui-toolbar__lock-button', { className={classNames('tlui-toolbar__lock-button', {
'tlui-toolbar__lock-button__mobile': breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM, 'tlui-toolbar__lock-button__mobile': breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM,
})} })}

View file

@ -99,7 +99,7 @@ export const TldrawUiButtonPicker = memo(function TldrawUiButtonPicker<T extends
) )
return ( return (
<div className={classNames('tlui-buttons__grid')}> <div data-testid={`style.${uiType}`} className={classNames('tlui-buttons__grid')}>
{items.map((item) => ( {items.map((item) => (
<TldrawUiButton <TldrawUiButton
type="icon" type="icon"