Files
upstream-tldraw/apps/examples/e2e/tests/test-kbds.spec.ts
upstream-mirror a774ac673a
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

892 lines
25 KiB
TypeScript

import { Page, expect } from '@playwright/test'
import { Editor } from 'tldraw'
import test from '../fixtures/fixtures'
import { setupPage, setupPageWithShapes } from '../shared-e2e'
declare const editor: Editor
declare const __tldraw_ui_event: { name: string }
// We're just testing the events, not the actual results.
let page: Page
test.describe('Keyboard Shortcuts', () => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
await setupPage(page)
})
test('tools', async ({ isMobile }) => {
const geoToolKds = [
['r', 'rectangle'],
['o', 'ellipse'],
]
for (const [key, geo] of geoToolKds) {
await page.keyboard.press('v') // set back to select
await page.keyboard.press(key)
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'select-tool',
data: { id: `geo-${geo}`, source: 'kbd' },
})
}
const simpleToolKbds = [
['v', 'select'],
['h', 'hand'],
]
for (const [key, tool] of simpleToolKbds) {
await page.keyboard.press('v') // set back to select
await page.keyboard.press(key)
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'select-tool',
data: { id: tool, source: 'kbd' },
})
}
const shapeToolKbds = [
['d', 'draw'],
['x', 'draw'],
['a', 'arrow'],
['l', 'line'],
['f', 'frame'],
['n', 'note'],
['f', 'frame'],
['e', 'eraser'],
['k', 'laser'],
['t', 'text'],
]
for (const [key, tool] of shapeToolKbds) {
await page.keyboard.press('v') // set back to select
await page.keyboard.press(key)
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'select-tool',
data: { id: tool, source: 'kbd' },
})
}
// make sure that the first dropdown item is rectangle
await page.keyboard.press('r')
const positionalToolKbds = [
['1', 'select'],
['2', 'hand'],
['3', 'draw'],
['4', 'eraser'],
['5', 'arrow'],
['6', 'text'],
]
if (isMobile) {
// on mobile, the last item (first from the dropdown) is 6
positionalToolKbds.push(['7', 'geo-rectangle'])
} else {
// on desktop, the last item (first from the dropdown) is 9
positionalToolKbds.push(['9', 'geo-rectangle'])
}
for (const [key, tool] of positionalToolKbds) {
await page.keyboard.press('v') // set back to select
await page.keyboard.press(key)
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'select-tool',
data: { id: tool, source: 'toolbar' },
})
}
})
})
test.describe('Keyboard Shortcuts', () => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
await setupPage(page)
// Make some shapes
await page.keyboard.press('r')
await page.mouse.click(100, 100)
await page.keyboard.press('r')
await page.mouse.click(250, 250)
await page.keyboard.press('v')
})
test('Zoom in', async () => {
await page.keyboard.press('Control+=')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'zoom-in',
data: { source: 'kbd' },
})
})
test('Zoom out', async () => {
await page.keyboard.press('Control+-')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'zoom-out',
data: { source: 'kbd' },
})
})
test('Zoom to fit', async () => {
await page.keyboard.press('Shift+1')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'zoom-to-fit',
data: { source: 'kbd' },
})
})
test('Zoom to selection', async () => {
await page.keyboard.press('Shift+2')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'zoom-to-selection',
data: { source: 'kbd' },
})
})
test('Zoom to 100', async () => {
await page.keyboard.press('Shift+0')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'reset-zoom',
data: { source: 'kbd' },
})
})
/* ---------------------- Files --------------------- */
// new-project — Cmd+N
// open — Cmd+O
// save — Cmd+S
// save-as — Cmd+Shift+S
// upload-media — Cmd+I
/* -------------------- Clipboard ------------------- */
// await page.keyboard.press('Control+c')
// expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
// name: 'copy',
// data: { source: 'kbd' },
// })
// await page.keyboard.press('Control+v')
// expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
// name: 'paste',
// data: { source: 'kbd' },
// })
// await page.keyboard.press('Control+x')
// expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
// name: 'cut',
// data: { source: 'kbd' },
// })
test('Toggle grid mode', async () => {
await page.keyboard.press("Control+'")
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'toggle-grid-mode',
data: { source: 'kbd' },
})
})
test('Toggle dark mode', async () => {
await page.keyboard.press('Control+/')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'color-scheme',
data: { source: 'kbd' },
})
})
test('Toggle tool lock', async () => {
await page.keyboard.press('q')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'toggle-tool-lock',
data: { source: 'kbd' },
})
})
test('Copy hovered styles', async () => {
// The action copies styles from the shape under the pointer, so create a shape in the
// middle of the viewport and move the mouse over it before pressing the shortcut.
const point = await page.evaluate(() => {
const center = editor.getViewportPageBounds().center
editor.createShape({
type: 'geo',
x: center.x - 50,
y: center.y - 50,
props: { w: 100, h: 100, fill: 'solid' },
})
const screen = editor.pageToScreen(center)
return { x: screen.x, y: screen.y }
})
await page.mouse.move(point.x, point.y)
await page.keyboard.press('Shift+q')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'copy-hovered-styles',
data: { source: 'kbd' },
})
})
})
test.describe('Actions on shapes', () => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
await setupPage(page)
})
/* -------------- Operations on Shapes -------------- */
test('Operations on shapes', async () => {
await setupPageWithShapes(page)
// needs shapes on the canvas
// select-all — Cmd+A
await page.keyboard.press('Control+a')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'select-all-shapes',
data: { source: 'kbd' },
})
// flip-h — Shift+H
await page.keyboard.press('Shift+h')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'flip-shapes',
data: { operation: 'horizontal', source: 'kbd' },
})
// flip-v — Shift+V
await page.keyboard.press('Shift+v')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'flip-shapes',
data: { operation: 'vertical', source: 'kbd' },
})
// move-to-front — ]
await page.keyboard.press(']')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'reorder-shapes',
data: { operation: 'toFront', source: 'kbd' },
})
// move-forward — Alt+]
await page.keyboard.press('Alt+]')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'reorder-shapes',
data: { operation: 'forward', source: 'kbd' },
})
// move-to-back — [
await page.keyboard.press('[')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'reorder-shapes',
data: { operation: 'toBack', source: 'kbd' },
})
// move-backward — Alt+[
await page.keyboard.press('Alt+[')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'reorder-shapes',
data: { operation: 'backward', source: 'kbd' },
})
// group — Cmd+G
await page.keyboard.press('Control+g')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'group-shapes',
data: { source: 'kbd' },
})
// ungroup — Cmd+Shift+G
await page.keyboard.press('Control+Shift+g')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'ungroup-shapes',
data: { source: 'kbd' },
})
// duplicate — Cmd+D
await page.keyboard.press('Control+d')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'duplicate-shapes',
data: { source: 'kbd' },
})
// align left — Alt+A
await page.keyboard.press('Alt+a')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'align-shapes',
data: { operation: 'left', source: 'kbd' },
})
// align right — Alt+D
await page.keyboard.press('Alt+d')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'align-shapes',
data: { operation: 'right', source: 'kbd' },
})
// align top — Alt+W
await page.keyboard.press('Alt+w')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'align-shapes',
data: { operation: 'top', source: 'kbd' },
})
// align bottom — Alt+W'
await page.keyboard.press('Alt+s')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'align-shapes',
data: { operation: 'bottom', source: 'kbd' },
})
// Copy as SVG — this should have a clipboard error
// await page.keyboard.press('Control+Shift+c')
// expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
// name: 'copy-as',
// data: { format: 'svg', source: 'kbd' },
// })
// delete — backspace
await page.keyboard.press('Control+a') // selected
await page.keyboard.press('Backspace')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'delete-shapes',
data: { source: 'kbd' },
})
// delete — ⌫
// Make some shapes and select them
await page.keyboard.press('r')
await page.mouse.click(100, 100)
await page.keyboard.press('r')
await page.mouse.click(250, 250)
await page.keyboard.press('v')
await page.keyboard.press('Control+a')
await page.keyboard.press('Delete')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'delete-shapes',
data: { source: 'kbd' },
})
// Next, previous pages — Alt+ArrowLeft, Alt+ArrowRight
// Try a previous page move. We can't go lower since we're on the first page.
// So the most recent event should be the previous delete
await page.keyboard.press('Alt+ArrowLeft')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'delete-shapes',
data: { source: 'kbd' },
})
// Next page. Since there's only one page and the page is empty, nothing should happen.
await page.keyboard.press('Alt+ArrowRight')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'delete-shapes',
data: { source: 'kbd' },
})
// make something on the new page and delete it
await page.keyboard.press('r')
await page.mouse.click(100, 100)
// If there's something on the page, we can create the next page by moving up
await page.keyboard.press('Alt+ArrowRight')
// We'll also have a change page here... but the most recent will be the new page
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'new-page',
data: { source: 'kbd' },
})
// We can go back down...
await page.keyboard.press('Alt+ArrowLeft')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'change-page',
data: { source: 'kbd', direction: 'prev' },
})
// We can go up again
await page.keyboard.press('Alt+ArrowRight')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'change-page',
data: { source: 'kbd', direction: 'next' },
})
// We can back down and up with the up and down keys too...
await page.keyboard.press('Alt+ArrowUp')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'change-page',
data: { source: 'kbd', direction: 'prev' },
})
await page.keyboard.press('Alt+ArrowDown')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'change-page',
data: { source: 'kbd', direction: 'next' },
})
/* ---------------------- Misc ---------------------- */
// These actions require selected shapes to run
await page.keyboard.press('r')
await page.mouse.click(100, 100)
await page.keyboard.press('r')
await page.mouse.click(100, 300)
await page.keyboard.press('r')
await page.mouse.click(300, 300)
// First verify they don't fire when nothing is selected
await page.keyboard.press('Escape') // deselect all
await page.keyboard.press('Escape') // deselect all
const prevEvent = await page.evaluate(() => __tldraw_ui_event)
await page.keyboard.press('Shift+l')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject(prevEvent)
await page.keyboard.press('Control+Shift+Alt+=')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject(prevEvent)
await page.keyboard.press('Control+Shift+Alt+-')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject(prevEvent)
// Now select shapes and verify the actions fire
await page.keyboard.press('Control+a')
await page.keyboard.press('Control+Shift+Alt+=')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'enlarge-shapes',
})
await page.keyboard.press('Control+Shift+Alt+-')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'shrink-shapes',
})
// toggle lock (must be the last one here)
await page.keyboard.press('Shift+l')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'toggle-lock',
})
// await page.keyboard.press('Control+i')
// expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
// name: 'open-menu',
// data: { source: 'dialog' },
// })
// await page.keyboard.press('Control+u')
// expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
// name: 'open-menu',
// data: { source: 'dialog' },
// })
})
// TODO: Annoyingly, the alt key is being 'released' in the above test suite.
// I'll need to figure that out in a separate PR. In the meantime,
// this pulls out the rotate tests into a separate test suite.
test('rotate', async () => {
await setupPageWithShapes(page)
await page.keyboard.press('Control+a')
// rotate — Shift+ArrowLeft, Shift+ArrowRight
await page.keyboard.press('Shift+.')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'rotate-cw',
data: { source: 'kbd', fine: false },
})
await page.keyboard.press('Shift+,')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'rotate-ccw',
data: { source: 'kbd', fine: false },
})
// rotate — Shift+Alt+ArrowLeft, Shift+Alt+ArrowRight
await page.keyboard.press('Shift+Alt+.')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'rotate-cw',
data: { source: 'kbd', fine: true },
})
await page.keyboard.press('Shift+Alt+,')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'rotate-ccw',
data: { source: 'kbd', fine: true },
})
})
})
test.describe('Delete bug', () => {
test.beforeEach(async ({ browser }) => {
page = await browser.newPage()
await setupPage(page)
})
test('delete bug without drag', async () => {
await page.keyboard.press('r')
await page.mouse.click(100, 100)
await page.keyboard.press('Backspace')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'delete-shapes',
data: { source: 'kbd' },
})
})
test('delete bug with drag', async () => {
await page.keyboard.press('r')
await page.mouse.down()
await page.mouse.move(100, 100)
await page.mouse.up()
await page.keyboard.press('Backspace')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'delete-shapes',
data: { source: 'kbd' },
})
})
})
test.describe('Shape Navigation', () => {
test.beforeEach(async ({ browser }) => {
page = await browser.newPage()
await setupPage(page)
})
test('Tab navigation between shapes', async ({ isMobile }) => {
if (isMobile) return // can't test this on mobile
// Create multiple shapes
await page.keyboard.press('r')
await page.mouse.click(100, 100)
await page.keyboard.press('r')
await page.mouse.click(250, 100)
await page.keyboard.press('r')
await page.mouse.click(400, 100)
await page.keyboard.press('v')
// Click on the first shape to select it
await page.mouse.click(100, 100)
// Navigate forward with Tab
await page.keyboard.press('Tab')
expect(await page.evaluate(() => editor.getOnlySelectedShape())).toMatchObject({
x: 150,
y: 0,
})
// Navigate backward with Shift+Tab
await page.keyboard.press('Shift+Tab')
expect(await page.evaluate(() => editor.getOnlySelectedShape())).toMatchObject({
x: 0,
y: 0,
})
})
test('Directional navigation between shapes', async ({ isMobile }) => {
if (isMobile) return // can't test this on mobile
// Create shapes in different directions
await page.keyboard.press('r')
await page.mouse.click(200, 200) // Center shape
await page.keyboard.press('r')
await page.mouse.click(300, 200) // Right shape
await page.keyboard.press('r')
await page.mouse.click(100, 200) // Left shape
await page.keyboard.press('r')
await page.mouse.click(200, 100) // Up shape
await page.keyboard.press('r')
await page.mouse.click(200, 300) // Down shape
await page.keyboard.press('v')
// Select center shape
await page.mouse.click(200, 200)
// Test navigation to the right
await page.keyboard.press('Control+ArrowRight')
expect(await page.evaluate(() => editor.getOnlySelectedShape())).toMatchObject({
x: 200,
y: 100,
})
// Navigate back to center
await page.mouse.click(200, 200)
// Disabled: I think this conflicts with browser nav or something.
// // Test navigation to the left
// await page.keyboard.press('Control+ArrowLeft')
// expect(await page.evaluate(() => editor.getOnlySelectedShape())).toMatchObject({
// x: 200,
// y: 100
// })
// Navigate back to center
await page.mouse.click(200, 200)
// Test navigation up
await page.keyboard.press('Control+ArrowUp')
expect(await page.evaluate(() => editor.getOnlySelectedShape())).toMatchObject({
x: 100,
y: 100,
})
// Navigate back to center
await page.mouse.click(200, 200)
// Test navigation down
await page.keyboard.press('Control+ArrowDown')
expect(await page.evaluate(() => editor.getOnlySelectedShape())).toMatchObject({
x: 100,
y: 200,
})
})
test('Container navigation (entering and exiting containers)', async ({ isMobile }) => {
if (isMobile) return // can't test this on mobile
// Create a frame
await page.keyboard.press('f')
await page.mouse.down()
await page.mouse.move(300, 300)
await page.mouse.up()
// Create a shape inside the frame
await page.keyboard.press('r')
await page.mouse.click(200, 200)
await page.keyboard.press('v')
// Navigate up to the parent (frame) with Control+Shift+Up
await page.keyboard.press('Control+Shift+ArrowUp')
expect(
await page.evaluate(() => {
const shape = editor.getOnlySelectedShape()
return shape && shape.type === 'frame'
})
).toBe(true)
// Navigate down to the first child with Control+Shift+Down
await page.keyboard.press('Control+Shift+ArrowDown')
expect(
await page.evaluate(() => {
const shape = editor.getOnlySelectedShape()
return shape && shape.type === 'geo'
})
).toBe(true)
// Now test with a group
// Create multiple shapes to group
await page.keyboard.press('v')
await page.keyboard.press('r')
await page.mouse.click(450, 200)
await page.keyboard.press('r')
await page.mouse.click(475, 200)
// Select both shapes
await page.keyboard.press('v')
await page.mouse.move(310, 50)
await page.mouse.down()
await page.mouse.move(310, 50)
await page.mouse.move(700, 500)
await page.mouse.up()
// Group them
await page.keyboard.press('Control+g')
// Navigate up to the group with Control+Shift+Up
await page.keyboard.press('Control+Shift+ArrowUp')
expect(
await page.evaluate(() => {
const shape = editor.getOnlySelectedShape()
return shape && shape.type === 'group'
})
).toBe(true)
// Navigate down to a child in the group
await page.keyboard.press('Control+Shift+ArrowDown')
expect(
await page.evaluate(() => {
const shape = editor.getOnlySelectedShape()
return shape && shape.type === 'geo'
})
).toBe(true)
})
test('Tab navigation respects container boundaries', async ({ isMobile }) => {
if (isMobile) return // can't test this on mobile
// Clear the canvas and reset the view
await page.keyboard.press('Control+a')
await page.keyboard.press('Backspace')
await page.keyboard.press('v')
// Create a frame
await page.keyboard.press('f')
await page.mouse.move(100, 100)
await page.mouse.down()
await page.mouse.move(300, 300)
await page.mouse.up()
// Create shapes inside the frame
await page.keyboard.press('r')
await page.mouse.click(150, 150) // Shape inside frame
await page.keyboard.press('r')
await page.mouse.click(250, 150) // Another shape inside frame
// Create shapes outside the frame
await page.keyboard.press('r')
await page.mouse.click(400, 150) // Shape outside frame
await page.keyboard.press('r')
await page.mouse.click(400, 250) // Another shape outside frame
await page.keyboard.press('v')
// Test 1: Tab from outside shape should only navigate between outside shapes
await page.mouse.click(400, 150) // Select first outside shape
// Tab to next outside shape
await page.keyboard.press('Tab')
expect(
await page.evaluate(() => {
const shape = editor.getOnlySelectedShape()!
return { x: Math.round(shape.x), y: Math.round(shape.y) }
})
).toMatchObject({
x: 100,
y: 100,
})
// Tab again should wrap around to first outside shape
await page.keyboard.press('Tab')
expect(
await page.evaluate(() => {
const shape = editor.getOnlySelectedShape()!
return { x: Math.round(shape.x), y: Math.round(shape.y) }
})
).toMatchObject({
x: 300,
y: 50,
})
// Test 2: Tab from inside shape should only navigate between shapes in the same container
await page.mouse.click(150, 150) // Select first inside shape
// Tab to next shape inside frame
await page.keyboard.press('Tab')
expect(
await page.evaluate(() => {
const shape = editor.getOnlySelectedShape()!
return { x: Math.round(shape.x), y: Math.round(shape.y) }
})
).toMatchObject({
x: -50,
y: -50,
})
// Tab again should wrap around to first shape inside frame
await page.keyboard.press('Tab')
expect(
await page.evaluate(() => {
const shape = editor.getOnlySelectedShape()!
return { x: Math.round(shape.x), y: Math.round(shape.y) }
})
).toMatchObject({
x: 50,
y: -50,
})
})
test('Tab navigation with nested containers', async ({ isMobile }) => {
if (isMobile) return // can't test this on mobile
// Clear the canvas and reset the view
await page.keyboard.press('Control+a')
await page.keyboard.press('Backspace')
await page.keyboard.press('v')
// Create an outer frame
await page.keyboard.press('f')
await page.mouse.move(100, 100)
await page.mouse.down()
await page.mouse.move(500, 400)
await page.mouse.up()
// Create an inner frame inside the outer frame
await page.keyboard.press('f')
await page.mouse.move(150, 150)
await page.mouse.down()
await page.mouse.move(300, 300)
await page.mouse.up()
// Create shapes directly inside outer frame
await page.keyboard.press('r')
await page.mouse.click(400, 200) // Shape in outer frame
await page.keyboard.press('r')
await page.mouse.click(400, 300) // Another shape in outer frame
// Create shapes inside inner frame
await page.keyboard.press('r')
await page.mouse.click(200, 200) // Shape in inner frame
await page.keyboard.press('r')
await page.mouse.click(250, 200) // Another shape in inner frame
await page.keyboard.press('v')
// Test: Tab from inner frame shape should only navigate within inner frame
await page.mouse.click(200, 200) // Select first shape in inner frame
// Tab to next shape in inner frame
await page.keyboard.press('Tab')
expect(
await page.evaluate(() => {
const shape = editor.getOnlySelectedShape()!
const parent = editor.getShape(shape.parentId)
return {
shapeX: Math.round(shape.x),
shapeY: Math.round(shape.y),
isInInnerFrame:
parent && parent.type === 'frame' && parent.parentId !== editor.getCurrentPageId(),
}
})
).toMatchObject({
shapeX: 0,
shapeY: -50,
isInInnerFrame: true,
})
// Tab should never select shapes from outer frame
await page.keyboard.press('Tab')
expect(
await page.evaluate(() => {
const shape = editor.getOnlySelectedShape()!
const parent = editor.getShape(shape.parentId)
return {
isInInnerFrame:
parent && parent.type === 'frame' && parent.parentId !== editor.getCurrentPageId(),
}
})
).toMatchObject({
isInInnerFrame: true,
})
})
test('Tab navigation does not happen when alt key is pressed', async ({ isMobile }) => {
if (isMobile) return // can't test this on mobile
// Create multiple shapes
await page.keyboard.press('r')
await page.mouse.click(100, 100)
await page.keyboard.press('r')
await page.mouse.click(250, 100)
await page.keyboard.press('r')
await page.mouse.click(400, 100)
await page.keyboard.press('v')
// Click on the first shape to select it
await page.mouse.click(100, 100)
await page.keyboard.press('Alt+Tab')
expect(await page.evaluate(() => editor.getOnlySelectedShape())).toMatchObject({
x: 0,
y: 0,
})
})
})