diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index 891b2a4e31..1cfaf1e524 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -108,8 +108,12 @@ export class WorkspacePage extends BaseWebSocketPage { async waitForIdle(options) { await this.page.evaluate( - (options) => new Promise( - (resolve) => globalThis.requestIdleCallback(resolve, options)), options); + (options) => + new Promise((resolve) => + globalThis.requestIdleCallback(resolve, options), + ), + options, + ); } }; @@ -229,7 +233,7 @@ export class WorkspacePage extends BaseWebSocketPage { async #waitForWebSocketReadiness(pageName) { // TODO: find a better event to settle whether the app is ready to receive notifications via ws - await expect(this.pageName).toHaveText(pageName, { timeout: 30000 }) + await expect(this.pageName).toHaveText(pageName, { timeout: 30000 }); } async sendPresenceMessage(fixture) { @@ -448,6 +452,33 @@ export class WorkspacePage extends BaseWebSocketPage { await pagesToggle.click(); } + async selectToolbarTool(workspacePage, toolName) { + await workspacePage.page + .getByRole("button", { name: toolName }) + .first() + .click(); + } + + async selectToolFromFlyout( + workspacePage, + { triggerToolName, targetToolName }, + ) { + const trigger = workspacePage.page + .getByRole("button", { name: triggerToolName }) + .first(); + + const option = workspacePage.page + .getByRole("menuitemradio", { name: targetToolName }) + .first(); + + await trigger.hover(); + // Flyout opening is delayed by 350ms in the toolbar component. + await workspacePage.page.waitForTimeout(450); + await expect(trigger).toHaveAttribute("aria-expanded", "true"); + await option.waitFor({ state: "visible" }); + await option.click(); + } + async moveSelectionToShape(name) { await this.page.locator("rect.viewport-selrect").hover(); await this.page.mouse.down(); diff --git a/frontend/playwright/ui/specs/toolbar.spec.js b/frontend/playwright/ui/specs/toolbar.spec.js new file mode 100644 index 0000000000..2bd9cef5c4 --- /dev/null +++ b/frontend/playwright/ui/specs/toolbar.spec.js @@ -0,0 +1,88 @@ +import { test, expect } from "@playwright/test"; +import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; +import WorkspacePage from "../pages/WorkspacePage"; + +test.beforeEach(async ({ page }) => { + await WasmWorkspacePage.init(page); +}); + +const expectLayerNamed = async (workspacePage, name) => { + await expect(workspacePage.layers.getByText(name).last()).toBeVisible(); +}; + +test("User creates a frame with the toolbar frame tool", async ({ page }) => { + const workspacePage = new WasmWorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.goToWorkspace(); + + await workspacePage.selectToolbarTool(workspacePage, "Board (B)"); + await workspacePage.clickWithDragViewportAt(100, 100, 180, 120); + await expectLayerNamed(workspacePage, "Board"); +}); + +test("User creates a rectangle with the toolbar rect tool", async ({ + page, +}) => { + const workspacePage = new WasmWorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.goToWorkspace(); + + await workspacePage.selectToolbarTool(workspacePage, "Rectangle (R)"); + await workspacePage.clickWithDragViewportAt(350, 100, 120, 80); + await expectLayerNamed(workspacePage, "Rectangle"); +}); + +test("User creates an ellipse from the shapes flyout", async ({ page }) => { + const workspacePage = new WasmWorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.goToWorkspace(); + + await workspacePage.selectToolFromFlyout(workspacePage, { + triggerToolName: "Rectangle (R)", + targetToolName: "Ellipse (E)", + }); + await workspacePage.clickWithDragViewportAt(520, 100, 100, 100); + await expectLayerNamed(workspacePage, "Ellipse"); +}); + +test("User creates a text shape with the toolbar text tool", async ({ + page, +}) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.goToWorkspace(); + + await workspacePage.selectToolbarTool(workspacePage, "Text (T)"); + await workspacePage.clickAndMove(120, 320, 300, 380); + await workspacePage.waitForSelectedShapeName("Text"); + await workspacePage.page.keyboard.type("toolbar test"); + await workspacePage.page.keyboard.press("Escape"); + await expectLayerNamed(workspacePage, "Text"); +}); + +test.skip("User creates a path with the toolbar path tool", async ({ + page, +}) => { + const workspacePage = new WasmWorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.goToWorkspace(); + + await workspacePage.selectToolbarTool(workspacePage, "Path (P)"); + await workspacePage.clickAndMove(120, 320, 300, 380); + await workspacePage.page.keyboard.press("Enter"); + await expectLayerNamed(workspacePage, "Path"); +}); + +test("User creates a curve from the path flyout", async ({ page }) => { + const workspacePage = new WasmWorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.goToWorkspace(); + + await workspacePage.selectToolFromFlyout(workspacePage, { + triggerToolName: "Path (P)", + targetToolName: "Curve (Shift+C)", + }); + await workspacePage.clickAndMove(120, 320, 300, 380); + await workspacePage.page.keyboard.press("Enter"); + await expectLayerNamed(workspacePage, "Path"); +}); diff --git a/frontend/playwright/ui/specs/variants.spec.js b/frontend/playwright/ui/specs/variants.spec.js index 0c50f7dc8b..d9dbb687e5 100644 --- a/frontend/playwright/ui/specs/variants.spec.js +++ b/frontend/playwright/ui/specs/variants.spec.js @@ -343,7 +343,9 @@ test("User drag and drop a variant outside the container", async ({ page }) => { // and use it to calculate the target position await workspacePage.clickWithDragViewportAt(600, 500, 0, 300); - await expect(workspacePage.layers.getByText("Rectangle / Value 1")).toBeVisible(); + await expect( + workspacePage.layers.getByText("Rectangle / Value 1"), + ).toBeVisible(); }); test("User cut paste a component inside a variant", async ({ page }) => { @@ -353,7 +355,10 @@ test("User cut paste a component inside a variant", async ({ page }) => { const variant = await findVariant(workspacePage, 0); //Create a component - await workspacePage.ellipseShapeButton.click(); + await workspacePage.selectToolFromFlyout(workspacePage, { + triggerToolName: "Rectangle (R)", + targetToolName: "Ellipse (E)", + }); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickLeafLayer("Ellipse"); await workspacePage.page.keyboard.press("ControlOrMeta+k"); @@ -384,7 +389,10 @@ test("User cut paste a component with path inside a variant", async ({ const variant = await findVariant(workspacePage, 0); // Create a component - await workspacePage.ellipseShapeButton.click(); + await workspacePage.selectToolFromFlyout(workspacePage, { + triggerToolName: "Rectangle (R)", + targetToolName: "Ellipse (E)", + }); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickLeafLayer("Ellipse"); await workspacePage.page.keyboard.press("ControlOrMeta+k"); @@ -425,7 +433,10 @@ test("User drag and drop a component with path inside a variant", async ({ const variant = findVariantNoWait(workspacePage, 0); //Create a component - await workspacePage.ellipseShapeButton.click(); + await workspacePage.selectToolFromFlyout(workspacePage, { + triggerToolName: "Rectangle (R)", + targetToolName: "Ellipse (E)", + }); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickLeafLayer("Ellipse"); await workspacePage.page.keyboard.press("ControlOrMeta+k"); @@ -457,7 +468,10 @@ test("User cut paste a variant into another container", async ({ page }) => { await setupVariantsFileWithVariant(workspacePage); // Create anothe variant - await workspacePage.ellipseShapeButton.click(); + await workspacePage.selectToolFromFlyout(workspacePage, { + triggerToolName: "Rectangle (R)", + targetToolName: "Ellipse (E)", + }); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickLeafLayer("Ellipse"); await workspacePage.page.keyboard.press("ControlOrMeta+k"); diff --git a/frontend/src/app/main/ui/ds/tool_toolbar/tool_toolbar.cljs b/frontend/src/app/main/ui/ds/tool_toolbar/tool_toolbar.cljs index c4907127d1..9d5e9b2581 100644 --- a/frontend/src/app/main/ui/ds/tool_toolbar/tool_toolbar.cljs +++ b/frontend/src/app/main/ui/ds/tool_toolbar/tool_toolbar.cljs @@ -217,6 +217,7 @@ plugins-enabled (features/active-feature? @st/state "plugins/runtime") rulers-enabled (mf/deref refs/rulers?) toolbar-hidden (mf/deref toolbar-hidden-ref) + read-only? (mf/use-ctx ctx/workspace-read-only?) display-plugins-manager (mf/use-fn (fn [] (st/emit! @@ -253,62 +254,64 @@ (dom/blur! (dom/get-target event)) (st/emit! (dwc/toggle-toolbar-visibility))))] - [:div {:role "toolbar" - :aria-label (tr "workspace.toolbar.label") - :tabindex "0" - :class (stl/css-case :main-toolbar true - :main-toolbar-no-rulers (not rulers-enabled) - :main-toolbar-hidden toolbar-hidden)} - [:ul {:class (stl/css :main-toolbar-options)} - [:li {:class (stl/css :main-toolbar-option)} - [:> tool-button* {:title (tr "workspace.toolbar.move" (sc/get-tooltip :move)) - :selected (and (nil? selected-drawing-tool) - (not selected-edition)) - :icon i/move - :on-click on-interrupt}]] + (when-not ^boolean read-only? + [:div {:role "toolbar" + :aria-label (tr "workspace.toolbar.label") + :tabindex "0" + :class (stl/css-case :main-toolbar true + :main-toolbar-no-rulers (not rulers-enabled) + :main-toolbar-hidden toolbar-hidden)} + [:ul {:class (stl/css :main-toolbar-options) + :data-testid "toolbar-options"} + [:li {:class (stl/css :main-toolbar-option)} + [:> tool-button* {:title (tr "workspace.toolbar.move" (sc/get-tooltip :move)) + :selected (and (nil? selected-drawing-tool) + (not selected-edition)) + :icon i/move + :on-click on-interrupt}]] - [:li {:class (stl/css :main-toolbar-option)} - [:> tool-button* {:title (tool-label :frame) - :selected (= selected-drawing-tool :frame) - :icon i/board - :on-click on-select-tool - :data-tool "frame"}]] + [:li {:class (stl/css :main-toolbar-option)} + [:> tool-button* {:title (tool-label :frame) + :selected (= selected-drawing-tool :frame) + :icon i/board + :on-click on-select-tool + :data-tool "frame"}]] - [:> grouped-tool-flyout* {:key :shapes - :group (get grouped-tools :shapes) - :drawtool selected-drawing-tool - :on-select-tool on-select-tool}] + [:> grouped-tool-flyout* {:key :shapes + :group (get grouped-tools :shapes) + :drawtool selected-drawing-tool + :on-select-tool on-select-tool}] - [:li {:class (stl/css :main-toolbar-option)} - [:> tool-button* {:title (tool-label :text) - :selected (= selected-drawing-tool :text) - :icon i/text - :on-click on-select-tool - :data-tool "text"}]] + [:li {:class (stl/css :main-toolbar-option)} + [:> tool-button* {:title (tool-label :text) + :selected (= selected-drawing-tool :text) + :icon i/text + :on-click on-select-tool + :data-tool "text"}]] - [:> image-upload-tool] + [:> image-upload-tool] - [:> grouped-tool-flyout* {:key :free-draw - :group (get grouped-tools :free-draw) - :drawtool selected-drawing-tool - :on-select-tool on-select-tool}] + [:> grouped-tool-flyout* {:key :free-draw + :group (get grouped-tools :free-draw) + :drawtool selected-drawing-tool + :on-select-tool on-select-tool}] - (when plugins-enabled - [:li {:class (stl/css :main-toolbar-option :main-toolbar-option-plugins)} - [:> tool-button* {:title (tool-label :plugins) - :icon i/puzzle - :on-click display-plugins-manager - :data-tool "plugins"}]]) + (when plugins-enabled + [:li {:class (stl/css :main-toolbar-option :main-toolbar-option-plugins)} + [:> tool-button* {:title (tool-label :plugins) + :icon i/puzzle + :on-click display-plugins-manager + :data-tool "plugins"}]]) - (when *assert* - [:li {:class (stl/css :main-toolbar-option :main-toolbar-option-debug)} - [:> tool-button* {:title (tool-label :debug) - :selected (contains? layout :debug-panel) - :icon i/bug - :on-click toggle-debug-panel}]])] - [:button {:title (tr "workspace.toolbar.toggle-toolbar") - :aria-label (tr "workspace.toolbar.toggle-toolbar") - :class (stl/css :toolbar-handler) - :on-click toggle-toolbar} - [:div {:class (stl/css :toolbar-handler-indicator)}]]])) + (when *assert* + [:li {:class (stl/css :main-toolbar-option :main-toolbar-option-debug)} + [:> tool-button* {:title (tool-label :debug) + :selected (contains? layout :debug-panel) + :icon i/bug + :on-click toggle-debug-panel}]])] + [:button {:title (tr "workspace.toolbar.toggle-toolbar") + :aria-label (tr "workspace.toolbar.toggle-toolbar") + :class (stl/css :toolbar-handler) + :on-click toggle-toolbar} + [:div {:class (stl/css :toolbar-handler-indicator)}]]]))) diff --git a/frontend/src/app/main/ui/ds/tool_toolbar/tool_toolbar.scss b/frontend/src/app/main/ui/ds/tool_toolbar/tool_toolbar.scss index 63eb1203cb..6a5c32d9d5 100644 --- a/frontend/src/app/main/ui/ds/tool_toolbar/tool_toolbar.scss +++ b/frontend/src/app/main/ui/ds/tool_toolbar/tool_toolbar.scss @@ -11,7 +11,7 @@ .main-toolbar { --toolbar-position-y: #{$sz-28}; - --toolbar-offset-y: 0; + --toolbar-offset-y: 0px; --menu-border-color: var(--color-background-quaternary); --menu-background-color: var(--color-background-primary); --stroke-color: var(--color-foreground-secondary);