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:
Taha 2024-02-23 14:37:15 +00:00 committed by GitHub
parent 046ebc4ac0
commit fcf97958e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 234 additions and 85 deletions

View file

@ -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 */

View file

@ -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')
}
}

View file

@ -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)
}
}

View file

@ -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()
})
})

View 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()
})
})

View file

@ -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)
}
})
}
})
})