E2E Style Panel Tests (#2878)
This PR adds E2E tests for the style panel. It checks that: - the style panel opens and closes as expected on mobile - the style panel button is disabled for the eraser tool on mobile - selecting a style hints the button - changing a style changes the appearance of the shape - It also moves a test from the toolbar tests that checks the correct styles are exposed for the right tools fixes tld-2222 - [x] `tests` — Changes to any test code only[^2] ### Release Notes - Add style panel E2E tests --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
046ebc4ac0
commit
fcf97958e8
6 changed files with 234 additions and 85 deletions
|
@ -47,6 +47,7 @@ const config: PlaywrightTestConfig = {
|
|||
trace: 'on-first-retry',
|
||||
headless: true, // !process.env.CI,
|
||||
video: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import { Page } from '@playwright/test'
|
||||
import { Locator, Page, expect } from '@playwright/test'
|
||||
|
||||
export class StylePanel {
|
||||
readonly stylesArray: string[]
|
||||
readonly colors: { [key: string]: Locator }
|
||||
readonly fill: { [key: string]: Locator }
|
||||
readonly dash: { [key: string]: Locator }
|
||||
readonly size: { [key: string]: Locator }
|
||||
readonly font: { [key: string]: Locator }
|
||||
readonly align: { [key: string]: Locator }
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.page = page
|
||||
|
@ -14,8 +20,68 @@ export class StylePanel {
|
|||
'style.font',
|
||||
'style.align',
|
||||
]
|
||||
this.colors = {
|
||||
black: this.page.getByTestId('style.color.black'),
|
||||
grey: this.page.getByTestId('style.color.grey'),
|
||||
lightViolet: this.page.getByTestId('style.color.light-violet'),
|
||||
violet: this.page.getByTestId('style.color.violet'),
|
||||
blue: this.page.getByTestId('style.color.blue'),
|
||||
lightBlue: this.page.getByTestId('style.color.light-blue'),
|
||||
yellow: this.page.getByTestId('style.color.yellow'),
|
||||
orange: this.page.getByTestId('style.color.orange'),
|
||||
green: this.page.getByTestId('style.color.green'),
|
||||
lightGreen: this.page.getByTestId('style.color.light-green'),
|
||||
lightRed: this.page.getByTestId('style.color.light-red'),
|
||||
red: this.page.getByTestId('style.color.red'),
|
||||
}
|
||||
this.fill = {
|
||||
none: this.page.getByTestId('style.fill.none'),
|
||||
semi: this.page.getByTestId('style.fill.semi'),
|
||||
solid: this.page.getByTestId('style.fill.solid'),
|
||||
pattern: this.page.getByTestId('style.fill.pattern'),
|
||||
}
|
||||
this.dash = {
|
||||
draw: this.page.getByTestId('style.dash.draw'),
|
||||
dashed: this.page.getByTestId('style.dash.dashed'),
|
||||
dotted: this.page.getByTestId('style.dash.dotted'),
|
||||
solid: this.page.getByTestId('style.dash.solid'),
|
||||
}
|
||||
this.size = {
|
||||
s: this.page.getByTestId('style.size.s'),
|
||||
m: this.page.getByTestId('style.size.m'),
|
||||
l: this.page.getByTestId('style.size.l'),
|
||||
xl: this.page.getByTestId('style.size.xl'),
|
||||
}
|
||||
this.font = {
|
||||
draw: this.page.getByTestId('style.font.draw'),
|
||||
sans: this.page.getByTestId('style.font.sans'),
|
||||
serif: this.page.getByTestId('style.font.serif'),
|
||||
mono: this.page.getByTestId('style.font.mono'),
|
||||
}
|
||||
|
||||
this.align = {
|
||||
start: this.page.getByTestId('style.align.start'),
|
||||
middle: this.page.getByTestId('style.align.middle'),
|
||||
end: this.page.getByTestId('style.align.end'),
|
||||
}
|
||||
}
|
||||
async getAfterElementStyle(style: Locator, property: string) {
|
||||
const getStyle = (element: Element, property: string) =>
|
||||
window.getComputedStyle(element, '::after').getPropertyValue(property)
|
||||
return await style.evaluate(getStyle, property)
|
||||
}
|
||||
|
||||
async isHinted(style: Locator) {
|
||||
const backgroundColor = await this.getAfterElementStyle(style, 'background-color')
|
||||
return expect(backgroundColor).toBe('rgba(0, 0, 0, 0.055)')
|
||||
}
|
||||
|
||||
async isNotHinted(style: Locator) {
|
||||
const backgroundColor = await this.getAfterElementStyle(style, 'background-color')
|
||||
// The color is different on mobile
|
||||
return expect(['rgba(0, 0, 0, 0.043)', 'rgba(0, 0, 0, 0)']).toContain(backgroundColor)
|
||||
}
|
||||
getElement() {
|
||||
return this.page.locator('[data-testid="style.panel"]')
|
||||
return this.page.getByTestId('style.panel')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,28 +10,34 @@ export class Toolbar {
|
|||
|
||||
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.toolLock = this.page.getByTestId('tool-lock')
|
||||
this.moreToolsButton = this.page.getByTestId('tools.more-button')
|
||||
this.moreToolsPopover = this.page.getByTestId('tools.more-content')
|
||||
this.mobileStylesButton = this.page.getByTestId('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"]'),
|
||||
select: this.page.getByTestId('tools.select'),
|
||||
draw: this.page.getByTestId('tools.draw'),
|
||||
arrow: this.page.getByTestId('tools.arrow'),
|
||||
cloud: this.page.getByTestId('tools.cloud'),
|
||||
eraser: this.page.getByTestId('tools.eraser'),
|
||||
rectangle: this.page.getByTestId('tools.rectangle'),
|
||||
}
|
||||
this.popOverTools = {
|
||||
popoverCloud: this.page.locator('[data-testid="tools.more.cloud"]'),
|
||||
popoverFrame: this.page.locator('[data-testid="tools.more.frame"]'),
|
||||
popoverCloud: this.page.getByTestId('tools.more.cloud'),
|
||||
popoverFrame: this.page.getByTestId('tools.more.frame'),
|
||||
popoverRectangle: this.page.getByTestId('tools.more.rectangle'),
|
||||
}
|
||||
}
|
||||
async clickTool(tool: Locator) {
|
||||
await tool.click()
|
||||
}
|
||||
async isSelected(tool: Locator, isSelected: boolean) {
|
||||
async isSelected(tool: Locator) {
|
||||
// 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)'
|
||||
const expectedColor = 'rgb(255, 255, 255)'
|
||||
await expect(tool).toHaveCSS('color', expectedColor)
|
||||
}
|
||||
async isNotSelected(tool: Locator) {
|
||||
const expectedColor = 'rgb(46, 46, 46)'
|
||||
await expect(tool).toHaveCSS('color', expectedColor)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
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()
|
||||
})
|
||||
})
|
142
apps/examples/e2e/tests/test-style-panel.spec.ts
Normal file
142
apps/examples/e2e/tests/test-style-panel.spec.ts
Normal file
|
@ -0,0 +1,142 @@
|
|||
import { expect } from '@playwright/test'
|
||||
import { Editor } from '@tldraw/tldraw'
|
||||
import { setup } from '../shared-e2e'
|
||||
import test from './fixtures/fixtures'
|
||||
|
||||
declare const editor: Editor
|
||||
|
||||
test.describe('Style selection behaviour', () => {
|
||||
test.beforeEach(setup)
|
||||
test('selecting a style hints the button', async ({ isMobile, stylePanel, toolbar }) => {
|
||||
const { blue, black } = stylePanel.colors
|
||||
const { pattern, none } = stylePanel.fill
|
||||
if (isMobile) {
|
||||
await toolbar.mobileStylesButton.click()
|
||||
}
|
||||
// these are hinted by default
|
||||
await stylePanel.isHinted(black)
|
||||
await stylePanel.isHinted(none)
|
||||
// these are not hinted by default
|
||||
await stylePanel.isNotHinted(pattern)
|
||||
await stylePanel.isNotHinted(blue)
|
||||
|
||||
await blue.click()
|
||||
await stylePanel.isHinted(blue)
|
||||
await stylePanel.isNotHinted(black)
|
||||
|
||||
await pattern.click()
|
||||
await stylePanel.isHinted(pattern)
|
||||
await stylePanel.isNotHinted(none)
|
||||
// this should not change the hint state of color buttons
|
||||
await stylePanel.isHinted(blue)
|
||||
})
|
||||
|
||||
test('selecting a style changes the style of the shapes', async ({
|
||||
page,
|
||||
stylePanel,
|
||||
toolbar,
|
||||
isMobile,
|
||||
}) => {
|
||||
const { blue } = stylePanel.colors
|
||||
const { rectangle } = toolbar.tools
|
||||
const { popoverRectangle } = toolbar.popOverTools
|
||||
const { pattern } = stylePanel.fill
|
||||
if (isMobile) {
|
||||
await toolbar.mobileStylesButton.click()
|
||||
}
|
||||
await blue.click()
|
||||
if (isMobile) {
|
||||
await toolbar.moreToolsButton.click()
|
||||
await popoverRectangle.click()
|
||||
} else {
|
||||
await rectangle.click()
|
||||
}
|
||||
await page.mouse.click(150, 150)
|
||||
const shapes1 = await page.evaluate(() => editor.getSelectedShapes())
|
||||
expect(shapes1.every((s: any) => s.props.color === 'blue' && s.props.fill === 'none')).toBe(
|
||||
true
|
||||
)
|
||||
if (isMobile) {
|
||||
await toolbar.mobileStylesButton.click()
|
||||
}
|
||||
await pattern.click()
|
||||
await rectangle.click()
|
||||
await page.mouse.click(250, 150)
|
||||
await page.mouse.move(100, 100)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(400, 400)
|
||||
await page.mouse.up()
|
||||
const shapes2 = await page.evaluate(() => editor.getSelectedShapes())
|
||||
expect(shapes2.every((s: any) => s.props.color === 'blue' && s.props.fill === 'pattern')).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
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'],
|
||||
},
|
||||
{ name: 'tools.eraser', styles: [] },
|
||||
]
|
||||
|
||||
for (const tool of toolsStylesArr) {
|
||||
await test.step(`Check tool ${tool.name}`, async () => {
|
||||
// mobile styles button is disabled for the eraser tool
|
||||
if (tool.name === 'tools.eraser' && isMobile) return
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('mobile style panel', () => {
|
||||
test.beforeEach(setup)
|
||||
test('opens and closes as expected', async ({ isMobile, page, toolbar, stylePanel }) => {
|
||||
test.skip(!isMobile, 'only run on mobile')
|
||||
|
||||
await test.step('clicking the mobile styles button', async () => {
|
||||
await expect(stylePanel.getElement()).toBeHidden()
|
||||
await toolbar.mobileStylesButton.click()
|
||||
await expect(stylePanel.getElement()).toBeVisible()
|
||||
await toolbar.mobileStylesButton.click()
|
||||
await expect(stylePanel.getElement()).toBeHidden()
|
||||
})
|
||||
|
||||
await test.step('clicking off the panel closes it', async () => {
|
||||
await toolbar.mobileStylesButton.click()
|
||||
await expect(stylePanel.getElement()).toBeVisible()
|
||||
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()
|
||||
})
|
||||
})
|
|
@ -11,11 +11,11 @@ test.describe('when selecting a tool from the toolbar', () => {
|
|||
|
||||
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 toolbar.isSelected(select)
|
||||
await toolbar.isNotSelected(draw)
|
||||
await draw.click()
|
||||
await toolbar.isSelected(select, false)
|
||||
await toolbar.isSelected(draw, true)
|
||||
await toolbar.isNotSelected(select)
|
||||
await toolbar.isSelected(draw)
|
||||
})
|
||||
|
||||
await test.step('selecting certain tools exposes the tool-lock button', async () => {
|
||||
|
@ -35,42 +35,4 @@ test.describe('when selecting a tool from the toolbar', () => {
|
|||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue