From 7e673b5e37cc72f349b0e8cc6eb9acb5ef30c1fd Mon Sep 17 00:00:00 2001 From: Taha <98838967+Taha-Hassan-Git@users.noreply.github.com> Date: Fri, 16 Feb 2024 14:15:00 +0000 Subject: [PATCH] E2e tests for the toolbar (#2709) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/examples/e2e/playwright.config.ts | 1 + apps/examples/e2e/tests/fixtures/fixtures.ts | 21 +++++ .../e2e/tests/fixtures/menus/StylePanel.ts | 21 +++++ .../e2e/tests/fixtures/menus/Toolbar.ts | 37 +++++++++ .../e2e/tests/test-mobile-style-panel.spec.ts | 28 +++++++ apps/examples/e2e/tests/test-shapes.spec.ts | 33 ++++---- apps/examples/e2e/tests/test-smoke.spec.ts | 10 +-- apps/examples/e2e/tests/test-toolbar.spec.ts | 76 +++++++++++++++++++ apps/examples/e2e/tests/test-ui.spec.ts | 10 --- lerna.json | 4 +- .../lib/ui/components/MobileStylePanel.tsx | 6 +- .../components/PageMenu/DefaultPageMenu.tsx | 9 +-- .../StylePanel/DefaultStylePanelContent.tsx | 1 + .../ui/components/Toolbar/DefaultToolbar.tsx | 4 +- .../Toolbar/ToggleToolLockedButton.tsx | 1 + .../primitives/TldrawUiButtonPicker.tsx | 2 +- 16 files changed, 216 insertions(+), 48 deletions(-) create mode 100644 apps/examples/e2e/tests/fixtures/fixtures.ts create mode 100644 apps/examples/e2e/tests/fixtures/menus/StylePanel.ts create mode 100644 apps/examples/e2e/tests/fixtures/menus/Toolbar.ts create mode 100644 apps/examples/e2e/tests/test-mobile-style-panel.spec.ts create mode 100644 apps/examples/e2e/tests/test-toolbar.spec.ts delete mode 100644 apps/examples/e2e/tests/test-ui.spec.ts diff --git a/apps/examples/e2e/playwright.config.ts b/apps/examples/e2e/playwright.config.ts index 9c3137ba0..29d211e07 100644 --- a/apps/examples/e2e/playwright.config.ts +++ b/apps/examples/e2e/playwright.config.ts @@ -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 */ diff --git a/apps/examples/e2e/tests/fixtures/fixtures.ts b/apps/examples/e2e/tests/fixtures/fixtures.ts new file mode 100644 index 000000000..d86645aea --- /dev/null +++ b/apps/examples/e2e/tests/fixtures/fixtures.ts @@ -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({ + 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 diff --git a/apps/examples/e2e/tests/fixtures/menus/StylePanel.ts b/apps/examples/e2e/tests/fixtures/menus/StylePanel.ts new file mode 100644 index 000000000..3422a6419 --- /dev/null +++ b/apps/examples/e2e/tests/fixtures/menus/StylePanel.ts @@ -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"]') + } +} diff --git a/apps/examples/e2e/tests/fixtures/menus/Toolbar.ts b/apps/examples/e2e/tests/fixtures/menus/Toolbar.ts new file mode 100644 index 000000000..ce48bd09b --- /dev/null +++ b/apps/examples/e2e/tests/fixtures/menus/Toolbar.ts @@ -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) + } +} diff --git a/apps/examples/e2e/tests/test-mobile-style-panel.spec.ts b/apps/examples/e2e/tests/test-mobile-style-panel.spec.ts new file mode 100644 index 000000000..df930e200 --- /dev/null +++ b/apps/examples/e2e/tests/test-mobile-style-panel.spec.ts @@ -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() + }) +}) diff --git a/apps/examples/e2e/tests/test-shapes.spec.ts b/apps/examples/e2e/tests/test-shapes.spec.ts index f1b00c14c..fb638a357 100644 --- a/apps/examples/e2e/tests/test-shapes.spec.ts +++ b/apps/examples/e2e/tests/test-shapes.spec.ts @@ -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() diff --git a/apps/examples/e2e/tests/test-smoke.spec.ts b/apps/examples/e2e/tests/test-smoke.spec.ts index c6d856bea..ec21e3857 100644 --- a/apps/examples/e2e/tests/test-smoke.spec.ts +++ b/apps/examples/e2e/tests/test-smoke.spec.ts @@ -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 diff --git a/apps/examples/e2e/tests/test-toolbar.spec.ts b/apps/examples/e2e/tests/test-toolbar.spec.ts new file mode 100644 index 000000000..ac3f68e13 --- /dev/null +++ b/apps/examples/e2e/tests/test-toolbar.spec.ts @@ -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) + } + }) + } + }) +}) diff --git a/apps/examples/e2e/tests/test-ui.spec.ts b/apps/examples/e2e/tests/test-ui.spec.ts deleted file mode 100644 index 8a9fe2b01..000000000 --- a/apps/examples/e2e/tests/test-ui.spec.ts +++ /dev/null @@ -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') - }) -}) diff --git a/lerna.json b/lerna.json index 831cdb9e4..3088f6efd 100644 --- a/lerna.json +++ b/lerna.json @@ -1,7 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "packages": [ - "packages/*" - ], + "packages": ["packages/*"], "version": "2.0.0-beta.3" } diff --git a/packages/tldraw/src/lib/ui/components/MobileStylePanel.tsx b/packages/tldraw/src/lib/ui/components/MobileStylePanel.tsx index 94a330f14..28feb384d 100644 --- a/packages/tldraw/src/lib/ui/components/MobileStylePanel.tsx +++ b/packages/tldraw/src/lib/ui/components/MobileStylePanel.tsx @@ -51,10 +51,12 @@ export function MobileStylePanel() {
) : ( -
+
{color === undefined ? null : ( @@ -181,7 +181,7 @@ const OverflowToolsContent = track(function OverflowToolsContent({ const msg = useTranslation() return ( -
+
{toolbarItems.map(({ toolItem: { id, meta, kbd, label, onSelect, icon } }) => { return ( +
{items.map((item) => (