snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
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

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
import { Locator, Page } from '@playwright/test'
export class ActionsMenu {
readonly quickActions: { [key: string]: Locator }
readonly mainActions: { [key: string]: Locator }
readonly actionsMenuButton: Locator
readonly actionsMenuContent: Locator
constructor(public readonly page: Page) {
this.page = page
this.actionsMenuButton = this.page.getByTestId('actions-menu.button')
this.actionsMenuContent = this.page.getByTestId('actions-menu.content')
this.quickActions = {
undo: this.page.getByTestId('quick-actions.undo'),
redo: this.page.getByTestId('quick-actions.redo'),
delete: this.page.getByTestId('quick-actions.delete'),
duplicate: this.page.getByTestId('quick-actions.duplicate'),
}
this.mainActions = {
alignLeft: this.page.getByTestId('actions-menu.align-left'),
alignCenterHorizontal: this.page.getByTestId('actions-menu.align-center-horizontal'),
alignRight: this.page.getByTestId('actions-menu.align-right'),
stretchHorizontal: this.page.getByTestId('actions-menu.stretch-horizontal'),
alignTop: this.page.getByTestId('actions-menu.align-top'),
alignCenterVertical: this.page.getByTestId('actions-menu.align-center-vertical'),
alignBottom: this.page.getByTestId('actions-menu.align-bottom'),
stretchVertical: this.page.getByTestId('actions-menu.stretch-vertical'),
distributeHorizontal: this.page.getByTestId('actions-menu.distribute-horizontal'),
distributeVertical: this.page.getByTestId('actions-menu.distribute-vertical'),
stackHorizontal: this.page.getByTestId('actions-menu.stack-horizontal'),
stackVertical: this.page.getByTestId('actions-menu.stack-vertical'),
sendToBack: this.page.getByTestId('actions-menu.send-to-back'),
sendBackward: this.page.getByTestId('actions-menu.send-backward'),
bringForward: this.page.getByTestId('actions-menu.bring-forward'),
bringToFront: this.page.getByTestId('actions-menu.bring-to-front'),
rotateCCW: this.page.getByTestId('actions-menu.rotate-ccw'),
rotateCW: this.page.getByTestId('actions-menu.rotate-cw'),
editLink: this.page.getByTestId('actions-menu.edit-link'),
group: this.page.getByTestId('actions-menu.group'),
}
}
}

View File

@@ -0,0 +1,34 @@
import { Locator, Page } from '@playwright/test'
export class HelpMenu {
readonly helpMenuButton: Locator
readonly languagesButton: Locator
readonly languagesContent: Locator
readonly keyboardShortcutsMenu: KeyboardShortcutsMenu
readonly linkGroup: { [key: string]: Locator }
constructor(public readonly page: Page) {
this.page = page
this.helpMenuButton = this.page.getByTestId('help-menu.button')
this.languagesButton = this.page.getByTestId('help-menu-sub.language-button')
this.languagesContent = this.page.getByTestId('help-menu-sub.language-content')
this.keyboardShortcutsMenu = new KeyboardShortcutsMenu(this.page)
this.linkGroup = {
github: this.page.getByTestId('help-menu.github'),
twitter: this.page.getByTestId('help-menu.twitter'),
discord: this.page.getByTestId('help-menu.discord'),
about: this.page.getByTestId('help-menu.about'),
}
}
}
class KeyboardShortcutsMenu {
readonly heading: Locator
readonly button: Locator
readonly closeButton: Locator
constructor(private readonly page: Page) {
this.heading = this.page.getByRole('dialog').getByText('Keyboard shortcuts')
this.button = this.page.getByTestId('help-menu.keyboard-shortcuts-button')
this.closeButton = this.page.getByRole('dialog').getByTestId('dialog.close')
}
}

View File

