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:
parent
c039d44f72
commit
7e673b5e37
16 changed files with 216 additions and 48 deletions
|
@ -46,6 +46,7 @@ const config: PlaywrightTestConfig = {
|
|||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
headless: true, // !process.env.CI,
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
|
|
21
apps/examples/e2e/tests/fixtures/fixtures.ts
vendored
Normal file
21
apps/examples/e2e/tests/fixtures/fixtures.ts
vendored
Normal 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
|
21
apps/examples/e2e/tests/fixtures/menus/StylePanel.ts
vendored
Normal file
21
apps/examples/e2e/tests/fixtures/menus/StylePanel.ts
vendored
Normal 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"]')
|
||||
}
|
||||
}
|
37
apps/examples/e2e/tests/fixtures/menus/Toolbar.ts
vendored
Normal file
37
apps/examples/e2e/tests/fixtures/menus/Toolbar.ts
vendored
Normal 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)
|
||||
}
|
||||
}
|
28
apps/examples/e2e/tests/test-mobile-style-panel.spec.ts
Normal file
28
apps/examples/e2e/tests/test-mobile-style-panel.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
|
@ -1,5 +1,6 @@
|
|||
import test, { Page, expect } from '@playwright/test'
|
||||
import { getAllShapeTypes, setupPage } from '../shared-e2e'
|
||||
import { expect } from '@playwright/test'
|
||||
import { getAllShapeTypes, setup } from '../shared-e2e'
|
||||
import test from './fixtures/fixtures'
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
@ -57,15 +58,9 @@ const draggableShapeCreators = [
|
|||
|
||||
const otherTools = [{ tool: 'select' }, { tool: 'eraser' }, { tool: 'laser' }]
|
||||
|
||||
let page: Page
|
||||
|
||||
test.describe('Shape Tools', () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await setupPage(page)
|
||||
})
|
||||
|
||||
test('creates shapes with other tools', async () => {
|
||||
test.beforeEach(setup)
|
||||
test('creates shapes with other tools', async ({ toolbar, page }) => {
|
||||
await page.keyboard.press('Control+a')
|
||||
await page.keyboard.press('Backspace')
|
||||
expect(await getAllShapeTypes(page)).toEqual([])
|
||||
|
@ -73,17 +68,17 @@ test.describe('Shape Tools', () => {
|
|||
for (const { tool } of otherTools) {
|
||||
// Find and click the button
|
||||
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`)
|
||||
}
|
||||
await page.getByTestId('tools.more').click()
|
||||
await toolbar.moreToolsButton.click()
|
||||
|
||||
if (!(await page.getByTestId(`tools.more.${tool}`).isVisible())) {
|
||||
throw Error(`Tool in more panel is not visible`)
|
||||
}
|
||||
await page.getByTestId(`tools.more.${tool}`).click()
|
||||
|
||||
await page.getByTestId(`tools.more`).click()
|
||||
await toolbar.moreToolsButton.click()
|
||||
}
|
||||
|
||||
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('Control+a')
|
||||
await page.keyboard.press('Backspace')
|
||||
|
@ -111,9 +106,9 @@ test.describe('Shape Tools', () => {
|
|||
for (const { tool, shape } of clickableShapeCreators) {
|
||||
// Find and click the button
|
||||
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').click()
|
||||
await toolbar.moreToolsButton.click()
|
||||
}
|
||||
await page.getByTestId(`tools.${tool}`).click()
|
||||
|
||||
|
@ -141,7 +136,7 @@ test.describe('Shape Tools', () => {
|
|||
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('Backspace')
|
||||
expect(await getAllShapeTypes(page)).toEqual([])
|
||||
|
@ -149,9 +144,9 @@ test.describe('Shape Tools', () => {
|
|||
for (const { tool, shape } of draggableShapeCreators) {
|
||||
// Find and click the button
|
||||
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').click()
|
||||
await toolbar.moreToolsButton.click()
|
||||
}
|
||||
|
||||
await page.getByTestId(`tools.${tool}`).click()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import test, { expect } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { Editor, TLGeoShape } from '@tldraw/tldraw'
|
||||
import { getAllShapeTypes, setup } from '../shared-e2e'
|
||||
import test from './fixtures/fixtures'
|
||||
|
||||
export function sleep(ms: number) {
|
||||
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)
|
||||
})
|
||||
|
||||
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.mouse.move(100, 100)
|
||||
await page.mouse.down()
|
||||
|
@ -71,12 +72,11 @@ test.describe('smoke tests', () => {
|
|||
expect(await getSelectedShapeColor()).toBe('black')
|
||||
|
||||
// when on a mobile device...
|
||||
const mobileStylesButton = page.getByTestId('mobile.styles')
|
||||
const hasMobileMenu = await mobileStylesButton.isVisible()
|
||||
const hasMobileMenu = await toolbar.mobileStylesButton.isVisible()
|
||||
|
||||
if (hasMobileMenu) {
|
||||
// open the style menu
|
||||
await page.getByTestId('mobile.styles').click()
|
||||
await toolbar.mobileStylesButton.click()
|
||||
}
|
||||
|
||||
// Click the light-blue color
|
||||
|
|
76
apps/examples/e2e/tests/test-toolbar.spec.ts
Normal file
76
apps/examples/e2e/tests/test-toolbar.spec.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
|
@ -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')
|
||||
})
|
||||
})
|
|
@ -1,7 +1,5 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"packages": ["packages/*"],
|
||||
"version": "2.0.0-beta.3"
|
||||
}
|
||||
|
|
|
@ -51,10 +51,12 @@ export function MobileStylePanel() {
|
|||
<TldrawUiPopoverTrigger>
|
||||
<TldrawUiButton
|
||||
type="tool"
|
||||
data-testid="mobile-styles.button"
|
||||
style={{
|
||||
color: disableStylePanel ? 'var(--color-muted-1)' : currentColor,
|
||||
}}
|
||||
title={msg('style-panel.title')}
|
||||
data-testid="mobile.styles"
|
||||
disabled={disableStylePanel}
|
||||
style={{ color: disableStylePanel ? 'var(--color-muted-1)' : currentColor }}
|
||||
>
|
||||
<TldrawUiButtonIcon
|
||||
icon={disableStylePanel ? 'blob' : color?.type === 'mixed' ? 'mixed' : 'blob'}
|
||||
|
|
|
@ -299,6 +299,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
|
|||
)}
|
||||
</div>
|
||||
<div
|
||||
data-testid="page-menu.list"
|
||||
className="tlui-page-menu__list tlui-menu__group"
|
||||
style={{ height: ITEM_HEIGHT * pages.length + 4 }}
|
||||
ref={rSortableContainer}
|
||||
|
@ -312,7 +313,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
|
|||
return isEditing ? (
|
||||
<div
|
||||
key={page.id + '_editing'}
|
||||
data-testid={`page-menu-item-${page.id}`}
|
||||
data-testid="page-menu.item"
|
||||
className="tlui-page_menu__item__sortable"
|
||||
style={{
|
||||
zIndex: page.id === currentPage.id ? 888 : index,
|
||||
|
@ -370,11 +371,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
|
|||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
key={page.id}
|
||||
data-testid={`page-menu-item-${page.id}`}
|
||||
className="tlui-page-menu__item"
|
||||
>
|
||||
<div key={page.id} data-testid="page-menu.item" className="tlui-page-menu__item">
|
||||
<TldrawUiButton
|
||||
type="normal"
|
||||
className="tlui-page-menu__item__button"
|
||||
|
|
|
@ -136,6 +136,7 @@ function CommonStylePickerSet({
|
|||
tabIndex={-1}
|
||||
className="tlui-style-panel__section__common"
|
||||
aria-label="style panel styles"
|
||||
data-testid="style.panel"
|
||||
>
|
||||
{color === undefined ? null : (
|
||||
<TldrawUiButtonPicker
|
||||
|
|
|
@ -150,7 +150,7 @@ export const DefaultToolbar = memo(function DefaultToolbar() {
|
|||
title={msg('tool-panel.more')}
|
||||
type="tool"
|
||||
className="tlui-toolbar__overflow"
|
||||
data-testid="tools.more"
|
||||
data-testid="tools.more-button"
|
||||
>
|
||||
<TldrawUiButtonIcon icon="chevron-up" />
|
||||
</TldrawUiButton>
|
||||
|
@ -181,7 +181,7 @@ const OverflowToolsContent = track(function OverflowToolsContent({
|
|||
const msg = useTranslation()
|
||||
|
||||
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 } }) => {
|
||||
return (
|
||||
<TldrawUiDropdownMenuItem
|
||||
|
|
|
@ -36,6 +36,7 @@ export function ToggleToolLockedButton({ activeToolId }: ToggleToolLockedButtonP
|
|||
<TldrawUiButton
|
||||
type="normal"
|
||||
title={msg('action.toggle-tool-lock')}
|
||||
data-testid="tool-lock"
|
||||
className={classNames('tlui-toolbar__lock-button', {
|
||||
'tlui-toolbar__lock-button__mobile': breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM,
|
||||
})}
|
||||
|
|
|
@ -99,7 +99,7 @@ export const TldrawUiButtonPicker = memo(function TldrawUiButtonPicker<T extends
|
|||
)
|
||||
|
||||
return (
|
||||
<div className={classNames('tlui-buttons__grid')}>
|
||||
<div data-testid={`style.${uiType}`} className={classNames('tlui-buttons__grid')}>
|
||||
{items.map((item) => (
|
||||
<TldrawUiButton
|
||||
type="icon"
|
||||
|
|
Loading…
Reference in a new issue