diff --git a/apps/examples/e2e/playwright.config.ts b/apps/examples/e2e/playwright.config.ts index 29d211e07..ba0f8d4a1 100644 --- a/apps/examples/e2e/playwright.config.ts +++ b/apps/examples/e2e/playwright.config.ts @@ -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 */ diff --git a/apps/examples/e2e/tests/fixtures/menus/StylePanel.ts b/apps/examples/e2e/tests/fixtures/menus/StylePanel.ts index 3422a6419..faff05f31 100644 --- a/apps/examples/e2e/tests/fixtures/menus/StylePanel.ts +++ b/apps/examples/e2e/tests/fixtures/menus/StylePanel.ts @@ -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') } } diff --git a/apps/examples/e2e/tests/fixtures/menus/Toolbar.ts b/apps/examples/e2e/tests/fixtures/menus/Toolbar.ts index ce48bd09b..fcc254dc8 100644 --- a/apps/examples/e2e/tests/fixtures/menus/Toolbar.ts +++ b/apps/examples/e2e/tests/fixtures/menus/Toolbar.ts @@ -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) } } diff --git a/apps/examples/e2e/tests/test-mobile-style-panel.spec.ts b/apps/examples/e2e/tests/test-mobile-style-panel.spec.ts deleted file mode 100644 index df930e200..000000000 --- a/apps/examples/e2e/tests/test-mobile-style-panel.spec.ts +++ /dev/null @@ -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() - }) -}) diff --git a/apps/examples/e2e/tests/test-style-panel.spec.ts b/apps/examples/e2e/tests/test-style-panel.spec.ts new file mode 100644 index 000000000..fb14c5c57 --- /dev/null +++ b/apps/examples/e2e/tests/test-style-panel.spec.ts @@ -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() + }) +}) diff --git a/apps/examples/e2e/tests/test-toolbar.spec.ts b/apps/examples/e2e/tests/test-toolbar.spec.ts index ac3f68e13..73e65bfb3 100644 --- a/apps/examples/e2e/tests/test-toolbar.spec.ts +++ b/apps/examples/e2e/tests/test-toolbar.spec.ts @@ -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) - } - }) - } - }) })