@@ -0,0 +1,62 @@
import { Locator, Page } from '@playwright/test'
export class MainMenu {
readonly mainMenuButton: Locator
readonly buttons: { [key: string]: Locator }
readonly subMenus: { [key: string]: Locator[] }
constructor(public readonly page: Page) {
this.page = page
this.mainMenuButton = this.page.getByTestId('main-menu.button')
this.buttons = {
edit: this.page.getByRole('menuitem').getByText('Edit'),
object: this.page.getByRole('menuitem').getByText('Object'),
view: this.page.getByRole('menuitem').getByText('View'),
insertEmbed: this.page.getByRole('menuitem').getByText('Insert Embed'),
uploadMedia: this.page.getByRole('menuitem').getByText('Upload Media'),
preferences: this.page.getByRole('menuitem').getByText('Preferences'),
}
this.subMenus = {
editSubmenu: [
this.page.getByTestId('main-menu-sub.edit-content').getByText('Undo'),
this.page.getByTestId('main-menu-sub.edit-content').getByText('Redo'),
this.page.getByTestId('main-menu-sub.edit-content').getByText('Cut'),
this.page.getByTestId('main-menu-sub.edit-content').getByText('Copy'),
this.page.getByTestId('main-menu-sub.edit-content').getByText('Copy as'),
this.page.getByTestId('main-menu-sub.edit-content').getByText('Duplicate'),
this.page.getByTestId('main-menu-sub.edit-content').getByText('Paste'),
this.page.getByTestId('main-menu-sub.edit-content').getByText('Delete'),
this.page.getByTestId('main-menu-sub.edit-content').getByText('Select all'),
],
objectSubmenu: [
this.page.getByRole('menuitem').getByText('Export as'),
this.page.getByRole('menuitem').getByText('Group'),
this.page.getByRole('menuitem').getByText('Ungroup'),
this.page.getByRole('menuitem').getByText('Remove frame'),
this.page.getByRole('menuitem').getByText('Fit to content'),
this.page.getByRole('menuitem').getByText('Toggle auto size'),
this.page.getByRole('menuitem').getByText('Edit link'),
this.page.getByRole('menuitem').getByText('Convert to Bookmark'),
this.page.getByRole('menuitem').getByText('Convert to Embed'),
this.page.getByRole('menuitem').getByText('Toggle locked'),
this.page.getByRole('menuitem').getByText('Unlock all'),
],
viewSubmenu: [
this.page.getByTestId('main-menu-sub.view-content').getByText('Zoom in'),
this.page.getByTestId('main-menu-sub.view-content').getByText('Zoom out'),
this.page.getByTestId('main-menu-sub.view-content').getByText('Zoom to 100%'),
this.page.getByTestId('main-menu-sub.view-content').getByText('Zoom to fit'),
this.page.getByTestId('main-menu-sub.view-content').getByText('Zoom to selection'),
],
preferencesSubmenu: [
this.page.getByTestId('main-menu-sub.preferences-content').getByText('Always snap'),
this.page.getByTestId('main-menu-sub.preferences-content').getByText('Tool lock'),
this.page.getByTestId('main-menu-sub.preferences-content').getByText('Show grid'),
this.page.getByTestId('main-menu-sub.preferences-content').getByText('Dark mode'),
this.page.getByTestId('main-menu-sub.preferences-content').getByText('Focus mode'),
this.page.getByTestId('main-menu-sub.preferences-content').getByText('Edge scrolling'),
this.page.getByTestId('main-menu-sub.preferences-content').getByText('Reduce motion'),
this.page.getByTestId('main-menu-sub.preferences-content').getByText('Language'),
],
}
}
}

View File

@@ -0,0 +1,10 @@
import { Locator, Page } from '@playwright/test'
export class MenuClickCapture {
readonly content: Locator
constructor(public readonly page: Page) {
this.page = page
this.content = this.page.getByTestId('menu-click-capture.content')
}
}

View File

@@ -0,0 +1,21 @@
import { Locator, Page } from '@playwright/test'
export class NavigationPanel {
readonly minimap: Locator
readonly zoomMenuButton: Locator
readonly toggleButton: Locator
readonly zoomMenuItems: { [key: string]: Locator }
constructor(private readonly page: Page) {
this.page = page
this.minimap = page.getByTestId('minimap.canvas')
this.zoomMenuButton = page.getByTestId('minimap.zoom-menu-button')
this.toggleButton = page.getByTestId('minimap.toggle-button')
this.zoomMenuItems = {
zoomIn: page.getByRole('menuitem').getByText('Zoom in'),
zoomOut: page.getByRole('menuitem').getByText('Zoom out'),
zoomToHundred: page.getByRole('menuitem').getByText('Zoom to 100%'),
zoomToFit: page.getByRole('menuitem').getByText('Zoom to fit'),
zoomToSelection: page.getByRole('menuitem').getByText('Zoom to selection'),
}
}
}

