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',
|
trace: 'on-first-retry',
|
||||||
headless: true, // !process.env.CI,
|
headless: true, // !process.env.CI,
|
||||||
video: 'retain-on-failure',
|
video: 'retain-on-failure',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
/* Configure projects for major browsers */
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import { Page } from '@playwright/test'
|
import { Locator, Page, expect } from '@playwright/test'
|
||||||
|
|
||||||
export class StylePanel {
|
export class StylePanel {
|
||||||
readonly stylesArray: string[]
|
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) {
|
constructor(public readonly page: Page) {
|
||||||
this.page = page
|
this.page = page
|
||||||
|
@ -14,8 +20,68 @@ export class StylePanel {
|
||||||
'style.font',
|
'style.font',
|
||||||
'style.align',
|
'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() {
|
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) {
|
constructor(public readonly page: Page) {
|
||||||
this.page = page
|
this.page = page
|
||||||
this.toolLock = this.page.locator('[data-testid="tool-lock"]')
|
this.toolLock = this.page.getByTestId('tool-lock')
|
||||||
this.moreToolsButton = this.page.locator('[data-testid="tools.more-button"]')
|
this.moreToolsButton = this.page.getByTestId('tools.more-button')
|
||||||
this.moreToolsPopover = this.page.locator('[data-testid="tools.more-content"]')
|
this.moreToolsPopover = this.page.getByTestId('tools.more-content')
|
||||||
this.mobileStylesButton = this.page.locator('[data-testid="mobile-styles.button"]')
|
this.mobileStylesButton = this.page.getByTestId('mobile-styles.button')
|
||||||
this.tools = {
|
this.tools = {
|
||||||
select: this.page.locator('[data-testid="tools.select"]'),
|
select: this.page.getByTestId('tools.select'),
|
||||||
draw: this.page.locator('[data-testid="tools.draw"]'),
|
draw: this.page.getByTestId('tools.draw'),
|
||||||
arrow: this.page.locator('[data-testid="tools.arrow"]'),
|
arrow: this.page.getByTestId('tools.arrow'),
|
||||||
cloud: this.page.locator('[data-testid="tools.cloud"]'),
|
cloud: this.page.getByTestId('tools.cloud'),
|
||||||
eraser: this.page.locator('[data-testid="tools.eraser"]'),
|
eraser: this.page.getByTestId('tools.eraser'),
|
||||||
|
rectangle: this.page.getByTestId('tools.rectangle'),
|
||||||
}
|
}
|
||||||
this.popOverTools = {
|
this.popOverTools = {
|
||||||
popoverCloud: this.page.locator('[data-testid="tools.more.cloud"]'),
|
popoverCloud: this.page.getByTestId('tools.more.cloud'),
|
||||||
popoverFrame: this.page.locator('[data-testid="tools.more.frame"]'),
|
popoverFrame: this.page.getByTestId('tools.more.frame'),
|
||||||
|
popoverRectangle: this.page.getByTestId('tools.more.rectangle'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async clickTool(tool: Locator) {
|
async clickTool(tool: Locator) {
|
||||||
await tool.click()
|
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
|
// 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)
|
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 test.step('selecting a tool changes the button color', async () => {
|
||||||
await select.click()
|
await select.click()
|
||||||
await toolbar.isSelected(select, true)
|
await toolbar.isSelected(select)
|
||||||
await toolbar.isSelected(draw, false)
|
await toolbar.isNotSelected(draw)
|
||||||
await draw.click()
|
await draw.click()
|
||||||
await toolbar.isSelected(select, false)
|
await toolbar.isNotSelected(select)
|
||||||
await toolbar.isSelected(draw, true)
|
await toolbar.isSelected(draw)
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step('selecting certain tools exposes the tool-lock button', async () => {
|
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()
|
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