View File

@@ -0,0 +1,27 @@
import { Locator, Page } from '@playwright/test'
export class PageMenu {
readonly pagemenuButton: Locator
readonly header: Locator
readonly createButton: Locator
readonly pageList: Locator
readonly pageItems: Locator
constructor(public readonly page: Page) {
this.page = page
this.pagemenuButton = this.page.getByTestId('page-menu.button')
// The popover no longer has a title row — use the list as the "is the menu open" anchor.
this.header = this.page.getByTestId('page-menu.list')
this.createButton = this.page.getByTestId('page-menu.create')
this.pageList = this.page.getByTestId('page-menu.list')
this.pageItems = this.page.getByTestId('page-menu.item')
}
async getPageItem(index: number) {
return this.pageItems.nth(index)
}
getPageItemByName(name: string) {
return this.page.getByTestId('page-menu.item').filter({ hasText: name })
}
}

View File

@@ -0,0 +1,60 @@
import { Locator, Page, expect } from '@playwright/test'
export class RichTextToolbar {
container: Locator
readonly tools: { [key: string]: Locator }
constructor(public readonly page: Page) {
this.container = this.page.getByTestId('contextual-toolbar')
this.tools = {
bold: this.page.getByTestId('rich-text.bold'),
italic: this.page.getByTestId('rich-text.italic'),
code: this.page.getByTestId('rich-text.code'),
strike: this.page.getByTestId('rich-text.strike'),
link: this.page.getByTestId('rich-text.link'),
bulletList: this.page.getByTestId('rich-text.bulletList'),
highlight: this.page.getByTestId('rich-text.highlight'),
linkRemove: this.page.getByTestId('rich-text.link-remove'),
heading: this.page.getByTestId('rich-text.heading'),
}
}
async clickTool(tool: Locator) {
await tool.click()
}
async isSelected(tool: Locator) {
// pseudo elements aren't exposed to the DOM, but we can check the color as a proxy
const expectedColor = 'rgb(46, 46, 46)'
await expect(tool).toHaveCSS('color', expectedColor)
}
async isNotSelected(tool: Locator) {
const expectedColor = 'rgb(255, 255, 255)'
await expect(tool).toHaveCSS('color', expectedColor)
}
async getSelectionBounds() {
return this.page.evaluate(() => {
const selection = window.getSelection()
// If there are no selections, don't return a box
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return
// Get a common box from all of the ranges' screen rects
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
for (let i = 0; i < selection.rangeCount; i++) {
const range = selection.getRangeAt(i)
const box = range.getBoundingClientRect()
minX = Math.min(minX, box.left)
minY = Math.min(minY, box.top)
maxX = Math.max(maxX, box.right)
maxY = Math.max(maxY, box.bottom)
}
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY }
})
}
}

View File

@@ -0,0 +1,88 @@
import { Locator, Page, expect } from '@playwright/test'
export class StylePanel {
readonly stylesArray: string[]
readonly colors: { [key: string]: Locator }
readonly fill: { [key: string]: Locator }
readonly fillExtra: 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
this.stylesArray = [
'style.color',
'style.opacity',
'style.fill',
'style.dash',
'style.size',
'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'),
}
this.fillExtra = this.page.getByTestId('style.fill-extra')
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 isActive(style: Locator) {
const backgroundColor = await this.getAfterElementStyle(style, 'background-color')
return expect(backgroundColor).toBe('rgba(0, 0, 0, 0.055)')
}
async isInactive(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.getByTestId('style.panel')
}
}

View File

@@ -0,0 +1,45 @@
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.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.getByTestId('tools.select'),
draw: this.page.getByTestId('tools.draw'),
text: this.page.getByTestId('tools.text'),
arrow: this.page.getByTestId('tools.arrow'),
cloud: this.page.getByTestId('tools.cloud'),
eraser: this.page.getByTestId('tools.eraser'),
rectangle: this.page.getByTestId('tools.rectangle'),
hand: this.page.getByTestId('tools.hand'),
}
this.popOverTools = {
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) {
// pseudo elements aren't exposed to the DOM, but we can check the color as a proxy
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)
}
}