Add playwright tests (#1484)
This PR replaces our webdriver end to end tests with playwright tests. It: - replaces our webdriver workflow with a new e2e workflow based on playwright - removes the webdriver project - adds e2e tests to our examples app - replaces all `data-wd` attributes with `data-testid` ### Coverage Most of the tests from our previous e2e tests are reproduced here, though there are some related to our gestures that will need to be done in a different way—or not at all. I've also added a handful of new tests, too. ### Where are they The tests are now part of our examples app rather than being in its own different app. This should help us test our different examples too. As far as I can tell there are no downsides here in terms of the regular developer experience, though they might complicate any CodeSandbox projects that are hooked into the examples app. ### Change Type - [x] `tests` — Changes to any testing-related code only (will not publish a new version)
This commit is contained in:
parent
e3dec58499
commit
e3cf05f408
99 changed files with 1759 additions and 9253 deletions
|
@ -5,7 +5,7 @@ module.exports = {
|
|||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@next/next/core-web-vitals',
|
||||
],
|
||||
ignorePatterns: ['e2e/wdio.*.js'],
|
||||
ignorePatterns: [],
|
||||
plugins: ['@typescript-eslint', 'no-only-tests', 'import', 'local', '@next/next', 'react-hooks'],
|
||||
settings: {
|
||||
next: {
|
||||
|
|
8
.github/workflows/checks.yml
vendored
8
.github/workflows/checks.yml
vendored
|
@ -4,7 +4,7 @@ on:
|
|||
pull_request:
|
||||
merge_group:
|
||||
push:
|
||||
branches: [main, lite]
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CI: 1
|
||||
|
@ -54,8 +54,8 @@ jobs:
|
|||
# turbo's prefix
|
||||
run: "yarn build | sed -E 's/^.*? ::/::/'"
|
||||
|
||||
- name: Pack public packages
|
||||
run: "yarn lazy pack-tarball | sed -E 's/^.*? ::/::/'"
|
||||
|
||||
- name: Test
|
||||
run: yarn test
|
||||
|
||||
- name: Pack public packages
|
||||
run: "yarn lazy pack-tarball | sed -E 's/^.*? ::/::/'"
|
||||
|
|
56
.github/workflows/playwright.yml
vendored
Normal file
56
.github/workflows/playwright.yml
vendored
Normal file
|
@ -0,0 +1,56 @@
|
|||
name: Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CI: 1
|
||||
PRINT_GITHUB_ANNOTATIONS: 1
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: 'End to end tests'
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest-16-cores-open # TODO: this should probably run on multiple OSes
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'public-yarn.lock'
|
||||
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Build all projects
|
||||
# the sed pipe makes sure that github annotations come through without
|
||||
# turbo's prefix
|
||||
run: "yarn build | sed -E 's/^.*? ::/::/'"
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: yarn e2e
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
67
.github/workflows/webdriver.yml
vendored
67
.github/workflows/webdriver.yml
vendored
|
@ -1,67 +0,0 @@
|
|||
name: Webdriver checks
|
||||
|
||||
on:
|
||||
merge_group:
|
||||
pull_request:
|
||||
branches: [main, production]
|
||||
push:
|
||||
branches: [main, production]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: 'test/standalone-${{ matrix.browser }} (${{ matrix.os }})'
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest-16-cores-open]
|
||||
node-version: [16]
|
||||
browser: [chrome]
|
||||
browser-version: ['111.0']
|
||||
|
||||
container:
|
||||
image: node:${{ matrix.node-version }}
|
||||
options: --network-alias testhost
|
||||
volumes:
|
||||
- /home/runner/work/_temp/e2e:/home/runner/work/_temp/e2e
|
||||
|
||||
services:
|
||||
selenium:
|
||||
image: selenium/standalone-${{ matrix.browser }}:${{ matrix.browser-version }}
|
||||
ports:
|
||||
- 4444:4444
|
||||
options: --shm-size=2gb
|
||||
volumes:
|
||||
- /home/runner/work/_temp/e2e/:/home/seluser/files
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'public-yarn.lock'
|
||||
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- run: DOCKER_HOST=selenium yarn e2e test:ci local
|
||||
env:
|
||||
CI: true
|
||||
DOWNLOADS_DIR: /home/runner/work/_temp/e2e/
|
||||
BROWSER: ${{ matrix.browser }}
|
||||
TEST_URL: "https://testhost:5421"
|
||||
GH_EVENT_NAME: ${{ github.event_name }}
|
||||
GH_PR_NUMBER: ${{ github.pull_request.number }}
|
||||
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -79,7 +79,5 @@ apps/examples/www/index.js
|
|||
|
||||
apps/examples/build.esbuild.json
|
||||
|
||||
e2e/screenshots
|
||||
e2e/downloads
|
||||
e2e/driver-logs
|
||||
e2e/.wdio-vscode-service/
|
||||
apps/examples/e2e/test-results
|
||||
apps/examples/playwright-report/index.html
|
||||
|
|
7
apps/examples/e2e/global-setup.ts
Normal file
7
apps/examples/e2e/global-setup.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { FullConfig } from '@playwright/test'
|
||||
|
||||
async function globalSetup(config: FullConfig) {
|
||||
return config
|
||||
}
|
||||
|
||||
export default globalSetup
|
8
apps/examples/e2e/global-teardown.ts
Normal file
8
apps/examples/e2e/global-teardown.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { FullConfig } from '@playwright/test'
|
||||
|
||||
async function globalTeardown(config: FullConfig) {
|
||||
// noop
|
||||
return config
|
||||
}
|
||||
|
||||
export default globalTeardown
|
107
apps/examples/e2e/playwright.config.ts
Normal file
107
apps/examples/e2e/playwright.config.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import type { PlaywrightTestConfig } from '@playwright/test'
|
||||
import { devices } from '@playwright/test'
|
||||
import { config as _config } from 'dotenv'
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
_config()
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './tests',
|
||||
globalSetup: './global-setup.ts',
|
||||
globalTeardown: './global-teardown.ts',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 2000,
|
||||
toHaveScreenshot: {
|
||||
maxDiffPixelRatio: 0.15,
|
||||
},
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
// reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://localhost:5420',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
headless: true, // !!process.env.CI,
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
},
|
||||
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: {
|
||||
// ...devices['Desktop Safari'],
|
||||
// },
|
||||
// },
|
||||
|
||||
// /* Test against mobile viewports. */
|
||||
{
|
||||
name: 'Mobile Chrome',
|
||||
use: {
|
||||
...devices['Pixel 5'],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
outputDir: './test-results',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'yarn dev',
|
||||
port: 5420,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
77
apps/examples/e2e/shared-e2e.ts
Normal file
77
apps/examples/e2e/shared-e2e.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import { PlaywrightTestArgs, PlaywrightWorkerArgs } from '@playwright/test'
|
||||
import { App } from '@tldraw/tldraw'
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
// export async function expectPathToBe(page: Page, path: string) {
|
||||
// expect(await page.evaluate(() => app.root.path.value)).toBe(path)
|
||||
// }
|
||||
|
||||
// export async function expectToHaveNShapes(page: Page, numberOfShapes: number) {
|
||||
// expect(await page.evaluate(() => app.shapesArray.length)).toBe(numberOfShapes)
|
||||
// }
|
||||
|
||||
// export async function expectToHaveNSelectedShapes(page: Page, numberOfSelectedShapes: number) {
|
||||
// expect(await page.evaluate(() => app.selectedIds.length)).toBe(numberOfSelectedShapes)
|
||||
// }
|
||||
|
||||
declare const app: App
|
||||
|
||||
export async function setup({ page }: PlaywrightTestArgs & PlaywrightWorkerArgs) {
|
||||
await setupPage(page)
|
||||
}
|
||||
|
||||
export async function setupWithShapes({ page }: PlaywrightTestArgs & PlaywrightWorkerArgs) {
|
||||
await setupPage(page)
|
||||
await setupPageWithShapes(page)
|
||||
}
|
||||
|
||||
export async function cleanup({ page }: PlaywrightTestArgs) {
|
||||
await cleanupPage(page)
|
||||
}
|
||||
|
||||
export async function setupPage(page: PlaywrightTestArgs['page']) {
|
||||
await page.goto('http://localhost:5420/e2e')
|
||||
await page.waitForSelector('.tl-canvas')
|
||||
await page.evaluate(() => (app.enableAnimations = false))
|
||||
}
|
||||
|
||||
export async function setupPageWithShapes(page: PlaywrightTestArgs['page']) {
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.click(200, 200)
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.click(200, 250)
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.click(250, 300)
|
||||
await page.evaluate(() => app.selectNone())
|
||||
}
|
||||
|
||||
export async function cleanupPage(page: PlaywrightTestArgs['page']) {
|
||||
await page.keyboard.press('Control+a')
|
||||
await page.keyboard.press('Delete')
|
||||
}
|
||||
|
||||
export async function getAllShapeLabels(page: PlaywrightTestArgs['page']) {
|
||||
return await page
|
||||
.locator('.tl-shapes')
|
||||
.first()
|
||||
.evaluate((e) => {
|
||||
const labels: { index: string; label: string }[] = []
|
||||
for (const child of e.children) {
|
||||
const index = (child as HTMLDivElement).style.zIndex
|
||||
const label = child.querySelector('.tl-text-content') as HTMLDivElement
|
||||
labels.push({ index, label: label.innerText })
|
||||
}
|
||||
labels.sort((a, b) => (a.index > b.index ? 1 : -1))
|
||||
return labels.map((l) => l.label)
|
||||
})
|
||||
}
|
||||
|
||||
export async function getAllShapeTypes(page: PlaywrightTestArgs['page']) {
|
||||
return await page
|
||||
.locator('.tl-shape')
|
||||
.elementHandles()
|
||||
.then((handles) => Promise.all(handles.map((h) => h.getAttribute('data-shape-type'))))
|
||||
}
|
26
apps/examples/e2e/tests/test-camera.spec.ts
Normal file
26
apps/examples/e2e/tests/test-camera.spec.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import test from '@playwright/test'
|
||||
import { setup } from '../shared-e2e'
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
test.describe('camera', () => {
|
||||
test.beforeEach(setup)
|
||||
|
||||
test.fixme('panning', () => {
|
||||
// todo
|
||||
})
|
||||
|
||||
test.fixme('pinching', () => {
|
||||
// todo
|
||||
})
|
||||
|
||||
test.fixme('minimap', () => {
|
||||
// todo
|
||||
})
|
||||
|
||||
test.fixme('hand tool', () => {
|
||||
// todo
|
||||
})
|
||||
})
|
191
apps/examples/e2e/tests/test-canvas-events.spec.ts
Normal file
191
apps/examples/e2e/tests/test-canvas-events.spec.ts
Normal file
|
@ -0,0 +1,191 @@
|
|||
import test, { expect, Page } from '@playwright/test'
|
||||
import { App } from '@tldraw/tldraw'
|
||||
import { setupPage } from '../shared-e2e'
|
||||
|
||||
declare const __tldraw_editor_events: any[]
|
||||
|
||||
// We're just testing the events, not the actual results.
|
||||
|
||||
let page: Page
|
||||
|
||||
declare const app: App
|
||||
|
||||
test.describe('Canvas events', () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await setupPage(page)
|
||||
})
|
||||
|
||||
test.describe('pointer events', () => {
|
||||
test('pointer down', async () => {
|
||||
await page.mouse.move(200, 200) // to kill any double clicks
|
||||
await page.mouse.move(100, 100)
|
||||
await page.mouse.down()
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
target: 'canvas',
|
||||
type: 'pointer',
|
||||
name: 'pointer_down',
|
||||
})
|
||||
})
|
||||
|
||||
test('pointer move', async () => {
|
||||
await page.mouse.move(200, 200) // to kill any double clicks
|
||||
await page.mouse.move(100, 100)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(101, 101)
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
target: 'canvas',
|
||||
type: 'pointer',
|
||||
name: 'pointer_move',
|
||||
})
|
||||
})
|
||||
|
||||
test('pointer up', async () => {
|
||||
await page.mouse.move(200, 200) // to kill any double clicks
|
||||
await page.mouse.move(100, 100)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(101, 101)
|
||||
await page.mouse.up()
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
target: 'canvas',
|
||||
type: 'pointer',
|
||||
name: 'pointer_up',
|
||||
})
|
||||
})
|
||||
|
||||
test('pointer leave', async () => {
|
||||
await page.mouse.move(200, 200) // to kill any double clicks
|
||||
await page.mouse.move(100, 100)
|
||||
await page.mouse.move(-10, 50)
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
target: 'canvas',
|
||||
type: 'pointer',
|
||||
name: 'pointer_leave',
|
||||
})
|
||||
})
|
||||
|
||||
test('pointer enter', async () => {
|
||||
await page.mouse.move(200, 200) // to kill any double clicks
|
||||
await page.mouse.move(-10, 50)
|
||||
await page.mouse.move(1, 50)
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-2))).toMatchObject({
|
||||
target: 'canvas',
|
||||
type: 'pointer',
|
||||
name: 'pointer_enter',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('click events', () => {
|
||||
// todo
|
||||
})
|
||||
|
||||
test.describe('pinch events', () => {
|
||||
// todo
|
||||
})
|
||||
|
||||
test.describe('keyboard events', () => {
|
||||
// todo
|
||||
})
|
||||
|
||||
test.fixme('wheel events', async () => {
|
||||
await page.mouse.move(200, 200) // to kill any double clicks
|
||||
await page.mouse.move(100, 100)
|
||||
await page.mouse.wheel(10, 10)
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
type: 'wheel',
|
||||
name: 'wheel',
|
||||
delta: {
|
||||
x: -20,
|
||||
y: -20,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test.fixme('complete', async () => {
|
||||
await page.evaluate(async () => app.complete())
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
type: 'misc',
|
||||
name: 'complete',
|
||||
})
|
||||
})
|
||||
|
||||
test.fixme('cancel', async () => {
|
||||
await page.evaluate(async () => app.cancel())
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
type: 'misc',
|
||||
name: 'complete',
|
||||
})
|
||||
})
|
||||
|
||||
test.fixme('interrupt', async () => {
|
||||
await page.evaluate(async () => app.interrupt())
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
type: 'misc',
|
||||
name: 'interrupt',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Shape events', () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await setupPage(page)
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.click(150, 150)
|
||||
await page.mouse.move(0, 0)
|
||||
await page.keyboard.press('Escape')
|
||||
})
|
||||
|
||||
test.describe('pointer events', () => {
|
||||
test('pointer enter', async () => {
|
||||
await page.mouse.move(51, 51)
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-2))).toMatchObject({
|
||||
target: 'shape',
|
||||
type: 'pointer',
|
||||
name: 'pointer_enter',
|
||||
})
|
||||
})
|
||||
|
||||
test('pointer leave', async () => {
|
||||
await page.mouse.move(51, 51)
|
||||
await page.mouse.move(-10, -10)
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
target: 'shape',
|
||||
type: 'pointer',
|
||||
name: 'pointer_leave',
|
||||
})
|
||||
})
|
||||
|
||||
test('pointer down', async () => {
|
||||
await page.mouse.move(51, 51)
|
||||
await page.mouse.down()
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
target: 'shape',
|
||||
type: 'pointer',
|
||||
name: 'pointer_down',
|
||||
})
|
||||
})
|
||||
|
||||
test('pointer move', async () => {
|
||||
await page.mouse.move(51, 51)
|
||||
await page.mouse.move(52, 52)
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
target: 'shape',
|
||||
type: 'pointer',
|
||||
name: 'pointer_move',
|
||||
})
|
||||
})
|
||||
|
||||
test('pointer up', async () => {
|
||||
await page.mouse.move(51, 51)
|
||||
await page.mouse.down()
|
||||
await page.mouse.up()
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-2))).toMatchObject({
|
||||
target: 'shape',
|
||||
type: 'pointer',
|
||||
name: 'pointer_up',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
79
apps/examples/e2e/tests/test-clipboard.spec.ts
Normal file
79
apps/examples/e2e/tests/test-clipboard.spec.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import test, { expect } from '@playwright/test'
|
||||
import { App } from '@tldraw/tldraw'
|
||||
import { setup } from '../shared-e2e'
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
declare const app: App
|
||||
|
||||
/**
|
||||
* These tests are skipped. They are here to show how to use the clipboard
|
||||
* in tests. The clipboard is not really supported in playwright, so until
|
||||
* we figure out a way to do it (or until it is supported propertly), we've
|
||||
* had to skip these tests.
|
||||
*/
|
||||
test.describe.skip('clipboard tests', () => {
|
||||
test.beforeEach(setup)
|
||||
|
||||
test('copy and paste from keyboard shortcut', async ({ page }) => {
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.move(100, 100)
|
||||
await page.mouse.down()
|
||||
await page.mouse.up()
|
||||
|
||||
expect(await page.evaluate(() => app.shapesArray.length)).toBe(1)
|
||||
expect(await page.evaluate(() => app.selectedShapes.length)).toBe(1)
|
||||
|
||||
await page.keyboard.down('Control')
|
||||
await page.keyboard.press('KeyC')
|
||||
await sleep(100)
|
||||
await page.keyboard.press('KeyV')
|
||||
await page.keyboard.up('Control')
|
||||
|
||||
expect(await page.evaluate(() => app.shapesArray.length)).toBe(2)
|
||||
expect(await page.evaluate(() => app.selectedShapes.length)).toBe(1)
|
||||
})
|
||||
|
||||
test('copy and paste from main menu', async ({ page }) => {
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.move(100, 100)
|
||||
await page.mouse.down()
|
||||
await page.mouse.up()
|
||||
|
||||
expect(await page.evaluate(() => app.shapesArray.length)).toBe(1)
|
||||
expect(await page.evaluate(() => app.selectedShapes.length)).toBe(1)
|
||||
|
||||
await page.getByTestId('main.menu').click()
|
||||
await page.getByTestId('menu-item.edit').click()
|
||||
await page.getByTestId('menu-item.copy').click()
|
||||
await sleep(100)
|
||||
await page.getByTestId('main.menu').click()
|
||||
await page.getByTestId('menu-item.edit').click()
|
||||
await page.getByTestId('menu-item.paste').click()
|
||||
|
||||
expect(await page.evaluate(() => app.shapesArray.length)).toBe(2)
|
||||
expect(await page.evaluate(() => app.selectedShapes.length)).toBe(1)
|
||||
})
|
||||
|
||||
test('copy and paste from context menu', async ({ page }) => {
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.move(100, 100)
|
||||
await page.mouse.down()
|
||||
await page.mouse.up()
|
||||
|
||||
expect(await page.evaluate(() => app.shapesArray.length)).toBe(1)
|
||||
expect(await page.evaluate(() => app.selectedShapes.length)).toBe(1)
|
||||
|
||||
await page.mouse.click(100, 100, { button: 'right' })
|
||||
await page.getByTestId('menu-item.copy').click()
|
||||
await sleep(100)
|
||||
await page.mouse.move(200, 200)
|
||||
await page.mouse.click(100, 100, { button: 'right' })
|
||||
await page.getByTestId('menu-item.paste').click()
|
||||
|
||||
expect(await page.evaluate(() => app.shapesArray.length)).toBe(2)
|
||||
expect(await page.evaluate(() => app.selectedShapes.length)).toBe(1)
|
||||
})
|
||||
})
|
323
apps/examples/e2e/tests/test-kbds.spec.ts
Normal file
323
apps/examples/e2e/tests/test-kbds.spec.ts
Normal file
|
@ -0,0 +1,323 @@
|
|||
import test, { Page, expect } from '@playwright/test'
|
||||
import { setupPage, setupPageWithShapes } from '../shared-e2e'
|
||||
|
||||
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 () => {
|
||||
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' },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('actions', () => {
|
||||
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' },
|
||||
// })
|
||||
|
||||
/* ------------------- Preferences ------------------ */
|
||||
|
||||
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: 'toggle-dark-mode',
|
||||
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' },
|
||||
})
|
||||
})
|
||||
|
||||
/* -------------- Operations on Shapes -------------- */
|
||||
|
||||
test('Operations on shapes', async () => {
|
||||
await setupPageWithShapes(page)
|
||||
|
||||
// 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' },
|
||||
})
|
||||
|
||||
// distribute horizontal
|
||||
await page.keyboard.press('Control+a')
|
||||
await page.mouse.click(200, 200, { button: 'right' })
|
||||
await page.getByTestId('menu-item.arrange').click()
|
||||
await page.getByTestId('menu-item.distribute-horizontal').click()
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'distribute-shapes',
|
||||
data: { operation: 'horizontal', source: 'context-menu' },
|
||||
})
|
||||
|
||||
// distribute vertical — Shift+Alt+V
|
||||
await page.keyboard.press('Control+a')
|
||||
await page.mouse.click(200, 200, { button: 'right' })
|
||||
await page.getByTestId('menu-item.arrange').click()
|
||||
await page.getByTestId('menu-item.distribute-vertical').click()
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'distribute-shapes',
|
||||
data: { operation: 'vertical', source: 'context-menu' },
|
||||
})
|
||||
|
||||
// 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' },
|
||||
})
|
||||
|
||||
// delete — backspace
|
||||
await page.keyboard.press('Backspace')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'delete-shapes',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
|
||||
// delete — ⌫
|
||||
await page.keyboard.press('Delete')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'delete-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' },
|
||||
})
|
||||
|
||||
/* ---------------------- Misc ---------------------- */
|
||||
|
||||
// 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' },
|
||||
// })
|
||||
|
||||
/* --------------------- Export --------------------- */
|
||||
|
||||
await page.keyboard.press('Control+Shift+c')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'copy-as',
|
||||
data: { format: 'svg', source: 'kbd' },
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
169
apps/examples/e2e/tests/test-shapes.spec.ts
Normal file
169
apps/examples/e2e/tests/test-shapes.spec.ts
Normal file
|
@ -0,0 +1,169 @@
|
|||
import test, { Page, expect } from '@playwright/test'
|
||||
import { getAllShapeTypes, setupPage } from '../shared-e2e'
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
const clickableShapeCreators = [
|
||||
{ tool: 'draw', shape: 'draw' },
|
||||
{ tool: 'frame', shape: 'frame' },
|
||||
{ tool: 'note', shape: 'note' },
|
||||
{ tool: 'text', shape: 'text' },
|
||||
{ tool: 'rectangle', shape: 'geo' },
|
||||
{ tool: 'ellipse', shape: 'geo' },
|
||||
{ tool: 'triangle', shape: 'geo' },
|
||||
{ tool: 'diamond', shape: 'geo' },
|
||||
{ tool: 'pentagon', shape: 'geo' },
|
||||
{ tool: 'hexagon', shape: 'geo' },
|
||||
{ tool: 'octagon', shape: 'geo' },
|
||||
{ tool: 'star', shape: 'geo' },
|
||||
{ tool: 'rhombus', shape: 'geo' },
|
||||
{ tool: 'oval', shape: 'geo' },
|
||||
{ tool: 'trapezoid', shape: 'geo' },
|
||||
{ tool: 'arrow-right', shape: 'geo' },
|
||||
{ tool: 'arrow-left', shape: 'geo' },
|
||||
{ tool: 'arrow-up', shape: 'geo' },
|
||||
{ tool: 'arrow-down', shape: 'geo' },
|
||||
{ tool: 'x-box', shape: 'geo' },
|
||||
{ tool: 'check-box', shape: 'geo' },
|
||||
]
|
||||
|
||||
const draggableShapeCreators = [
|
||||
{ tool: 'draw', shape: 'draw' },
|
||||
{ tool: 'arrow', shape: 'arrow' },
|
||||
{ tool: 'frame', shape: 'frame' },
|
||||
{ tool: 'note', shape: 'note' },
|
||||
{ tool: 'text', shape: 'text' },
|
||||
{ tool: 'line', shape: 'line' },
|
||||
{ tool: 'rectangle', shape: 'geo' },
|
||||
{ tool: 'ellipse', shape: 'geo' },
|
||||
{ tool: 'triangle', shape: 'geo' },
|
||||
{ tool: 'diamond', shape: 'geo' },
|
||||
{ tool: 'pentagon', shape: 'geo' },
|
||||
{ tool: 'hexagon', shape: 'geo' },
|
||||
{ tool: 'octagon', shape: 'geo' },
|
||||
{ tool: 'star', shape: 'geo' },
|
||||
{ tool: 'rhombus', shape: 'geo' },
|
||||
{ tool: 'oval', shape: 'geo' },
|
||||
{ tool: 'trapezoid', shape: 'geo' },
|
||||
{ tool: 'arrow-right', shape: 'geo' },
|
||||
{ tool: 'arrow-left', shape: 'geo' },
|
||||
{ tool: 'arrow-up', shape: 'geo' },
|
||||
{ tool: 'arrow-down', shape: 'geo' },
|
||||
{ tool: 'x-box', shape: 'geo' },
|
||||
{ tool: 'check-box', shape: 'geo' },
|
||||
]
|
||||
|
||||
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 () => {
|
||||
await page.keyboard.press('Control+a')
|
||||
await page.keyboard.press('Backspace')
|
||||
expect(await getAllShapeTypes(page)).toEqual([])
|
||||
|
||||
for (const { tool } of otherTools) {
|
||||
// Find and click the button
|
||||
if (!(await page.getByTestId(`tools.${tool}`).isVisible())) {
|
||||
if (!(await page.getByTestId(`tools.more`).isVisible())) {
|
||||
throw Error(`Tool more is not visible`)
|
||||
}
|
||||
|
||||
await page.getByTestId('tools.more').click()
|
||||
}
|
||||
|
||||
if (!(await page.getByTestId(`tools.${tool}`).isVisible())) {
|
||||
throw Error(`Tool ${tool} is not visible`)
|
||||
}
|
||||
|
||||
await page.getByTestId(`tools.${tool}`).click()
|
||||
|
||||
// Button should be selected
|
||||
expect(
|
||||
await page
|
||||
.getByTestId(`tools.${tool}`)
|
||||
.elementHandle()
|
||||
.then((d) => d?.getAttribute('data-state'))
|
||||
).toBe('selected')
|
||||
}
|
||||
})
|
||||
|
||||
test('creates shapes clickable tools', async () => {
|
||||
await page.keyboard.press('Control+a')
|
||||
await page.keyboard.press('Backspace')
|
||||
expect(await getAllShapeTypes(page)).toEqual([])
|
||||
|
||||
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 page.getByTestId(`tools.${tool}`).click()
|
||||
|
||||
// Button should be selected
|
||||
expect(
|
||||
await page
|
||||
.getByTestId(`tools.${tool}`)
|
||||
.elementHandle()
|
||||
.then((d) => d?.getAttribute('data-state'))
|
||||
).toBe('selected')
|
||||
|
||||
// Click on the page
|
||||
await page.mouse.click(200, 200)
|
||||
|
||||
// We should have a corresponding shape in the page
|
||||
expect(await getAllShapeTypes(page)).toEqual([shape])
|
||||
|
||||
// Reset for next time
|
||||
await page.mouse.click(0, 0) // to ensure we're not focused
|
||||
await page.keyboard.press('Control+a')
|
||||
await page.keyboard.press('Backspace')
|
||||
}
|
||||
|
||||
expect(await getAllShapeTypes(page)).toEqual([])
|
||||
})
|
||||
|
||||
test('creates shapes with draggable tools', async () => {
|
||||
await page.keyboard.press('Control+a')
|
||||
await page.keyboard.press('Backspace')
|
||||
expect(await getAllShapeTypes(page)).toEqual([])
|
||||
|
||||
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 page.getByTestId(`tools.${tool}`).click()
|
||||
|
||||
// Button should be selected
|
||||
expect(
|
||||
await page
|
||||
.getByTestId(`tools.${tool}`)
|
||||
.elementHandle()
|
||||
.then((d) => d?.getAttribute('data-state'))
|
||||
).toBe('selected')
|
||||
|
||||
// Click and drag
|
||||
await page.mouse.move(200, 200)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(250, 250)
|
||||
await page.mouse.up()
|
||||
|
||||
// We should have a corresponding shape in the page
|
||||
expect(await getAllShapeTypes(page)).toEqual([shape])
|
||||
|
||||
// Reset for next time
|
||||
await page.mouse.click(0, 0) // to ensure we're not focused
|
||||
await page.keyboard.press('Control+a')
|
||||
await page.keyboard.press('Backspace')
|
||||
}
|
||||
})
|
||||
})
|
129
apps/examples/e2e/tests/test-smoke.spec.ts
Normal file
129
apps/examples/e2e/tests/test-smoke.spec.ts
Normal file
|
@ -0,0 +1,129 @@
|
|||
import test, { expect } from '@playwright/test'
|
||||
import { App, TLGeoShape } from '@tldraw/tldraw'
|
||||
import { getAllShapeTypes, setup } from '../shared-e2e'
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
declare const app: App
|
||||
|
||||
test.describe('smoke tests', () => {
|
||||
test.beforeEach(setup)
|
||||
|
||||
test('create a shape on the canvas', async ({ page }) => {
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.move(10, 50)
|
||||
await page.mouse.down()
|
||||
await page.mouse.up()
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.move(10, 250)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(100, 350)
|
||||
await page.mouse.up()
|
||||
expect(await getAllShapeTypes(page)).toEqual(['geo', 'geo'])
|
||||
})
|
||||
|
||||
test('undo and redo', async ({ page }) => {
|
||||
// buttons should be disabled when there is no history
|
||||
expect(page.getByTestId('main.undo')).toBeDisabled()
|
||||
expect(page.getByTestId('main.redo')).toBeDisabled()
|
||||
|
||||
// create a shape
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.move(100, 100)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(200, 200)
|
||||
await page.mouse.up()
|
||||
|
||||
expect(await getAllShapeTypes(page)).toEqual(['geo'])
|
||||
|
||||
// We should have an undoable shape
|
||||
expect(page.getByTestId('main.undo')).not.toBeDisabled()
|
||||
expect(page.getByTestId('main.redo')).toBeDisabled()
|
||||
|
||||
// Click the undo button to undo the shape
|
||||
await page.getByTestId('main.undo').click()
|
||||
|
||||
expect(await getAllShapeTypes(page)).toEqual([])
|
||||
expect(page.getByTestId('main.undo')).toBeDisabled()
|
||||
expect(page.getByTestId('main.redo')).not.toBeDisabled()
|
||||
|
||||
// Click the redo button to redo the shape
|
||||
await page.getByTestId('main.redo').click()
|
||||
|
||||
expect(await getAllShapeTypes(page)).toEqual(['geo'])
|
||||
expect(await page.getByTestId('main.undo').isDisabled()).not.toBe(true)
|
||||
expect(await page.getByTestId('main.redo').isDisabled()).toBe(true)
|
||||
})
|
||||
|
||||
test('style panel + undo and redo squashing', async ({ page }) => {
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.move(100, 100)
|
||||
await page.mouse.down()
|
||||
await page.mouse.up()
|
||||
expect(await getAllShapeTypes(page)).toEqual(['geo'])
|
||||
|
||||
const getSelectedShapeColor = async () =>
|
||||
await page.evaluate(() => (app.selectedShapes[0] as TLGeoShape).props.color)
|
||||
|
||||
// change style
|
||||
expect(await getSelectedShapeColor()).toBe('black')
|
||||
|
||||
// when on a mobile device...
|
||||
const hasMobileMenu = await page.isVisible('.tlui-toolbar__styles__button')
|
||||
|
||||
if (hasMobileMenu) {
|
||||
// open the style menu
|
||||
await page.getByTestId('mobile.styles').click()
|
||||
}
|
||||
|
||||
// Click the light-blue color
|
||||
await page.getByTestId('style.color.light-blue').click()
|
||||
expect(await getSelectedShapeColor()).toBe('light-blue')
|
||||
|
||||
// now drag from blue to orange; the color should change as we drag
|
||||
// but when we undo, we should ignore the colors which were changed
|
||||
// before the final color was chosen; i.e. the history should think
|
||||
// the color went from black to light blue to orange, though the shape
|
||||
// actually changed from black to light blue to blue to light blue to
|
||||
// yellow and then to orange.
|
||||
|
||||
// start a pointer down over the blue color button
|
||||
await page.getByTestId('style.color.blue').hover()
|
||||
await page.mouse.down()
|
||||
expect(await getSelectedShapeColor()).toBe('blue')
|
||||
|
||||
// now move across to the other colors before releasing
|
||||
await page.getByTestId('style.color.light-blue').hover()
|
||||
expect(await getSelectedShapeColor()).toBe('light-blue')
|
||||
|
||||
await page.getByTestId('style.color.yellow').hover()
|
||||
expect(await getSelectedShapeColor()).toBe('yellow')
|
||||
|
||||
await page.getByTestId('style.color.orange').hover()
|
||||
expect(await getSelectedShapeColor()).toBe('orange')
|
||||
|
||||
await page.mouse.up()
|
||||
|
||||
// Now undo and redo
|
||||
const undo = page.getByTestId('main.undo')
|
||||
const redo = page.getByTestId('main.redo')
|
||||
|
||||
await undo.click() // orange -> light blue
|
||||
expect(await getSelectedShapeColor()).toBe('light-blue') // skipping squashed colors!
|
||||
|
||||
await redo.click() // light blue -> orange
|
||||
expect(await getSelectedShapeColor()).toBe('orange') // skipping squashed colors!
|
||||
|
||||
await undo.click() // orange -> light blue
|
||||
await undo.click() // light blue -> black
|
||||
expect(await getSelectedShapeColor()).toBe('black')
|
||||
|
||||
await redo.click() // black -> light blue
|
||||
await redo.click() // light-blue -> orange
|
||||
|
||||
expect(await page.getByTestId('main.undo').isDisabled()).not.toBe(true)
|
||||
expect(await page.getByTestId('main.redo').isDisabled()).toBe(true)
|
||||
})
|
||||
})
|
221
apps/examples/e2e/tests/test-text.spec.ts
Normal file
221
apps/examples/e2e/tests/test-text.spec.ts
Normal file
|
@ -0,0 +1,221 @@
|
|||
import test, { Page, expect } from '@playwright/test'
|
||||
import { App, Box2dModel } from '@tldraw/tldraw'
|
||||
import { setupPage } from '../shared-e2e'
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
const measureTextOptions = {
|
||||
width: 'fit-content',
|
||||
fontFamily: 'var(--tl-font-draw)',
|
||||
fontSize: 24,
|
||||
lineHeight: 1.35,
|
||||
fontWeight: 'normal',
|
||||
fontStyle: 'normal',
|
||||
padding: '0px',
|
||||
maxWidth: 'auto',
|
||||
}
|
||||
|
||||
const measureTextSpansOptions = {
|
||||
width: 100,
|
||||
height: 1000,
|
||||
overflow: 'wrap' as const,
|
||||
padding: 0,
|
||||
fontSize: 24,
|
||||
fontWeight: 'normal',
|
||||
fontFamily: 'var(--tl-font-draw)',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 1.35,
|
||||
textAlign: 'start' as 'start' | 'middle' | 'end',
|
||||
}
|
||||
|
||||
function formatLines(spans: { box: Box2dModel; text: string }[]) {
|
||||
const lines = []
|
||||
|
||||
let currentLine: string[] | null = null
|
||||
let currentLineTop = null
|
||||
for (const span of spans) {
|
||||
if (currentLineTop !== span.box.y) {
|
||||
if (currentLine !== null) {
|
||||
lines.push(currentLine)
|
||||
}
|
||||
currentLine = []
|
||||
currentLineTop = span.box.y
|
||||
}
|
||||
currentLine!.push(span.text)
|
||||
}
|
||||
|
||||
if (currentLine !== null) {
|
||||
lines.push(currentLine)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
declare const app: App
|
||||
let page: Page
|
||||
|
||||
test.describe('text measurement', () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await setupPage(page)
|
||||
})
|
||||
|
||||
test('measures text', async () => {
|
||||
const { w, h } = await page.evaluate<{ w: number; h: number }, typeof measureTextOptions>(
|
||||
async (options) => app.textMeasure.measureText('testing', options),
|
||||
measureTextOptions
|
||||
)
|
||||
|
||||
expect(w).toBeCloseTo(85.828125, 0)
|
||||
expect(h).toBeCloseTo(32.3984375, 0)
|
||||
})
|
||||
|
||||
// The text-measurement tests below this point aren't super useful any
|
||||
// more. They were added when we had a different approach to text SVG
|
||||
// exports (trying to replicate browser decisions with our own code) to
|
||||
// what we do now (letting the browser make those decisions then
|
||||
// measuring the results).
|
||||
//
|
||||
// It's hard to write better tests here (e.g. ones where we actually
|
||||
// look at the measured values) because the specifics of text layout
|
||||
// vary from browser to browser. The ideal thing would be to replace
|
||||
// these with visual regression tests for text SVG exports, but we don't
|
||||
// have a way of doing visual regression testing right now.
|
||||
|
||||
test('should get a single text span', async () => {
|
||||
const spans = await page.evaluate<
|
||||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) => app.textMeasure.measureTextSpans('testing', options),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
expect(formatLines(spans)).toEqual([['testing']])
|
||||
})
|
||||
|
||||
test('should wrap a word when it has to', async () => {
|
||||
const spans = await page.evaluate<
|
||||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) => app.textMeasure.measureTextSpans('testing', { ...options, width: 50 }),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
expect(formatLines(spans)).toEqual([['test'], ['ing']])
|
||||
})
|
||||
|
||||
test('should preserve whitespace at line breaks', async () => {
|
||||
const spans = await page.evaluate<
|
||||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) => app.textMeasure.measureTextSpans('testing testing', options),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
expect(formatLines(spans)).toEqual([['testing', ' '], ['testing']])
|
||||
})
|
||||
|
||||
test('should preserve whitespace at the end of wrapped lines', async () => {
|
||||
const spans = await page.evaluate<
|
||||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) => app.textMeasure.measureTextSpans('testing testing ', options),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
expect(formatLines(spans)).toEqual([
|
||||
['testing', ' '],
|
||||
['testing', ' '],
|
||||
])
|
||||
})
|
||||
|
||||
test('preserves whitespace at the end of unwrapped lines', async () => {
|
||||
const spans = await page.evaluate<
|
||||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) =>
|
||||
app.textMeasure.measureTextSpans('testing testing ', { ...options, width: 200 }),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
expect(formatLines(spans)).toEqual([['testing', ' ', 'testing', ' ']])
|
||||
})
|
||||
|
||||
test('preserves whitespace at the start of an unwrapped line', async () => {
|
||||
const spans = await page.evaluate<
|
||||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) =>
|
||||
app.textMeasure.measureTextSpans(' testing testing', { ...options, width: 200 }),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
expect(formatLines(spans)).toEqual([[' ', 'testing', ' ', 'testing']])
|
||||
})
|
||||
|
||||
test('should place starting whitespace on its own line if it has to', async () => {
|
||||
const spans = await page.evaluate<
|
||||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) => app.textMeasure.measureTextSpans(' testing testing', options),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
expect(formatLines(spans)).toEqual([[' '], ['testing', ' '], ['testing']])
|
||||
})
|
||||
|
||||
test('should handle multiline text', async () => {
|
||||
const spans = await page.evaluate<
|
||||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) => app.textMeasure.measureTextSpans(' test\ning testing \n t', options),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
expect(formatLines(spans)).toEqual([
|
||||
[' ', 'test', '\n'],
|
||||
['ing', ' '],
|
||||
['testing', ' \n'],
|
||||
[' ', 't'],
|
||||
])
|
||||
})
|
||||
|
||||
test('should break long strings of text', async () => {
|
||||
const spans = await page.evaluate<
|
||||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(
|
||||
async (options) =>
|
||||
app.textMeasure.measureTextSpans('testingtestingtestingtestingtestingtesting', options),
|
||||
measureTextSpansOptions
|
||||
)
|
||||
|
||||
expect(formatLines(spans)).toEqual([
|
||||
['testingt'],
|
||||
['estingte'],
|
||||
['stingtes'],
|
||||
['tingtest'],
|
||||
['ingtesti'],
|
||||
['ng'],
|
||||
])
|
||||
})
|
||||
|
||||
test('should return an empty array if the text is empty', async () => {
|
||||
const spans = await page.evaluate<
|
||||
{ text: string; box: Box2dModel }[],
|
||||
typeof measureTextSpansOptions
|
||||
>(async (options) => app.textMeasure.measureTextSpans('', options), measureTextSpansOptions)
|
||||
|
||||
expect(formatLines(spans)).toEqual([])
|
||||
})
|
||||
})
|
|
@ -29,10 +29,13 @@
|
|||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "yarn run -T tsx ../../scripts/lint.ts"
|
||||
"lint": "yarn run -T tsx ../../scripts/lint.ts",
|
||||
"e2e": "playwright test -c ./e2e/playwright.config.ts",
|
||||
"e2e-ui": "playwright test --ui -c ./e2e/playwright.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@playwright/test": "^1.34.3",
|
||||
"@tldraw/assets": "workspace:*",
|
||||
"@tldraw/tldraw": "workspace:*",
|
||||
"@vercel/analytics": "^1.0.1",
|
||||
|
@ -43,5 +46,8 @@
|
|||
"signia": "0.1.4",
|
||||
"signia-react": "0.1.4",
|
||||
"vite": "^4.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "^16.0.3"
|
||||
}
|
||||
}
|
||||
|
|
23
apps/examples/src/end-to-end/ForEndToEndTests.tsx
Normal file
23
apps/examples/src/end-to-end/ForEndToEndTests.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Tldraw } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/editor.css'
|
||||
import '@tldraw/tldraw/ui.css'
|
||||
|
||||
export default function ForEndToEndTests() {
|
||||
;(window as any).__tldraw_editor_events = []
|
||||
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw
|
||||
autoFocus
|
||||
onUiEvent={(name, data) => {
|
||||
;(window as any).__tldraw_ui_event = { name, data }
|
||||
}}
|
||||
onMount={(app) => {
|
||||
app.on('event', (info) => {
|
||||
;(window as any).__tldraw_editor_events.push(info)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -21,6 +21,7 @@ import ExampleScroll from './6-scroll/ScrollExample'
|
|||
import ExampleMultiple from './7-multiple/MultipleExample'
|
||||
import ErrorBoundaryExample from './8-error-boundaries/ErrorBoundaryExample'
|
||||
import HideUiExample from './9-hide-ui/HideUiExample'
|
||||
import ForEndToEndTests from './end-to-end/ForEndToEndTests'
|
||||
|
||||
// we use secret internal `setDefaultAssetUrls` functions to set these at the
|
||||
// top-level so assets don't need to be passed down in every single example.
|
||||
|
@ -85,6 +86,10 @@ export const allExamples: Example[] = [
|
|||
path: '/user-presence',
|
||||
element: <UserPresenceExample />,
|
||||
},
|
||||
{
|
||||
path: '/e2e',
|
||||
element: <ForEndToEndTests />,
|
||||
},
|
||||
]
|
||||
|
||||
const router = createBrowserRouter(allExamples)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"extends": "../../config/tsconfig.base.json",
|
||||
"include": ["src", "./vite.config.ts"],
|
||||
"include": ["src", "e2e", "./vite.config.ts"],
|
||||
"exclude": ["node_modules", "dist", "**/*.css", ".tsbuild*", "./scripts/legacy-translations"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./.tsbuild"
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
# @tldraw/webdriver
|
||||
|
|
@ -1 +0,0 @@
|
|||
# @tldraw/webdriver
|
|
@ -1,51 +0,0 @@
|
|||
{
|
||||
"name": "webdriver",
|
||||
"description": "A tiny little drawing app (for webdriver).",
|
||||
"version": "2.0.0-alpha.11",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "tldraw GB Ltd.",
|
||||
"email": "hello@tldraw.com"
|
||||
},
|
||||
"homepage": "https://tldraw.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tldraw/tldraw"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tldraw/tldraw/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"tldraw",
|
||||
"drawing",
|
||||
"app",
|
||||
"development",
|
||||
"whiteboard",
|
||||
"canvas",
|
||||
"infinite"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "concurrently --names \"tsc,esbuild\" \"yarn run -T tsx ../../scripts/typecheck.ts --build --watch --preserveWatchOutput\" \"node ./scripts/dev.mjs\"",
|
||||
"lint": "yarn run -T tsx ../../scripts/lint.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/node-forge": "^1.3.1",
|
||||
"browserslist-to-esbuild": "^1.2.0",
|
||||
"concurrently": "^7.4.0",
|
||||
"esbuild": "^0.16.7",
|
||||
"ip": "^1.1.8",
|
||||
"kleur": "^4.1.5",
|
||||
"lazyrepo": "0.0.0-alpha.26",
|
||||
"node-forge": "^1.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tldraw/assets": "workspace:*",
|
||||
"@tldraw/tldraw": "workspace:*",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"signia": "0.1.4",
|
||||
"signia-react": "0.1.4"
|
||||
}
|
||||
}
|
|
@ -1,202 +0,0 @@
|
|||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
|
||||
import browserslist from 'browserslist-to-esbuild'
|
||||
import crypto from 'crypto'
|
||||
import esbuild from 'esbuild'
|
||||
import { createServer as createNonSslServer, request } from 'http'
|
||||
import { createServer as createSslServer } from 'https'
|
||||
import ip from 'ip'
|
||||
import chalk from 'kleur'
|
||||
import forge from 'node-forge'
|
||||
import * as url from 'url'
|
||||
|
||||
const LOG_REQUEST_PATHS = false
|
||||
|
||||
export const generateCert = ({ altNameIPs, altNameURIs, validityDays }) => {
|
||||
const keys = forge.pki.rsa.generateKeyPair(2048)
|
||||
const cert = forge.pki.createCertificate()
|
||||
cert.publicKey = keys.publicKey
|
||||
|
||||
// NOTE: serialNumber is the hex encoded value of an ASN.1 INTEGER.
|
||||
// Conforming CAs should ensure serialNumber is:
|
||||
// - no more than 20 octets
|
||||
// - non-negative (prefix a '00' if your value starts with a '1' bit)
|
||||
cert.serialNumber = '01' + crypto.randomBytes(19).toString('hex') // 1 octet = 8 bits = 1 byte = 2 hex chars
|
||||
cert.validity.notBefore = new Date()
|
||||
cert.validity.notAfter = new Date(
|
||||
new Date().getTime() + 1000 * 60 * 60 * 24 * (validityDays ?? 1)
|
||||
)
|
||||
const attrs = [
|
||||
{
|
||||
name: 'countryName',
|
||||
value: 'AU',
|
||||
},
|
||||
{
|
||||
shortName: 'ST',
|
||||
value: 'Some-State',
|
||||
},
|
||||
{
|
||||
name: 'organizationName',
|
||||
value: 'Temporary Testing Department Ltd',
|
||||
},
|
||||
]
|
||||
cert.setSubject(attrs)
|
||||
cert.setIssuer(attrs)
|
||||
|
||||
// add alt names so that the browser won't complain
|
||||
cert.setExtensions([
|
||||
{
|
||||
name: 'subjectAltName',
|
||||
altNames: [
|
||||
...(altNameURIs !== undefined ? altNameURIs.map((uri) => ({ type: 6, value: uri })) : []),
|
||||
...(altNameIPs !== undefined ? altNameIPs.map((uri) => ({ type: 7, ip: uri })) : []),
|
||||
],
|
||||
},
|
||||
])
|
||||
// self-sign certificate
|
||||
cert.sign(keys.privateKey)
|
||||
|
||||
// convert a Forge certificate and private key to PEM
|
||||
const pem = forge.pki.certificateToPem(cert)
|
||||
const privateKey = forge.pki.privateKeyToPem(keys.privateKey)
|
||||
|
||||
return {
|
||||
cert: pem,
|
||||
privateKey,
|
||||
}
|
||||
}
|
||||
|
||||
const { log } = console
|
||||
|
||||
const dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||
|
||||
const PORT = 5420
|
||||
const SSL_PORT = 5421
|
||||
const ENABLE_SSL = process.env.ENABLE_SSL === '1'
|
||||
const ENABLE_NETWORK_CACHING = process.env.ENABLE_NETWORK_CACHING === '1'
|
||||
const OUT_DIR = dirname + '/../www/'
|
||||
|
||||
const clients = []
|
||||
|
||||
async function main() {
|
||||
await esbuild.build({
|
||||
entryPoints: ['src/index.tsx'],
|
||||
outdir: OUT_DIR,
|
||||
bundle: true,
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
incremental: true,
|
||||
format: 'cjs',
|
||||
external: ['*.woff'],
|
||||
target: browserslist(['defaults']),
|
||||
define: {
|
||||
process: '{ "env": { "NODE_ENV": "development"} }',
|
||||
},
|
||||
loader: {
|
||||
'.woff2': 'file',
|
||||
'.svg': 'file',
|
||||
'.json': 'file',
|
||||
'.png': 'file',
|
||||
},
|
||||
watch: {
|
||||
onRebuild(error) {
|
||||
log('rebuilt')
|
||||
if (error) {
|
||||
log(error)
|
||||
}
|
||||
clients.forEach((res) => res.write('data: update\n\n'))
|
||||
clients.length = 0
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
esbuild.serve({ servedir: OUT_DIR, port: 8009 }, {}).then(({ host, port: esbuildPort }) => {
|
||||
const handler = async (req, res) => {
|
||||
const { url, method, headers } = req
|
||||
if (req.url === '/esbuild')
|
||||
return clients.push(
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
})
|
||||
)
|
||||
|
||||
function forwardRequest(url) {
|
||||
const path = (url?.split('/').pop()?.indexOf('.') ?? -1) > -1 ? url : `/index.html` //for PWA with router
|
||||
|
||||
if (LOG_REQUEST_PATHS) {
|
||||
console.log('[%s]=', method, path)
|
||||
}
|
||||
|
||||
const req2 = request(
|
||||
{ hostname: host, port: esbuildPort, path, method, headers },
|
||||
(prxRes) => {
|
||||
const newHeaders = {
|
||||
...prxRes.headers,
|
||||
}
|
||||
|
||||
if (ENABLE_NETWORK_CACHING) {
|
||||
const hrInSeconds = 60*60*60
|
||||
newHeaders['cache-control'] = `max-age=${hrInSeconds}`;
|
||||
}
|
||||
|
||||
if (url === '/index.js') {
|
||||
const jsReloadCode =
|
||||
' (() => new EventSource("/esbuild").onmessage = () => location.reload())();'
|
||||
|
||||
newHeaders['content-length'] = parseInt(prxRes.headers['content-length'] ?? '0', 10) + jsReloadCode.length,
|
||||
|
||||
res.writeHead(prxRes.statusCode ?? 0, newHeaders)
|
||||
res.write(jsReloadCode)
|
||||
} else {
|
||||
res.writeHead(prxRes.statusCode ?? 0, newHeaders)
|
||||
}
|
||||
prxRes.pipe(res, { end: true })
|
||||
}
|
||||
)
|
||||
|
||||
req.pipe(req2, { end: true })
|
||||
}
|
||||
|
||||
forwardRequest(url ?? '/')
|
||||
}
|
||||
|
||||
const nonSslServer = createNonSslServer(handler)
|
||||
nonSslServer.on('error', function (e) {
|
||||
// Handle your error here
|
||||
console.log(e)
|
||||
})
|
||||
nonSslServer.listen(PORT, () => {
|
||||
log(`Running on:\n`)
|
||||
log(chalk.bold().cyan(` http://localhost:${PORT}`))
|
||||
log(`\nNetwork:\n`)
|
||||
log(chalk.bold().cyan(` http://${ip.address()}:${PORT}`))
|
||||
})
|
||||
|
||||
if (ENABLE_SSL) {
|
||||
const cert = generateCert({
|
||||
altNameIPs: ['127.0.0.1'],
|
||||
altNameURIs: ['localhost'],
|
||||
validityDays: 2,
|
||||
})
|
||||
const sslServer = createSslServer({ key: cert.privateKey, cert: cert.cert }, handler)
|
||||
sslServer.on('error', function (e) {
|
||||
// Handle your error here
|
||||
console.log(e)
|
||||
})
|
||||
sslServer.listen(SSL_PORT, () => {
|
||||
// TODO: Nasty, but gets detected by script runner
|
||||
log('[tldraw:process_ready]');
|
||||
|
||||
log(`Running on:\n`)
|
||||
log(chalk.bold().cyan(` https://localhost:${SSL_PORT}`))
|
||||
log(`\nNetwork:\n`)
|
||||
log(chalk.bold().cyan(` https://${ip.address()}:${SSL_PORT}`))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
main()
|
|
@ -1,36 +0,0 @@
|
|||
import { hardReset, Tldraw } from '@tldraw/tldraw'
|
||||
/* eslint-disable import/no-internal-modules */
|
||||
import { getAssetUrlsByImport } from '@tldraw/assets/imports'
|
||||
import '@tldraw/tldraw/editor.css'
|
||||
import '@tldraw/tldraw/ui.css'
|
||||
/* eslint-enable import/no-internal-modules */
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
webdriverReset: () => void
|
||||
}
|
||||
}
|
||||
|
||||
const assetUrls = getAssetUrlsByImport()
|
||||
|
||||
// NOTE: This is currently very similar to `apps/examples/src/1-basic/BasicExample.tsx`
|
||||
// and should probably stay that way to make writing new tests easier as it's
|
||||
// what we're most familiar with
|
||||
export default function Example() {
|
||||
const [instanceKey, setInstanceKey] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
window.webdriverReset = () => {
|
||||
hardReset({ shouldReload: false })
|
||||
setInstanceKey(instanceKey + 1)
|
||||
}
|
||||
}, [instanceKey])
|
||||
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw key={instanceKey} autoFocus assetUrls={assetUrls} />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap');
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
overscroll-behavior: none;
|
||||
touch-action: none;
|
||||
min-height: 100vh;
|
||||
/* mobile viewport bug fix */
|
||||
min-height: -webkit-fill-available;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tldraw__editor {
|
||||
position: fixed;
|
||||
inset: 0px;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import { DefaultErrorFallback, ErrorBoundary } from '@tldraw/tldraw'
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
|
||||
import ExampleBasic from './1-basic/BasicExampleWebdriver'
|
||||
import './index.css'
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <ExampleBasic />,
|
||||
},
|
||||
])
|
||||
|
||||
const rootElement = document.getElementById('root')
|
||||
const root = createRoot(rootElement!)
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ErrorBoundary
|
||||
fallback={(error) => <DefaultErrorFallback error={error} />}
|
||||
onError={(error) => console.error(error)}
|
||||
>
|
||||
<RouterProvider router={router} />
|
||||
</ErrorBoundary>
|
||||
</StrictMode>
|
||||
)
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"removeComments": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"baseUrl": ".",
|
||||
"composite": true,
|
||||
"sourceMap": false,
|
||||
"importHelpers": false,
|
||||
"skipDefaultLibCheck": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["src", "scripts"],
|
||||
"references": [{ "path": "../../packages/tldraw" }]
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/tldraw.svg" />
|
||||
<link id="manifest" rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>tldraw</title>
|
||||
<script type="module" crossorigin src="/index.js"></script>
|
||||
<link rel="stylesheet" href="/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
175
e2e/CHANGELOG.md
175
e2e/CHANGELOG.md
|
@ -1,175 +0,0 @@
|
|||
# @tldraw/e2e
|
||||
|
||||
## 2.0.0-alpha.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Release day!
|
||||
|
||||
## 2.0.0-alpha.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Bug fixes.
|
||||
|
||||
## 2.0.0-alpha.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Add licenses.
|
||||
|
||||
## 2.0.0-alpha.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Add CSS files to tldraw/tldraw.
|
||||
|
||||
## 2.0.0-alpha.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Add children to tldraw/tldraw
|
||||
|
||||
## 2.0.0-alpha.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Change permissions.
|
||||
|
||||
## 2.0.0-alpha.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Add tldraw, editor
|
||||
|
||||
## 0.1.0-alpha.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix stale reactors.
|
||||
|
||||
## 0.1.0-alpha.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix type export bug.
|
||||
|
||||
## 0.1.0-alpha.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix import bugs.
|
||||
|
||||
## 0.1.0-alpha.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Changes validation requirements, exports validation helpers.
|
||||
|
||||
## 0.1.0-alpha.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- - Pre-pre-release update
|
||||
|
||||
## 0.0.2-alpha.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix error with HMR
|
||||
|
||||
## 0.0.2-alpha.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Initial release
|
||||
|
||||
## 0.0.1-alpha.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Initial release
|
||||
|
||||
## 0.0.1-alpha.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- -
|
||||
|
||||
## 0.0.1-alpha.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- -
|
||||
|
||||
## 0.0.1-alpha.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- -
|
||||
|
||||
## 0.0.1-alpha.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- -
|
||||
|
||||
## 0.0.1-alpha.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- -
|
||||
|
||||
## 0.0.1-alpha.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- -
|
||||
|
||||
## 0.0.1-alpha.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- -
|
||||
|
||||
## 0.0.1-alpha.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 16495ef7: -
|
||||
|
||||
## 0.0.1-alpha.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Changed a few things.
|
||||
|
||||
## 0.0.1-alpha.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Remove bundling
|
||||
|
||||
## 0.0.1-alpha.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix UI package.
|
||||
|
||||
## 0.0.1-alpha.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Add modules to missing packages
|
||||
|
||||
## 0.0.1-alpha.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Add module
|
||||
|
||||
## 0.0.1-alpha.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Add css to dist
|
333
e2e/README.md
333
e2e/README.md
|
@ -1,333 +0,0 @@
|
|||
# Webdriver
|
||||
|
||||
This docs describes Webdriver testing within the app.
|
||||
|
||||
Webdriver testing can be tricky because you're sending commands to an actual browser running on either you machine or browserstack. This can be slow but it's currently the only way to test things within a wide range of browsers without emulation. This give us the best chance of having a stable app across a range of browsers without excessive manual testing
|
||||
|
||||
> **A note on stability**: Webdriver tests are a lot more flakey than other types of testing, the major benefit is that you can run them on real devices, so we can hopefully get a good smoke test of various real devices. You can also screenshot those devices during test runs, to check look. You however probably don't want to write too many webdriver tests, they are best placed for smoke testing and testing stuff that's very browser specific.
|
||||
|
||||
There is a script called `yarn e2e`, running `yarn e2e --help` will show you it's usage
|
||||
|
||||
```
|
||||
Usage: yarn e2e <command> [options]
|
||||
|
||||
Commands:
|
||||
yarn e2e serve start test server
|
||||
yarn e2e test:ci runner for CI (github-actions)
|
||||
yarn e2e test:local run webdriver tests locally
|
||||
yarn e2e test:browserstack run webdriver tests on browserstack
|
||||
yarn e2e selenium:grid start selenium grid (test linux)
|
||||
|
||||
Options:
|
||||
--help Show help [boolean]
|
||||
--version Show version number [boolean]
|
||||
```
|
||||
|
||||
To run the tests you first must start the server with
|
||||
|
||||
```sh
|
||||
yarn e2e serve
|
||||
```
|
||||
|
||||
You can then either test locally with
|
||||
|
||||
```sh
|
||||
yarn e2e test:local
|
||||
```
|
||||
|
||||
By default it'll just run the chrome tests. You can also specify other browsers to run like this
|
||||
|
||||
```sh
|
||||
# Note edge, safari are a work in progress and will just be skipped for now.
|
||||
yarn e2e test:local -b firefox,chrome,edge,safari
|
||||
```
|
||||
|
||||
Or to test remotely via browserstack
|
||||
|
||||
```sh
|
||||
yarn e2e test:browserstack
|
||||
```
|
||||
|
||||
**Note**: You'll need to set `BROWSERSTACK_USER` and `BROWSERSTACK_KEY` in your environment, which you can grab from <https://automate.browserstack.com/dashboard>
|
||||
|
||||
There are three parts to the testing API
|
||||
|
||||
- `runtime` — the tldraw `App` instance in the browser window. This calls methods in the JS runtime of the app
|
||||
- `ui` — methods to interacting with the apps UI elements
|
||||
- `util` — some general helper methods
|
||||
|
||||
The `ui` is further broken up into
|
||||
|
||||
![test overview ui](test-overview-ui.png)
|
||||
|
||||
tldraw room for above is at <https://www.tldraw.com/v/CZw_c_7vMVkHpMcMWcMm1z8ZpN>
|
||||
|
||||
## Nightly tests
|
||||
We run all the tests on all browsers via browserstack each night at 02:00am. You can search https://github.com/tldraw/tldraw-lite/actions/workflows/webdriver-nightly.yml for the results of the previous evenings tests
|
||||
|
||||
## On demand tests
|
||||
To run tests on demand via github actions you can head to https://github.com/tldraw/tldraw-lite/actions/workflows/webdriver-on-demand.yml and select **run workflow** with you desired options and press the **run workflow** button
|
||||
|
||||
https://user-images.githubusercontent.com/235915/233660925-866d2db3-66f9-4e6c-b19a-8da597c8b512.mov
|
||||
|
||||
## Ongoing issues
|
||||
|
||||
You can see the on-going test issues in the project at <https://linear.app/tldraw/project/webdriver-tests-409b44580c4a/TLD>
|
||||
|
||||
Performance is a little slow remotely mostly to do with
|
||||
|
||||
- startup time of the browser, see <https://linear.app/tldraw/issue/TLD-1301/find-way-to-prevent-runner-from-running-if-all-tests-are-skipped-for>
|
||||
- app reset/refresh time, see <https://linear.app/tldraw/issue/TLD-1293/webdriver-setup-performance>
|
||||
|
||||
## Writing new tests
|
||||
|
||||
There are target tests with the `.todo(...)` suffix, an example might be
|
||||
|
||||
https://github.com/tldraw/tldraw-lite/blob/7ff0dd111bb9bf9b931d53f61722842780d9793e/e2e/test/specs/shortcuts.ts#L114
|
||||
|
||||
The missing tests (`.todo`) follow a lot of the same patterns that already exist in the codebase. Firefox is skipped quite a lot in the test runners, this is due to issues with the app and tests, so those also need to be resolved. You can search for `FIXME` in the `./e2e` directory to find those.
|
||||
|
||||
## Test writing guide
|
||||
|
||||
Below explains the process of writing a new test. Most actions don't have anything specific for a particular browser, however most actions do have specifics per-layout and viewport size. Lets walk through and explain a very simple test, the draw shortcut.
|
||||
|
||||
When pressing `d` in the browser on a desktop environment we should be changing the current state to `draw.idle`. Below is the test comments in line.
|
||||
|
||||
```js
|
||||
// These are helpers that we use in our tests to interact with our app via the browser.
|
||||
import { runtime, ui } from '../helpers'
|
||||
|
||||
// The runner uses mocha, `env` is an addition added by mocha-ext and `it` is
|
||||
// extended to support skipping of tests in certain environments.
|
||||
import { describe, env, it } from '../mocha-ext'
|
||||
|
||||
// Always group tests, this make them easy to run in isolation by changing
|
||||
// `describe(...)` to `describe.only(...)`
|
||||
describe('shortcuts', () => {
|
||||
// The `env` command is used to mark groups of tests as skippable under
|
||||
// certain conditions, here we're only running these tests under desktop
|
||||
// environments. Note that is **REALLY** important we don't just wrap this
|
||||
// in a `if(FOO) {...}` block. Otherwise `.only` would break under certain
|
||||
// circumstances
|
||||
env({ device: 'desktop' }, () => {
|
||||
// We haven't written this test yet, we can mark those with a `.todo`
|
||||
// This appears as `[TODO] {test name}` in the test output.
|
||||
it.todo('select — V', async () => {
|
||||
await ui.app.setup()
|
||||
await browser.keys(['v'])
|
||||
})
|
||||
|
||||
// Here is our draw shortcut test, all tests are going to be async as
|
||||
// commands are sent to the browser over HTTP
|
||||
it('draw — D', async () => {
|
||||
// We must always `setup` our app. This starts the browser (if it
|
||||
// isn't already) and does a hard reset. Note **NEVER** do this in
|
||||
// a mocha `before(...)` hook. Else you'll end up starting the browser
|
||||
// (doing heavy work) whether or not you actually run the test suite.
|
||||
// I believe this is a mocha/webdriver integration bug.
|
||||
await ui.app.setup()
|
||||
|
||||
// `browser` is a global from <https://webdriver.io/docs/api/browser>
|
||||
// This is the default way to interact with the browser. There are
|
||||
// also lots of helpers in `import { ui } from '../helpers'` module.
|
||||
// Here we're instructing the browser to enter `d` to the currently
|
||||
// selected element
|
||||
await browser.keys(['d'])
|
||||
|
||||
// Although we've pressed `d` we don't know how long that's going to
|
||||
// take to process in the browser. A slow environment might take
|
||||
// sometime to update the runtime. So we use `waitUntil` to wait
|
||||
// for the change. **DON'T** use `util.sleep(...)` here as this will
|
||||
// only work if the browser/env is fast and will generally slow down
|
||||
// the test runner and make things flakey.
|
||||
await browser.waitUntil(async () => {
|
||||
// `runtime` executes JS in the JS-runtime on the device. This
|
||||
// is to test for conditions to be met.
|
||||
return await runtime.isIn('draw.idle')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
To run the above tests, we'd probably want to first isolate the test and `only` run a single test, or test group. Change
|
||||
|
||||
```diff
|
||||
- it('draw — D', async () => {
|
||||
+ it.only('draw — D', async () => {
|
||||
```
|
||||
|
||||
Now lets start the dev server and run the tests locally. In one terminal session run
|
||||
|
||||
```sh
|
||||
yarn e2e serve
|
||||
```
|
||||
|
||||
In another terminal session run
|
||||
|
||||
```sh
|
||||
yarn e2e test:local
|
||||
```
|
||||
|
||||
The test should run twice in chrome. Once in desktop chrome and once chrome mobile emulation mode. The results should look something like
|
||||
|
||||
```
|
||||
------------------------------------------------------------------
|
||||
[chrome 109.0.5414.119 mac os x #0-0] Running: chrome (v109.0.5414.119) on mac os x
|
||||
[chrome 109.0.5414.119 mac os x #0-0] Session ID: 599e14341d743b938056560f3a467361
|
||||
[chrome 109.0.5414.119 mac os x #0-0]
|
||||
[chrome 109.0.5414.119 mac os x #0-0] » /test/specs/index.ts
|
||||
[chrome 109.0.5414.119 mac os x #0-0] shortcuts
|
||||
[chrome 109.0.5414.119 mac os x #0-0] ✓ draw — D (ignored only: desktop)
|
||||
[chrome 109.0.5414.119 mac os x #0-0]
|
||||
[chrome 109.0.5414.119 mac os x #0-0] 1 passing (59ms)
|
||||
------------------------------------------------------------------
|
||||
[chrome 109.0.5414.119 mac os x #1-0] Running: chrome (v109.0.5414.119) on mac os x
|
||||
[chrome 109.0.5414.119 mac os x #1-0] Session ID: 2b813d4f8793f910c1a02f771438e3f7
|
||||
[chrome 109.0.5414.119 mac os x #1-0]
|
||||
[chrome 109.0.5414.119 mac os x #1-0] » /test/specs/index.ts
|
||||
[chrome 109.0.5414.119 mac os x #1-0] shortcuts
|
||||
[chrome 109.0.5414.119 mac os x #1-0] ✓ draw — D
|
||||
[chrome 109.0.5414.119 mac os x #1-0]
|
||||
[chrome 109.0.5414.119 mac os x #1-0] 1 passing (2s)
|
||||
|
||||
|
||||
Spec Files: 2 passed, 2 total (100% completed) in 00:00:05
|
||||
```
|
||||
|
||||
Yay, passing tests 🎉
|
||||
|
||||
However we're not done quite yet. Next up, we need to test them across the rest of our supported browsers. With the test server still running
|
||||
|
||||
```sh
|
||||
yarn e2e test:browserstack
|
||||
```
|
||||
|
||||
This will start a tunnel from browserstack to your local machine, running the tests against the local server. You can head to browserstack to see the completed test suite <https://automate.browserstack.com/dashboard/v2/builds/2d87ffb21f5466b93e6ef8b4007df995d23da7b3>
|
||||
|
||||
In your terminal you should see the results
|
||||
|
||||
```
|
||||
------------------------------------------------------------------
|
||||
[chrome 110.0.5481.78 windows #0-0] Running: chrome (v110.0.5481.78) on windows
|
||||
[chrome 110.0.5481.78 windows #0-0] Session ID: 5123d74060f5dac19a960c50476b32007d71b758
|
||||
[chrome 110.0.5481.78 windows #0-0]
|
||||
[chrome 110.0.5481.78 windows #0-0] » /test/specs/index.ts
|
||||
[chrome 110.0.5481.78 windows #0-0] shortcuts
|
||||
[chrome 110.0.5481.78 windows #0-0] ✓ draw — D
|
||||
[chrome 110.0.5481.78 windows #0-0]
|
||||
[chrome 110.0.5481.78 windows #0-0] 1 passing (36.1s)
|
||||
------------------------------------------------------------------
|
||||
[msedge 109.0.1518.49 WINDOWS #1-0] Running: msedge (v109.0.1518.49) on WINDOWS
|
||||
[msedge 109.0.1518.49 WINDOWS #1-0] Session ID: fd44f7771955021532c16a151b2ccb7657d53cda
|
||||
[msedge 109.0.1518.49 WINDOWS #1-0]
|
||||
[msedge 109.0.1518.49 WINDOWS #1-0] » /test/specs/index.ts
|
||||
[msedge 109.0.1518.49 WINDOWS #1-0] shortcuts
|
||||
[msedge 109.0.1518.49 WINDOWS #1-0] ✓ draw — D
|
||||
[msedge 109.0.1518.49 WINDOWS #1-0]
|
||||
[msedge 109.0.1518.49 WINDOWS #1-0] 1 passing (9.2s)
|
||||
------------------------------------------------------------------
|
||||
[firefox 109.0 WINDOWS #2-0] Running: firefox (v109.0) on WINDOWS
|
||||
[firefox 109.0 WINDOWS #2-0] Session ID: b590ddccd02ee3a9f3f0ebb92c19d2c5f81421b9
|
||||
[firefox 109.0 WINDOWS #2-0]
|
||||
[firefox 109.0 WINDOWS #2-0] » /test/specs/index.ts
|
||||
[firefox 109.0 WINDOWS #2-0] shortcuts
|
||||
[firefox 109.0 WINDOWS #2-0] ✓ draw — D
|
||||
[firefox 109.0 WINDOWS #2-0]
|
||||
[firefox 109.0 WINDOWS #2-0] 1 passing (14.4s)
|
||||
------------------------------------------------------------------
|
||||
[chrome 110.0.5481.77 MAC #3-0] Running: chrome (v110.0.5481.77) on MAC
|
||||
[chrome 110.0.5481.77 MAC #3-0] Session ID: 12d8c60cfb3a20b516b300191e12c4d71952d607
|
||||
[chrome 110.0.5481.77 MAC #3-0]
|
||||
[chrome 110.0.5481.77 MAC #3-0] » /test/specs/index.ts
|
||||
[chrome 110.0.5481.77 MAC #3-0] shortcuts
|
||||
[chrome 110.0.5481.77 MAC #3-0] ✓ draw — D
|
||||
[chrome 110.0.5481.77 MAC #3-0]
|
||||
[chrome 110.0.5481.77 MAC #3-0] 1 passing (10.9s)
|
||||
------------------------------------------------------------------
|
||||
[firefox 109.0 MAC #4-0] Running: firefox (v109.0) on MAC
|
||||
[firefox 109.0 MAC #4-0] Session ID: be2fae1429977f192be311c7a1f6a177c0ff6170
|
||||
[firefox 109.0 MAC #4-0]
|
||||
[firefox 109.0 MAC #4-0] » /test/specs/index.ts
|
||||
[firefox 109.0 MAC #4-0] shortcuts
|
||||
[firefox 109.0 MAC #4-0] ✓ draw — D
|
||||
[firefox 109.0 MAC #4-0]
|
||||
[firefox 109.0 MAC #4-0] 1 passing (9.5s)
|
||||
------------------------------------------------------------------
|
||||
[msedge 109.0.1518.49 MAC #5-0] Running: msedge (v109.0.1518.49) on MAC
|
||||
[msedge 109.0.1518.49 MAC #5-0] Session ID: 0e59617ae8ac7e5403270c204e1108aa0b2a6c62
|
||||
[msedge 109.0.1518.49 MAC #5-0]
|
||||
[msedge 109.0.1518.49 MAC #5-0] » /test/specs/index.ts
|
||||
[msedge 109.0.1518.49 MAC #5-0] shortcuts
|
||||
[msedge 109.0.1518.49 MAC #5-0] ✓ draw — D
|
||||
[msedge 109.0.1518.49 MAC #5-0]
|
||||
[msedge 109.0.1518.49 MAC #5-0] 1 passing (8s)
|
||||
------------------------------------------------------------------
|
||||
[2B111FDH2007PR Android 13 #6-0] Running: 2B111FDH2007PR on Android 13 executing chrome
|
||||
[2B111FDH2007PR Android 13 #6-0] Session ID: 4317967dd4e85cff839a7965a34707a2c839e748
|
||||
[2B111FDH2007PR Android 13 #6-0]
|
||||
[2B111FDH2007PR Android 13 #6-0] » /test/specs/index.ts
|
||||
[2B111FDH2007PR Android 13 #6-0] shortcuts
|
||||
[2B111FDH2007PR Android 13 #6-0] ✓ draw — D (ignored only: desktop)
|
||||
[2B111FDH2007PR Android 13 #6-0]
|
||||
[2B111FDH2007PR Android 13 #6-0] 1 passing (1.6s)
|
||||
------------------------------------------------------------------
|
||||
[R5CR10PDY5Y Android 11 #7-0] Running: R5CR10PDY5Y on Android 11
|
||||
[R5CR10PDY5Y Android 11 #7-0] Session ID: 696e16707b7d9bfdcfc04d3dca08e4579a91af18
|
||||
[R5CR10PDY5Y Android 11 #7-0]
|
||||
[R5CR10PDY5Y Android 11 #7-0] » /test/specs/index.ts
|
||||
[R5CR10PDY5Y Android 11 #7-0] shortcuts
|
||||
[R5CR10PDY5Y Android 11 #7-0] ✓ draw — D (ignored only: desktop)
|
||||
[R5CR10PDY5Y Android 11 #7-0]
|
||||
[R5CR10PDY5Y Android 11 #7-0] 1 passing (1.8s)
|
||||
------------------------------------------------------------------
|
||||
[R3CR909M44J Android 11 #8-0] Running: R3CR909M44J on Android 11 executing chrome
|
||||
[R3CR909M44J Android 11 #8-0] Session ID: 797cf525cd07f287281a2051075e0b5e5edc9fd2
|
||||
[R3CR909M44J Android 11 #8-0]
|
||||
[R3CR909M44J Android 11 #8-0] » /test/specs/index.ts
|
||||
[R3CR909M44J Android 11 #8-0] shortcuts
|
||||
[R3CR909M44J Android 11 #8-0] ✓ draw — D (ignored only: desktop)
|
||||
[R3CR909M44J Android 11 #8-0]
|
||||
[R3CR909M44J Android 11 #8-0] 1 passing (2.1s)
|
||||
|
||||
|
||||
Spec Files: 9 passed, 9 total (100% completed) in 00:03:51
|
||||
```
|
||||
|
||||
Now you can remove the `.only` and open a PR and rejoice in your new found skills.
|
||||
|
||||
```diff
|
||||
- it.only('draw — D', async () => {
|
||||
+ it('draw — D', async () => {
|
||||
```
|
||||
|
||||
Existing tests are a good guide for writing more advance tests, hopefully this should give you a start 🤞
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
### `msedgedriver`
|
||||
You might notice that `msedgedriver` on version 91, and hasn't been updated in a while.
|
||||
|
||||
```
|
||||
"msedgedriver": "^91.0.0",
|
||||
```
|
||||
|
||||
This module isn't actually used but is required for `wdio-edgedriver-service` to start where we pass a custom path via the `edgedriverCustomPath` option.
|
||||
|
||||
### `safaridriver`
|
||||
Locally safari webdriver tests are currently somewhat buggy, there appear to be some tests that don't complete. Please take on this task if you have time 🙂
|
||||
|
||||
|
||||
## Experimental
|
||||
You can now also run _linux_ tests on _macos_. To do this start up a selenium grid with. Note, you **must** have `docker` installed locally.
|
||||
|
||||
```sh
|
||||
yarn e2e selenium:grid
|
||||
```
|
||||
|
||||
Then run
|
||||
|
||||
```sh
|
||||
yarn e2e test:local --os linux -b firefox
|
||||
```
|
||||
|
||||
**Note**: Currently only `firefox` is supported. You can hit <http://localhost:7900/?autoconnect=1&resize=scale&password=secret> to see a VNC of the app runing in a docker container.
|
|
@ -1,3 +0,0 @@
|
|||
# downloads
|
||||
|
||||
Location for temporary downloads.
|
|
@ -1,62 +0,0 @@
|
|||
{
|
||||
"name": "@tldraw/e2e",
|
||||
"version": "2.0.0-alpha.8",
|
||||
"private": true,
|
||||
"packageManager": "yarn@3.5.0",
|
||||
"author": {
|
||||
"name": "tldraw GB Ltd.",
|
||||
"email": "hello@tldraw.com"
|
||||
},
|
||||
"homepage": "https://tldraw.dev",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tldraw/tldraw"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tldraw/tldraw/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"tldraw",
|
||||
"drawing",
|
||||
"app",
|
||||
"development",
|
||||
"whiteboard",
|
||||
"canvas",
|
||||
"infinite"
|
||||
],
|
||||
"scripts": {
|
||||
"test:local": "TS_NODE_PROJECT=./tsconfig.json wdio run wdio.local.conf.js",
|
||||
"test:browserstack": "TS_NODE_PROJECT=./tsconfig.json wdio run wdio.browserstack.conf.js",
|
||||
"test:nightly": "TS_NODE_PROJECT=./tsconfig.json wdio run wdio.nightly.conf.js",
|
||||
"lint": "yarn run -T tsx ../scripts/lint.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sitespeed.io/edgedriver": "^112.0.1722-34",
|
||||
"@tldraw/editor": "workspace:*",
|
||||
"@tldraw/indices": "workspace:*",
|
||||
"@tldraw/primitives": "workspace:*",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@wdio/browserstack-service": "^8.10.1",
|
||||
"@wdio/cli": "^8.1.3",
|
||||
"@wdio/globals": "^8.1.3",
|
||||
"@wdio/local-runner": "^8.1.2",
|
||||
"@wdio/mocha-framework": "^8.1.2",
|
||||
"@wdio/spec-reporter": "^8.1.2",
|
||||
"chromedriver": "^112.0.0",
|
||||
"geckodriver": "^3.2.0",
|
||||
"lazyrepo": "0.0.0-alpha.26",
|
||||
"msedgedriver": "^91.0.0",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"pngjs": "^6.0.0",
|
||||
"sharp": "^0.31.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"ua-parser-js": "^1.0.33",
|
||||
"wdio-chromedriver-service": "^8.0.1",
|
||||
"wdio-edgedriver-service": "^2.1.2",
|
||||
"wdio-geckodriver-service": "^4.1.1",
|
||||
"wdio-safaridriver-service": "^2.1.0",
|
||||
"wdio-vscode-service": "^5.1.0"
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
# screenshots
|
||||
|
||||
Location for temporary screenshots.
|
Binary file not shown.
Before Width: | Height: | Size: 210 KiB |
|
@ -1,3 +0,0 @@
|
|||
export const MOVE_DEFAULTS = {
|
||||
duration: 20,
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export { MOVE_DEFAULTS } from './constants'
|
||||
export * as runtime from './runtime'
|
||||
export * as ui from './ui'
|
||||
export * as util from './util'
|
|
@ -1,46 +0,0 @@
|
|||
import { App, TLShape } from '@tldraw/editor'
|
||||
import { Box2d } from '@tldraw/primitives'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
app: App
|
||||
webdriverReset: () => void
|
||||
}
|
||||
}
|
||||
|
||||
export async function isIn(path: string) {
|
||||
return await browser.execute((path) => window.app.isIn(path), path)
|
||||
}
|
||||
|
||||
export async function propsForNextShape() {
|
||||
return await browser.execute(() => window.app.instanceState.propsForNextShape)
|
||||
}
|
||||
|
||||
export function getAllShapes() {
|
||||
return browser.execute(() => window.app.shapesArray)
|
||||
}
|
||||
export async function getShapesOfType(...types: TLShape['type'][]) {
|
||||
return await browser.execute((types) => {
|
||||
return window.app.store
|
||||
.allRecords()
|
||||
.filter((s) => s.typeName === 'shape' && types.includes(s.type))
|
||||
}, types)
|
||||
}
|
||||
|
||||
export async function selectionBounds(): Promise<Box2d> {
|
||||
return await browser.execute(() => {
|
||||
return window.app.selectionBounds
|
||||
})
|
||||
}
|
||||
|
||||
export async function getCamera() {
|
||||
return await browser.execute(() => {
|
||||
const { x, y, z } = window.app.camera
|
||||
return { x, y, z }
|
||||
})
|
||||
}
|
||||
export async function hardReset() {
|
||||
await browser.execute(() => {
|
||||
window.webdriverReset()
|
||||
})
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
import { ui, util } from '..'
|
||||
import * as runtime from '../runtime'
|
||||
|
||||
export function wd(key: string) {
|
||||
return `*[data-wd="${key}"]`
|
||||
}
|
||||
|
||||
export async function pointWithinActiveArea(x: number, y: number) {
|
||||
const offsetX = 0
|
||||
const offsetY = 52
|
||||
return { x: x + offsetX, y: y + offsetY }
|
||||
}
|
||||
|
||||
export async function waitForReady() {
|
||||
await Promise.any([
|
||||
new Promise<boolean>((r) => {
|
||||
browser.waitUntil(() => browser.execute(() => window.tldrawReady)).then(() => r(true))
|
||||
}),
|
||||
new Promise<boolean>((r) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('waitFor failed, using timeout')
|
||||
setTimeout(() => r(true), 2000)
|
||||
}),
|
||||
])
|
||||
|
||||
// Make sure the window is focused... maybe
|
||||
await ui.canvas.click(100, 100)
|
||||
|
||||
// Note: We need to not trigger the double click handler here so we need to wait a little bit.
|
||||
await util.sleep(300)
|
||||
}
|
||||
|
||||
export async function hardReset() {
|
||||
await runtime.hardReset()
|
||||
await waitForReady()
|
||||
}
|
||||
|
||||
export async function open() {
|
||||
await browser.url(global.webdriverTestUrl ?? `https://localhost:5420/`)
|
||||
/**
|
||||
* HACK: vscode doesn't support `browser.setWindowSize` so we use the
|
||||
* default size.
|
||||
*
|
||||
* This will break things currently if run on a small screen.
|
||||
*/
|
||||
if (global.tldrawOptions.windowSize !== 'default') {
|
||||
const windowSize = global.tldrawOptions.windowSize ?? [1200, 1200]
|
||||
await browser.setWindowSize(windowSize[0], windowSize[1])
|
||||
}
|
||||
|
||||
await waitForReady()
|
||||
global.isWindowOpen = true
|
||||
}
|
||||
|
||||
export async function shapesAsImgData() {
|
||||
return await browser.execute(async () => {
|
||||
return await window.app
|
||||
.getSvg([...window.app.shapeIds], { padding: 0, background: true })
|
||||
.then(async (svg) => {
|
||||
const svgStr = new XMLSerializer().serializeToString(svg)
|
||||
const svgImage = document.createElement('img')
|
||||
document.body.appendChild(svgImage)
|
||||
svgImage.src = URL.createObjectURL(
|
||||
new Blob([svgStr], {
|
||||
type: 'image/svg+xml',
|
||||
})
|
||||
)
|
||||
|
||||
const dpr = window.devicePixelRatio
|
||||
|
||||
return await new Promise<{
|
||||
width: number
|
||||
height: number
|
||||
dpr: number
|
||||
data: string
|
||||
}>((resolve) => {
|
||||
svgImage.onload = () => {
|
||||
const width = parseInt(svg.getAttribute('width'))
|
||||
const height = parseInt(svg.getAttribute('height'))
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
const canvasCtx = canvas.getContext('2d')
|
||||
canvasCtx.drawImage(svgImage, 0, 0, width * dpr, height * dpr)
|
||||
const imgData = canvas.toDataURL('image/png')
|
||||
resolve({
|
||||
dpr: dpr,
|
||||
width: width * dpr,
|
||||
height: height * dpr,
|
||||
data: imgData,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
global.isWindowOpen = false
|
||||
|
||||
export async function setup() {
|
||||
if (!global.isWindowOpen) {
|
||||
await open()
|
||||
} else {
|
||||
await hardReset()
|
||||
}
|
||||
}
|
||||
|
||||
export async function getElementByWd(...selectors: string[]) {
|
||||
for (const possibleSelector of selectors) {
|
||||
const element = wd(possibleSelector)
|
||||
const isDisplayed = await $(element).isDisplayed()
|
||||
if (isDisplayed) {
|
||||
return $(element)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
import { MOVE_DEFAULTS } from '../constants'
|
||||
import { getElementByWd, pointWithinActiveArea, wd } from './app'
|
||||
|
||||
export async function brush(x1: number, y1: number, x2: number, y2: number) {
|
||||
const start = await pointWithinActiveArea(x1, y1)
|
||||
const end = await pointWithinActiveArea(x2, y2)
|
||||
|
||||
await browser
|
||||
.action('pointer')
|
||||
.move({ ...start, ...MOVE_DEFAULTS })
|
||||
.down()
|
||||
.move(end)
|
||||
.up()
|
||||
.perform()
|
||||
}
|
||||
|
||||
export async function draw(points: { x: number; y: number }[]) {
|
||||
const mappedPoints = []
|
||||
for (const point of points) {
|
||||
mappedPoints.push(await pointWithinActiveArea(point.x, point.y))
|
||||
}
|
||||
|
||||
let chain = browser.action('pointer')
|
||||
for (const [index, mappedPoint] of mappedPoints.entries()) {
|
||||
if (index === 0) {
|
||||
chain = chain.move({ ...mappedPoint, ...MOVE_DEFAULTS }).down()
|
||||
} else {
|
||||
chain = chain.move({ ...mappedPoint, ...MOVE_DEFAULTS })
|
||||
}
|
||||
}
|
||||
await chain.perform()
|
||||
}
|
||||
|
||||
export async function click(x1: number, y1: number) {
|
||||
const start = await pointWithinActiveArea(x1, y1)
|
||||
await browser
|
||||
.action('pointer')
|
||||
.move({ ...start, ...MOVE_DEFAULTS })
|
||||
.down()
|
||||
.up()
|
||||
.perform()
|
||||
}
|
||||
|
||||
export async function doubleClick(x1: number, y1: number) {
|
||||
const start = await pointWithinActiveArea(x1, y1)
|
||||
await browser
|
||||
.action('pointer')
|
||||
.move({ ...start, ...MOVE_DEFAULTS })
|
||||
.down()
|
||||
.up()
|
||||
.down()
|
||||
.up()
|
||||
.perform()
|
||||
}
|
||||
|
||||
export async function dragBy(target: WebdriverIO.Element, dx: number, dy: number) {
|
||||
const loc = await target.getLocation()
|
||||
const size = await target.getSize()
|
||||
const locX = Math.floor(loc.x) + Math.floor(size.width / 2)
|
||||
const locY = Math.floor(loc.y) + Math.floor(size.height / 2)
|
||||
|
||||
const startX = locX
|
||||
const startY = locY
|
||||
const endX = locX + dx
|
||||
const endY = locY + dy
|
||||
|
||||
await browser.actions([
|
||||
browser
|
||||
.action('pointer')
|
||||
.move({ x: startX, y: startY, ...MOVE_DEFAULTS })
|
||||
.down('left')
|
||||
.move({ x: endX, y: endY, ...MOVE_DEFAULTS })
|
||||
.up(),
|
||||
])
|
||||
}
|
||||
|
||||
export async function contextMenu(x: number, y: number, path: string[] = []) {
|
||||
await browser
|
||||
.action('pointer')
|
||||
.move({ x, y, ...MOVE_DEFAULTS })
|
||||
.down('right')
|
||||
.up()
|
||||
.perform()
|
||||
// await $(wd('active-area')).click({button: 2, x, y})
|
||||
|
||||
for await (const item of path) {
|
||||
await $(wd(`menu-item.${item}`)).waitForExist()
|
||||
await $(wd(`menu-item.${item}`)).click()
|
||||
}
|
||||
}
|
||||
|
||||
export async function clickTextInput() {
|
||||
await (await $(wd(`canvas`) + ' textarea')).click()
|
||||
}
|
||||
|
||||
export async function selectionHandle(...possibleSelectors: string[]) {
|
||||
return getElementByWd(...possibleSelectors.map((s) => `selection.${s}`))
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export async function menu(_path: string[] = []) {
|
||||
// TODO...
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
export * as app from './app'
|
||||
export * as canvas from './canvas'
|
||||
export * as help from './help'
|
||||
export * as main from './main'
|
||||
export * as minimap from './minimap'
|
||||
export * as props from './props'
|
||||
export * as share from './share'
|
||||
export * as tools from './tools'
|
|
@ -1,21 +0,0 @@
|
|||
import { wd } from './app'
|
||||
|
||||
export async function menu(path = []) {
|
||||
await $(wd('main.menu')).click()
|
||||
|
||||
for await (const item of path) {
|
||||
await $(wd(`menu-item.${item}`)).click()
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionMenu(path = []) {
|
||||
await $(wd('main.action-menu')).click()
|
||||
|
||||
for await (const item of path) {
|
||||
await $(wd(`menu-item.${item}`)).click()
|
||||
}
|
||||
}
|
||||
|
||||
export async function click(key: string) {
|
||||
await $(wd(`main.${key}`)).click()
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import { wd } from './app'
|
||||
|
||||
export function $element() {
|
||||
return $(wd('minimap'))
|
||||
}
|
||||
|
||||
export async function zoomIn() {
|
||||
const button = wd(`minimap.zoom-in`)
|
||||
const toggle = wd(`minimap.toggle`)
|
||||
|
||||
if (await $(button).isExisting()) {
|
||||
await $(button).click()
|
||||
} else if (await $(wd(`minimap.toggle`)).isExisting()) {
|
||||
await $(toggle).click()
|
||||
await $(button).click()
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
export async function zoomOut() {
|
||||
const button = wd(`minimap.zoom-out`)
|
||||
const toggle = wd(`minimap.toggle`)
|
||||
|
||||
if (await $(button).isExisting()) {
|
||||
await $(button).click()
|
||||
} else if (await $(wd(`minimap.toggle`)).isExisting()) {
|
||||
await $(toggle).click()
|
||||
await $(button).click()
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
export async function menuButton() {
|
||||
return await $(wd('minimap.zoom-menu'))
|
||||
}
|
||||
|
||||
export async function menu(path: string[] = []) {
|
||||
await $(wd('minimap.zoom-menu')).click()
|
||||
|
||||
for await (const item of path) {
|
||||
await $(wd(`minimap.zoom-menu.${item}`)).click()
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
import { wd } from './app'
|
||||
|
||||
export async function ifMobileOpenStylesMenu() {
|
||||
if (globalThis.tldrawOpts.ui === 'mobile') {
|
||||
await $(wd(`mobile.styles`)).click()
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectColor(color: string) {
|
||||
await $(wd(`style.color.${color}`)).click()
|
||||
}
|
||||
|
||||
export async function selectFill(fill: string) {
|
||||
await $(wd(`style.fill.${fill}`)).click()
|
||||
}
|
||||
|
||||
export async function selectStroke(stroke: string) {
|
||||
await $(wd(`style.dash.${stroke}`)).click()
|
||||
}
|
||||
|
||||
export async function selectSize(size: string) {
|
||||
await $(wd(`style.size.${size}`)).click()
|
||||
}
|
||||
|
||||
export async function selectSpline(type: string) {
|
||||
await $(wd(`style.spline`)).click()
|
||||
await $(wd(`style.spline.${type}`)).click()
|
||||
}
|
||||
|
||||
export async function selectOpacity(_opacity: number) {
|
||||
// TODO...
|
||||
}
|
||||
|
||||
export async function selectShape() {
|
||||
// TODO...
|
||||
}
|
||||
|
||||
export async function selectFont(font: string) {
|
||||
await $(wd(`font.${font}`)).click()
|
||||
}
|
||||
|
||||
export async function selectAlign(alignment: string) {
|
||||
await $(wd(`align.${alignment}`)).click()
|
||||
}
|
||||
|
||||
export async function selectArrowheadStart(type: string) {
|
||||
await $(wd(`style.arrowheads.start`)).click()
|
||||
await $(wd(`style.arrowheads.start.${type}`)).click()
|
||||
}
|
||||
|
||||
export async function selectArrowheadEnd(type: string) {
|
||||
await $(wd(`style.arrowheads.end`)).click()
|
||||
await $(wd(`style.arrowheads.end.${type}`)).click()
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export async function menu(_path: string[] = []) {}
|
|
@ -1,21 +0,0 @@
|
|||
import { wd } from './app'
|
||||
|
||||
export function $element() {
|
||||
return $(wd('tools'))
|
||||
}
|
||||
|
||||
export async function click(toolName: string) {
|
||||
// Check if `tools.mobile-more` exists
|
||||
// Check ifisExisting()
|
||||
const toolSelector = wd(`tools.${toolName}`)
|
||||
const moreSelector = wd(`tools.more`)
|
||||
|
||||
if (await $(toolSelector).isExisting()) {
|
||||
await $(toolSelector).click()
|
||||
} else if (await $(moreSelector).isExisting()) {
|
||||
await $(moreSelector).click()
|
||||
await $(toolSelector).click()
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
import fs from 'fs'
|
||||
import { ui } from '.'
|
||||
|
||||
export async function textToClipboard(text: string) {
|
||||
const html = `<p id="copy_target">${text}</p>`
|
||||
const url = `data:text/html;base64,${btoa(html)}`
|
||||
|
||||
await browser.newWindow('', {
|
||||
windowName: 'copy_target',
|
||||
// windowFeatures: 'width=420,height=230,resizable,scrollbars=yes,status=1',
|
||||
})
|
||||
await browser.url(url)
|
||||
await $('#copy_target').waitForExist()
|
||||
|
||||
await $('#copy_target').click()
|
||||
|
||||
// For some reason the import isn't working...
|
||||
// From <https://github.com/webdriverio/webdriverio/blob/3620e90e47b6d3e62832f5de24f43cee6b31e972/packages/webdriverio/src/constants.ts#L360>
|
||||
const cmd = 'WDIO_CONTROL'
|
||||
|
||||
// Select all
|
||||
await browser.action('key').down(cmd).down('a').up(cmd).up('a').perform()
|
||||
|
||||
await browser.execute(() => new Promise((resolve) => setTimeout(resolve, 3000)))
|
||||
|
||||
// Copy
|
||||
await browser.action('key').down(cmd).down('c').up(cmd).up('c').perform()
|
||||
|
||||
await browser.closeWindow()
|
||||
|
||||
const handles = await browser.getWindowHandles()
|
||||
await browser.switchToWindow(handles[0])
|
||||
}
|
||||
|
||||
const LOCAL_DOWNLOAD_DIR = process.env.DOWNLOADS_DIR
|
||||
? process.env.DOWNLOADS_DIR + '/'
|
||||
: __dirname + '/../../downloads/'
|
||||
export async function getDownloadFile(fileName: string) {
|
||||
if (global.webdriverService === 'browserstack') {
|
||||
// In browserstack we must grab it from the service
|
||||
// <https://www.browserstack.com/docs/automate/selenium/test-file-download#nodejs>
|
||||
// Note this only works on desktop devices.
|
||||
const base64String = await browser.executeScript(
|
||||
`browserstack_executor: {"action": "getFileContent", "arguments": {"fileName": "${fileName}"}}`,
|
||||
[]
|
||||
)
|
||||
const buffer = Buffer.from(base64String, 'base64')
|
||||
return buffer
|
||||
} else {
|
||||
// Locally we can grab the file from the `LOCAL_DOWNLOAD_DIR`
|
||||
return await fs.promises.readFile(LOCAL_DOWNLOAD_DIR + fileName)
|
||||
}
|
||||
}
|
||||
|
||||
export async function imageToClipboard(_buffer: Buffer) {
|
||||
// TODO...
|
||||
}
|
||||
|
||||
export async function htmlToClipboard(_html: string) {
|
||||
// TODO...
|
||||
}
|
||||
|
||||
export async function nativeCopy() {
|
||||
// CMD+C
|
||||
await browser.action('key').down('WDIO_CONTROL').down('c').up('WDIO_CONTROL').up('c').perform()
|
||||
}
|
||||
|
||||
export async function nativePaste() {
|
||||
// CMD+V
|
||||
await browser.action('key').down('WDIO_CONTROL').down('v').up('WDIO_CONTROL').up('v').perform()
|
||||
}
|
||||
|
||||
export async function deleteAllShapesOnPage() {
|
||||
await ui.main.menu(['edit', 'select-all'])
|
||||
await ui.main.menu(['edit', 'delete'])
|
||||
}
|
||||
|
||||
export async function sleep(ms: number) {
|
||||
await browser.execute((ms) => new Promise((resolve) => setTimeout(resolve, ms)), ms)
|
||||
}
|
||||
|
||||
export async function clearClipboard() {
|
||||
return await browser.execute(async () => {
|
||||
if (!(navigator && navigator.clipboard)) return
|
||||
if (navigator.clipboard.write) {
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
'text/plain': new Blob(['CLEAR'], { type: 'text/plain' }),
|
||||
}),
|
||||
])
|
||||
} else {
|
||||
await navigator.clipboard.writeText('')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function grantPermissions(permissions: 'clipboard-read'[]) {
|
||||
for (const permission of permissions) {
|
||||
await browser.setPermissions(
|
||||
{
|
||||
name: permission,
|
||||
},
|
||||
'granted'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Checks that the clipboard is no-longer "clear" see 'clearClipboard' above
|
||||
export async function waitForClipboardContents() {
|
||||
return await browser.waitUntil(async () => {
|
||||
return await browser.execute(async () => {
|
||||
const results = await navigator.clipboard.read()
|
||||
if (results.length < 0) {
|
||||
return true
|
||||
}
|
||||
return (
|
||||
!results[0].types.includes('text/plain') ||
|
||||
(await (await results[0].getType('text/plain')).text()) !== 'CLEAR'
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function clipboardContents() {
|
||||
return await browser.execute(async () => {
|
||||
const results = await navigator.clipboard.read()
|
||||
const contents = []
|
||||
|
||||
for (const result of results) {
|
||||
const item = {}
|
||||
for (const type of result.types) {
|
||||
item[type] = type.match(/^text/)
|
||||
? await (await result.getType(type)).text()
|
||||
: await (await result.getType(type)).arrayBuffer()
|
||||
}
|
||||
contents.push(item)
|
||||
}
|
||||
return contents
|
||||
})
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
import { Box2d } from '@tldraw/primitives'
|
||||
import fs from 'fs'
|
||||
import pixelmatch from 'pixelmatch'
|
||||
import pngjs from 'pngjs'
|
||||
import sharp from 'sharp'
|
||||
import { app } from './ui'
|
||||
|
||||
const PNG = pngjs.PNG
|
||||
|
||||
type takeRegionScreenshotOpts = {
|
||||
writeTo: {
|
||||
path: string
|
||||
prefix: string
|
||||
}
|
||||
}
|
||||
type takeRegionScreenshotResult = {
|
||||
browserBuffer: Buffer
|
||||
exportBuffer: Buffer
|
||||
fullWidth: any
|
||||
fullHeight: any
|
||||
}
|
||||
export async function takeRegionScreenshot(
|
||||
bbox: Box2d,
|
||||
opts?: takeRegionScreenshotOpts
|
||||
): Promise<takeRegionScreenshotResult> {
|
||||
const { data, dpr } = await app.shapesAsImgData()
|
||||
|
||||
const base64 = await browser.takeScreenshot()
|
||||
const bufferInput = Buffer.from(data.replace('data:image/png;base64,', ''), 'base64')
|
||||
|
||||
const { data: exportBuffer, info: origInfo } = await sharp(bufferInput)
|
||||
.png()
|
||||
.toBuffer({ resolveWithObject: true })
|
||||
|
||||
const fullWidth = Math.floor(origInfo.width)
|
||||
const fullHeight = Math.floor(origInfo.height)
|
||||
|
||||
const binary = Buffer.from(base64, 'base64')
|
||||
|
||||
const browserBuffer = await sharp(binary)
|
||||
.extract({
|
||||
left: Math.floor(bbox.x * dpr),
|
||||
top: Math.floor(bbox.y * dpr),
|
||||
width: Math.floor(bbox.w * dpr),
|
||||
height: Math.floor(bbox.h * dpr),
|
||||
})
|
||||
.resize({ width: fullWidth, height: fullHeight })
|
||||
.png()
|
||||
.toBuffer()
|
||||
|
||||
if (opts.writeTo) {
|
||||
const { path: writeToPath, prefix: writeToPrefix } = opts.writeTo
|
||||
await fs.promises.writeFile(
|
||||
`${__dirname}/../../screenshots/${writeToPrefix}-app.png`,
|
||||
exportBuffer
|
||||
)
|
||||
await fs.promises.writeFile(`${writeToPath}/${writeToPrefix}-svg.png`, exportBuffer)
|
||||
}
|
||||
|
||||
return {
|
||||
browserBuffer,
|
||||
exportBuffer,
|
||||
fullWidth,
|
||||
fullHeight,
|
||||
}
|
||||
}
|
||||
|
||||
type diffScreenshotOpts = {
|
||||
writeTo: {
|
||||
path: string
|
||||
prefix: string
|
||||
}
|
||||
}
|
||||
export async function diffScreenshot(
|
||||
screenshotRegion: takeRegionScreenshotResult,
|
||||
opts?: diffScreenshotOpts
|
||||
) {
|
||||
const { exportBuffer, browserBuffer, fullWidth, fullHeight } = screenshotRegion
|
||||
const diff = new PNG({ width: fullWidth, height: fullHeight })
|
||||
const img1 = PNG.sync.read(browserBuffer)
|
||||
const img2 = PNG.sync.read(exportBuffer)
|
||||
const pxielDiff = pixelmatch(img1.data, img2.data, diff.data, fullWidth, fullHeight, {
|
||||
threshold: 0.6,
|
||||
})
|
||||
|
||||
if (opts.writeTo) {
|
||||
const { path: writeToPath, prefix: writeToPrefix } = opts.writeTo
|
||||
|
||||
await fs.promises.writeFile(`${writeToPath}/${writeToPrefix}-diff.png`, PNG.sync.write(diff))
|
||||
}
|
||||
|
||||
return {
|
||||
pxielDiff,
|
||||
}
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
const mochaIt = global.it
|
||||
const describe = global.describe
|
||||
|
||||
const DEFAULT_ENV_HANDLER = () => ({
|
||||
shouldIgnore: false,
|
||||
skipMessage: '',
|
||||
})
|
||||
|
||||
type EnvOpts = {
|
||||
device?: 'mobile' | 'desktop'
|
||||
skipBrowsers?: ('firefox' | 'safari' | 'edge' | 'samsung' | 'chrome' | 'vscode')[]
|
||||
input?: ('mouse' | 'touch')[]
|
||||
os?: 'windows' | 'macos' | 'linux' | 'android' | 'ios' | 'ipados'
|
||||
ui?: 'mobile' | 'desktop'
|
||||
ignoreWhen?: () => boolean
|
||||
}
|
||||
|
||||
// Gets set by env(...) and read in it(...)
|
||||
let envMethod = DEFAULT_ENV_HANDLER
|
||||
|
||||
/** This is a mocha extension to allow us to run tests only for specific environments */
|
||||
const env = (opts: EnvOpts, handler: () => void) => {
|
||||
envMethod = () => {
|
||||
// @ts-ignore
|
||||
const { tldrawOptions } = global
|
||||
let skipMessage = '(ignored)'
|
||||
let shouldIgnore = false
|
||||
|
||||
if (opts.device && tldrawOptions.device !== opts.device) {
|
||||
shouldIgnore = true
|
||||
skipMessage = `(ignored only: ${opts.device})`
|
||||
}
|
||||
if (opts.skipBrowsers && opts.skipBrowsers.includes(tldrawOptions.browser)) {
|
||||
shouldIgnore = true
|
||||
skipMessage = `(ignored browser)`
|
||||
}
|
||||
if (opts.input && !tldrawOptions.input?.find((item) => opts.input.includes(item))) {
|
||||
shouldIgnore = true
|
||||
skipMessage = `(ignored only: ${opts.input.join(', ')})`
|
||||
}
|
||||
if (opts.ui && tldrawOptions.ui !== opts.ui) {
|
||||
shouldIgnore = true
|
||||
skipMessage = `(ignored only: ${opts.ui})`
|
||||
}
|
||||
if (opts.os && tldrawOptions.os !== opts.os) {
|
||||
shouldIgnore = true
|
||||
skipMessage = `(ignored only: ${opts.os})`
|
||||
}
|
||||
if (opts.ignoreWhen && opts.ignoreWhen()) {
|
||||
shouldIgnore = true
|
||||
}
|
||||
|
||||
return { skipMessage, shouldIgnore }
|
||||
}
|
||||
|
||||
handler()
|
||||
envMethod = DEFAULT_ENV_HANDLER
|
||||
}
|
||||
|
||||
/** Same usage as the mocha it(...) method */
|
||||
const it = (msg: string, handler: () => void) => {
|
||||
const { shouldIgnore, skipMessage } = envMethod()
|
||||
if (shouldIgnore) {
|
||||
mochaIt(msg + ' ' + skipMessage, () => {})
|
||||
} else {
|
||||
mochaIt(msg, handler)
|
||||
}
|
||||
}
|
||||
|
||||
/** Same usage as the mocha it.only(...) method */
|
||||
// eslint-disable-next-line no-only-tests/no-only-tests
|
||||
it.only = (msg: string, handler: () => void) => {
|
||||
const { shouldIgnore, skipMessage } = envMethod()
|
||||
if (shouldIgnore) {
|
||||
mochaIt.only(msg + ' ' + skipMessage, () => {})
|
||||
} else {
|
||||
mochaIt.only(msg, handler)
|
||||
}
|
||||
}
|
||||
|
||||
/** Same usage as the mocha it.skip(...) method */
|
||||
it.skip = (msg: string, handler: () => void) => {
|
||||
const { shouldIgnore, skipMessage } = envMethod()
|
||||
if (shouldIgnore) {
|
||||
mochaIt.skip(msg + ' ' + skipMessage, () => {})
|
||||
} else {
|
||||
mochaIt.skip(msg, handler)
|
||||
}
|
||||
}
|
||||
|
||||
/** Same usage as the mocha it.skip(...) method */
|
||||
it.todo = (msg: string, _handler?: () => void) => {
|
||||
mochaIt.skip('[TODO] ' + msg, () => {})
|
||||
}
|
||||
|
||||
it.ok = it
|
||||
|
||||
it.fails = (msg: string, handler: () => void | Promise<void>) => {
|
||||
const { shouldIgnore, skipMessage } = envMethod()
|
||||
if (shouldIgnore) {
|
||||
mochaIt('[FAILS] ' + msg + ' ' + skipMessage, () => {})
|
||||
} else {
|
||||
mochaIt('[FAILS] ' + msg, async () => {
|
||||
let failed = false
|
||||
try {
|
||||
await handler()
|
||||
} catch (err: any) {
|
||||
failed = true
|
||||
}
|
||||
|
||||
if (!failed) {
|
||||
throw new Error('This expected to fail, did you fix it?')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export { env, describe, it }
|
|
@ -1,391 +0,0 @@
|
|||
import { runtime, ui } from '../helpers'
|
||||
import { describe, it } from '../mocha-ext'
|
||||
|
||||
const createShapes = async (pos0 = [20, 20], pos1 = [70, 70], pos2 = [120, 120]) => {
|
||||
const size = 30
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(pos0[0], pos0[1], pos0[0] + size, pos0[1] + size)
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(pos1[0], pos1[1], pos1[0] + size, pos1[1] + size)
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(pos2[0], pos2[1], pos2[0] + size, pos2[1] + size)
|
||||
|
||||
return await runtime.getAllShapes()
|
||||
}
|
||||
|
||||
const MODES = [/*'context', */ 'action']
|
||||
|
||||
describe('arrange', () => {
|
||||
describe('align-left', () => {
|
||||
for (const mode of MODES) {
|
||||
it(`${mode} menu`, async () => {
|
||||
await ui.app.setup()
|
||||
const origShapes = await createShapes()
|
||||
|
||||
await ui.canvas.brush(10, 10, 170, 170)
|
||||
|
||||
if (mode === 'context') {
|
||||
const point = await ui.app.pointWithinActiveArea(11, 11)
|
||||
await ui.canvas.contextMenu(point.x, point.y, ['arrange', 'align-left'])
|
||||
} else if (mode === 'action') {
|
||||
await ui.main.actionMenu(['align-left'])
|
||||
}
|
||||
|
||||
const shapes = await runtime.getAllShapes()
|
||||
expect(origShapes[0].x).toBe(shapes[0].x)
|
||||
expect(origShapes[0].x).toBe(shapes[1].x)
|
||||
expect(origShapes[0].x).toBe(shapes[2].x)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('align-center-horizontal', () => {
|
||||
for (const mode of MODES) {
|
||||
it(`${mode} menu`, async () => {
|
||||
await ui.app.setup()
|
||||
const origShapes = await createShapes()
|
||||
|
||||
await ui.canvas.brush(10, 10, 170, 170)
|
||||
if (mode === 'context') {
|
||||
const point = await ui.app.pointWithinActiveArea(21, 21)
|
||||
await ui.canvas.contextMenu(point.x, point.y, ['arrange', 'align-center-horizontal'])
|
||||
} else if (mode === 'action') {
|
||||
await ui.main.actionMenu(['align-center-horizontal'])
|
||||
}
|
||||
const shapes = await runtime.getAllShapes()
|
||||
expect(origShapes[1].x).toBe(shapes[0].x)
|
||||
expect(origShapes[1].x).toBe(shapes[1].x)
|
||||
expect(origShapes[1].x).toBe(shapes[2].x)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('align-right', () => {
|
||||
for (const mode of MODES) {
|
||||
it(`${mode} menu`, async () => {
|
||||
await ui.app.setup()
|
||||
const origShapes = await createShapes()
|
||||
|
||||
await ui.canvas.brush(10, 10, 170, 170)
|
||||
if (mode === 'context') {
|
||||
const point = await ui.app.pointWithinActiveArea(21, 21)
|
||||
await ui.canvas.contextMenu(point.x, point.y, ['arrange', 'align-right'])
|
||||
} else if (mode === 'action') {
|
||||
await ui.main.actionMenu(['align-right'])
|
||||
}
|
||||
|
||||
const shapes = await runtime.getAllShapes()
|
||||
expect(origShapes[2].x).toBe(shapes[0].x)
|
||||
expect(origShapes[2].x).toBe(shapes[1].x)
|
||||
expect(origShapes[2].x).toBe(shapes[2].x)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('align-top', () => {
|
||||
for (const mode of MODES) {
|
||||
it(`${mode} menu`, async () => {
|
||||
await ui.app.setup()
|
||||
const origShapes = await createShapes()
|
||||
|
||||
await ui.canvas.brush(10, 10, 170, 170)
|
||||
if (mode === 'context') {
|
||||
const point = await ui.app.pointWithinActiveArea(21, 21)
|
||||
await ui.canvas.contextMenu(point.x, point.y, ['arrange', 'align-top'])
|
||||
} else if (mode === 'action') {
|
||||
await ui.main.actionMenu(['align-top'])
|
||||
}
|
||||
|
||||
const shapes = await runtime.getAllShapes()
|
||||
expect(origShapes[0].y).toBe(shapes[0].y)
|
||||
expect(origShapes[0].y).toBe(shapes[1].y)
|
||||
expect(origShapes[0].y).toBe(shapes[2].y)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('align-center-vertical', () => {
|
||||
for (const mode of MODES) {
|
||||
it(`${mode} menu`, async () => {
|
||||
await ui.app.setup()
|
||||
const origShapes = await createShapes()
|
||||
|
||||
await ui.canvas.brush(10, 10, 170, 170)
|
||||
if (mode === 'context') {
|
||||
const point = await ui.app.pointWithinActiveArea(21, 21)
|
||||
await ui.canvas.contextMenu(point.x, point.y, ['arrange', 'align-center-vertical'])
|
||||
} else if (mode === 'action') {
|
||||
await ui.main.actionMenu(['align-center-vertical'])
|
||||
}
|
||||
|
||||
const shapes = await runtime.getAllShapes()
|
||||
expect(origShapes[1].y).toBe(shapes[0].y)
|
||||
expect(origShapes[1].y).toBe(shapes[1].y)
|
||||
expect(origShapes[1].y).toBe(shapes[2].y)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('align-bottom', () => {
|
||||
for (const mode of MODES) {
|
||||
it(`${mode} menu`, async () => {
|
||||
await ui.app.setup()
|
||||
const origShapes = await createShapes()
|
||||
|
||||
await ui.canvas.brush(10, 10, 170, 170)
|
||||
await ui.app.pointWithinActiveArea(11, 11)
|
||||
if (mode === 'context') {
|
||||
const point = await ui.app.pointWithinActiveArea(11, 11)
|
||||
await ui.canvas.contextMenu(point.x, point.y, ['arrange', 'align-bottom'])
|
||||
} else if (mode === 'action') {
|
||||
await ui.main.actionMenu(['align-bottom'])
|
||||
}
|
||||
|
||||
const shapes = await runtime.getAllShapes()
|
||||
expect(origShapes[2].y).toBe(shapes[0].y)
|
||||
expect(origShapes[2].y).toBe(shapes[1].y)
|
||||
expect(origShapes[2].y).toBe(shapes[2].y)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('distribute-horizontal', () => {
|
||||
for (const mode of MODES) {
|
||||
const createHorzShapes = async () => {
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(20, 20, 50, 50)
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(90, 70, 120, 100)
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(120, 120, 150, 150)
|
||||
|
||||
return await runtime.getAllShapes()
|
||||
}
|
||||
|
||||
it(`${mode} menu`, async () => {
|
||||
await ui.app.setup()
|
||||
const origShapes = await createHorzShapes()
|
||||
|
||||
await ui.canvas.brush(10, 10, 170, 170)
|
||||
if (mode === 'context') {
|
||||
const point = await ui.app.pointWithinActiveArea(21, 21)
|
||||
await ui.canvas.contextMenu(point.x, point.y, ['arrange', 'distribute-horizontal'])
|
||||
} else if (mode === 'action') {
|
||||
await ui.main.actionMenu(['distribute-horizontal'])
|
||||
}
|
||||
|
||||
const shapes = await runtime.getAllShapes()
|
||||
expect(origShapes[0].x).toBe(shapes[0].x)
|
||||
expect(origShapes[0].x + 50).toBe(shapes[1].x)
|
||||
expect(origShapes[0].x + 100).toBe(shapes[2].x)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('distribute-vertical', () => {
|
||||
for (const mode of MODES) {
|
||||
const createVertShapes = async () => {
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(20, 20, 50, 50)
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(70, 90, 100, 120)
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(120, 120, 150, 150)
|
||||
|
||||
return await runtime.getAllShapes()
|
||||
}
|
||||
|
||||
it(`${mode} menu`, async () => {
|
||||
await ui.app.setup()
|
||||
const origShapes = await createVertShapes()
|
||||
|
||||
await ui.canvas.brush(10, 10, 170, 170)
|
||||
|
||||
if (mode === 'context') {
|
||||
const point = await ui.app.pointWithinActiveArea(21, 21)
|
||||
await ui.canvas.contextMenu(point.x, point.y, ['arrange', 'distribute-vertical'])
|
||||
} else if (mode === 'action') {
|
||||
await ui.main.actionMenu(['distribute-vertical'])
|
||||
}
|
||||
|
||||
const shapes = await runtime.getAllShapes()
|
||||
expect(origShapes[0].x).toBe(shapes[0].x)
|
||||
expect(origShapes[0].x + 50).toBe(shapes[1].x)
|
||||
expect(origShapes[0].x + 100).toBe(shapes[2].x)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('stretch-horizontal', () => {
|
||||
for (const mode of MODES) {
|
||||
it(`${mode} menu`, async () => {
|
||||
await ui.app.setup()
|
||||
await createShapes()
|
||||
|
||||
await ui.canvas.brush(10, 10, 170, 170)
|
||||
if (mode === 'context') {
|
||||
const point = await ui.app.pointWithinActiveArea(21, 21)
|
||||
await ui.canvas.contextMenu(point.x, point.y, ['arrange', 'stretch-horizontal'])
|
||||
} else if (mode === 'action') {
|
||||
await ui.main.actionMenu(['stretch-horizontal'])
|
||||
}
|
||||
|
||||
const shapes = await runtime.getAllShapes()
|
||||
expect(shapes[0].props).toHaveProperty('w', 130)
|
||||
expect(shapes[1].props).toHaveProperty('w', 130)
|
||||
expect(shapes[2].props).toHaveProperty('w', 130)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('stretch-vertical', () => {
|
||||
for (const mode of MODES) {
|
||||
it(`${mode} menu`, async () => {
|
||||
await ui.app.setup()
|
||||
await createShapes()
|
||||
|
||||
await ui.canvas.brush(10, 10, 170, 170)
|
||||
if (mode === 'context') {
|
||||
const point = await ui.app.pointWithinActiveArea(21, 21)
|
||||
await ui.canvas.contextMenu(point.x, point.y, ['arrange', 'stretch-vertical'])
|
||||
} else if (mode === 'action') {
|
||||
await ui.main.actionMenu(['stretch-vertical'])
|
||||
}
|
||||
|
||||
const shapes = await runtime.getAllShapes()
|
||||
expect(shapes[0].props).toHaveProperty('h', 130)
|
||||
expect(shapes[1].props).toHaveProperty('h', 130)
|
||||
expect(shapes[2].props).toHaveProperty('h', 130)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// describe('flip-horizontal', () => {
|
||||
// for (const mode of ['context']) {
|
||||
// it(`${mode} menu`, async () => {
|
||||
// await ui.app.setup()
|
||||
// const origShapes = await createShapes()
|
||||
|
||||
// // TODO: Move back to front
|
||||
// await ui.canvas.brush(0, 0, 150, 150)
|
||||
// const point = await ui.app.pointWithinActiveArea(11, 11)
|
||||
// if (mode === 'context') {
|
||||
// const point = await ui.app.pointWithinActiveArea(11, 11)
|
||||
// await ui.canvas.contextMenu(point.x, point.y, ['arrange', 'flip-horizontal'])
|
||||
// } else if (mode === 'action') {
|
||||
// await ui.main.actionMenu(['flip-horizontal'])
|
||||
// }
|
||||
|
||||
// const shapes = await runtime.getAllShapes()
|
||||
// expect(shapes[0].x).toBe(origShapes[2].x)
|
||||
// expect(shapes[1].x).toBe(origShapes[1].x)
|
||||
// expect(shapes[2].x).toBe(origShapes[0].x)
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
|
||||
// describe('flip-vertical', () => {
|
||||
// for (const mode of ['context']) {
|
||||
// it(`${mode} menu`, async () => {
|
||||
// await ui.app.setup()
|
||||
// const origShapes = await createShapes()
|
||||
|
||||
// // TODO: Move back to front
|
||||
// await ui.canvas.brush(0, 0, 150, 150)
|
||||
// const point = await ui.app.pointWithinActiveArea(11, 11)
|
||||
// if (mode === 'context') {
|
||||
// const point = await ui.app.pointWithinActiveArea(11, 11)
|
||||
// await ui.canvas.contextMenu(point.x, point.y, ['arrange', 'flip-vertical'])
|
||||
// } else if (mode === 'action') {
|
||||
// await ui.main.actionMenu(['flip-vertical'])
|
||||
// }
|
||||
|
||||
// const shapes = await runtime.getAllShapes()
|
||||
// expect(shapes[0].y).toBe(origShapes[2].y)
|
||||
// expect(shapes[1].y).toBe(origShapes[1].y)
|
||||
// expect(shapes[2].y).toBe(origShapes[0].y)
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
|
||||
// describe('pack', () => {
|
||||
// for (const mode of ['context']) {
|
||||
// it(`${mode} menu`, async () => {
|
||||
// await ui.app.setup()
|
||||
// const origShapes = await createShapes()
|
||||
|
||||
// // TODO: Move back to front
|
||||
// await ui.canvas.brush(0, 0, 150, 150)
|
||||
// const point = await ui.app.pointWithinActiveArea(11, 11)
|
||||
// if (mode === 'context') {
|
||||
// const point = await ui.app.pointWithinActiveArea(11, 11)
|
||||
// await ui.canvas.contextMenu(point.x, point.y, ['arrange', 'pack'])
|
||||
// } else if (mode === 'action') {
|
||||
// await ui.main.actionMenu(['pack'])
|
||||
// }
|
||||
|
||||
// const shapes = await runtime.getAllShapes()
|
||||
// expect(shapes.length).toBe(3)
|
||||
// expect(shapes[0].x).toBe(339)
|
||||
// expect(shapes[0].y).toBe(122)
|
||||
// expect(shapes[1].x).toBe(385)
|
||||
// expect(shapes[1].y).toBe(122)
|
||||
// expect(shapes[2].x).toBe(431)
|
||||
// expect(shapes[2].y).toBe(122)
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
|
||||
// describe('stack-vertical', () => {
|
||||
// for (const mode of MODES) {
|
||||
// it(`${mode} menu`, async () => {
|
||||
// await ui.app.setup()
|
||||
// const origShapes = await createShapes([10, 10], [60, 16], [110, 24])
|
||||
|
||||
// await ui.canvas.brush(0, 0, 150, 150)
|
||||
// if (mode === 'context') {
|
||||
// const point = await ui.app.pointWithinActiveArea(11, 11)
|
||||
// await ui.canvas.contextMenu(point.x, point.y, ['arrange', 'stack-vertical'])
|
||||
// } else if (mode === 'action') {
|
||||
// await ui.main.actionMenu(['stack-vertical'])
|
||||
// }
|
||||
|
||||
// const shapes = await runtime.getAllShapes()
|
||||
// console.log(shapes)
|
||||
// expect(shapes[0].y).toBe(72)
|
||||
// expect(shapes[1].y).toBe(102)
|
||||
// expect(shapes[2].y).toBe(132)
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
|
||||
// describe('stack-horizontal', () => {
|
||||
// for (const mode of MODES) {
|
||||
// it(`${mode} menu`, async () => {
|
||||
// await ui.app.setup()
|
||||
// const origShapes = await createShapes([10, 10], [16, 60], [24, 110])
|
||||
|
||||
// await ui.canvas.brush(0, 0, 150, 150)
|
||||
// if (mode === 'context') {
|
||||
// const point = await ui.app.pointWithinActiveArea(11, 11)
|
||||
// await ui.canvas.contextMenu(point.x, point.y, ['arrange', 'stack-horizontal'])
|
||||
// } else if (mode === 'action') {
|
||||
// await ui.main.actionMenu(['stack-horizontal'])
|
||||
// }
|
||||
|
||||
// const shapes = await runtime.getAllShapes()
|
||||
// console.log(shapes)
|
||||
// expect(shapes[0].x).toBe(335)
|
||||
// expect(shapes[1].x).toBe(365)
|
||||
// expect(shapes[2].x).toBe(395)
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
})
|
|
@ -1,295 +0,0 @@
|
|||
import { MOVE_DEFAULTS, runtime, ui } from '../helpers'
|
||||
import { describe, env, it } from '../mocha-ext'
|
||||
|
||||
describe('camera', () => {
|
||||
env(
|
||||
{
|
||||
skipBrowsers: ['firefox'],
|
||||
},
|
||||
() => {
|
||||
describe('panning', () => {
|
||||
it('hand tool', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
const c1 = await runtime.getCamera()
|
||||
await ui.tools.click('hand')
|
||||
await browser.actions([
|
||||
browser
|
||||
.action('pointer')
|
||||
.move({ x: 200, y: 200, ...MOVE_DEFAULTS })
|
||||
.down('left')
|
||||
.move({ x: 300, y: 300, ...MOVE_DEFAULTS })
|
||||
.up(),
|
||||
])
|
||||
|
||||
const c2 = await runtime.getCamera()
|
||||
expect(c1).not.toMatchObject(c2 as any)
|
||||
expect(c1.z).toBe(c2.z)
|
||||
})
|
||||
|
||||
env(
|
||||
{
|
||||
input: ['mouse'],
|
||||
},
|
||||
() => {
|
||||
// Failed in <https://automate.browserstack.com/dashboard/v2/public-build/Nyt1KzhUQ1FXNVRrcWFsaE4vSUREQzN1ZFNMaW5YUGpaL0UyU2RvUFd1WFBsK3lBSXRRZHUwSHlyaWk1a0dqelJlbUsvL0xUM2xadnhFY28xODE4aUE9PS0tUDNQbHorbWFPeTVQNGJzWHVXNUp4Zz09--581e9085c67c6e1508b22e4757f4936e70bb68d1>
|
||||
it('wheel', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
const c1 = await runtime.getCamera()
|
||||
await browser
|
||||
.action('wheel')
|
||||
.scroll({ x: 200, y: 200, deltaX: 100, deltaY: 100, duration: 100 })
|
||||
.perform()
|
||||
|
||||
const c2 = await runtime.getCamera()
|
||||
expect(c1).not.toMatchObject(c2 as any)
|
||||
expect(c1.z).toBe(c2.z)
|
||||
})
|
||||
|
||||
// REMOTE:OK
|
||||
it('spacebar', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
const c1 = await runtime.getCamera()
|
||||
await browser.actions([
|
||||
browser.action('key').down(' '),
|
||||
browser
|
||||
.action('pointer')
|
||||
.move({ x: 200, y: 200, ...MOVE_DEFAULTS })
|
||||
.down('left')
|
||||
.move({ x: 300, y: 300, ...MOVE_DEFAULTS })
|
||||
.up(),
|
||||
])
|
||||
|
||||
const c2 = await runtime.getCamera()
|
||||
expect(c1).not.toMatchObject(c2 as any)
|
||||
expect(c1.z).toBe(c2.z)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('zooming', () => {
|
||||
env(
|
||||
{
|
||||
ui: 'desktop',
|
||||
input: ['mouse'],
|
||||
skipBrowsers: ['firefox'],
|
||||
},
|
||||
() => {
|
||||
it('wheel', async () => {
|
||||
await ui.app.setup()
|
||||
const c1 = await runtime.getCamera()
|
||||
await browser.actions([
|
||||
// For some reason the import isn't working...
|
||||
// From <https://github.com/webdriverio/webdriverio/blob/3620e90e47b6d3e62832f5de24f43cee6b31e972/packages/webdriverio/src/constants.ts#L360>
|
||||
browser.action('key').down('WDIO_CONTROL'),
|
||||
browser
|
||||
.action('wheel')
|
||||
.scroll({ x: 200, y: 200, deltaX: 100, deltaY: 100, duration: 100 }),
|
||||
])
|
||||
const c2 = await runtime.getCamera()
|
||||
expect(c1).not.toMatchObject(c2 as any)
|
||||
expect(c1.z).not.toBe(c2.z)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
env(
|
||||
{
|
||||
input: ['touch'],
|
||||
},
|
||||
() => {
|
||||
it('pinch-in', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
const c1 = await runtime.getCamera()
|
||||
await browser.actions([
|
||||
browser
|
||||
.action('pointer', { parameters: { pointerType: 'touch' } })
|
||||
.move({ x: 200, y: 200, ...MOVE_DEFAULTS })
|
||||
.down('left')
|
||||
.move({ x: 300, y: 300, ...MOVE_DEFAULTS })
|
||||
.up(),
|
||||
browser
|
||||
.action('pointer', { parameters: { pointerType: 'touch' } })
|
||||
.move({ x: 200, y: 200, ...MOVE_DEFAULTS })
|
||||
.down('left')
|
||||
.move({ x: 100, y: 100, ...MOVE_DEFAULTS })
|
||||
.up(),
|
||||
])
|
||||
|
||||
const c2 = await runtime.getCamera()
|
||||
expect(c1).not.toMatchObject(c2 as any)
|
||||
expect(c1.z).toBeLessThan(c2.z)
|
||||
})
|
||||
|
||||
it('pinch-out', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
const c1 = await runtime.getCamera()
|
||||
await browser.actions([
|
||||
browser
|
||||
.action('pointer', { parameters: { pointerType: 'touch' } })
|
||||
.move({ x: 300, y: 300, ...MOVE_DEFAULTS })
|
||||
.down('left')
|
||||
.move({ x: 220, y: 220, ...MOVE_DEFAULTS })
|
||||
.up(),
|
||||
browser
|
||||
.action('pointer', { parameters: { pointerType: 'touch' } })
|
||||
.move({ x: 100, y: 100, ...MOVE_DEFAULTS })
|
||||
.down('left')
|
||||
.move({ x: 180, y: 180, ...MOVE_DEFAULTS })
|
||||
.up(),
|
||||
])
|
||||
|
||||
const c2 = await runtime.getCamera()
|
||||
expect(c1).not.toMatchObject(c2 as any)
|
||||
expect(c1.z).toBeGreaterThan(c2.z)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
env(
|
||||
{
|
||||
ui: 'desktop',
|
||||
},
|
||||
() => {
|
||||
describe('minimap', () => {
|
||||
describe('buttons', () => {
|
||||
// REMOTE:OK
|
||||
it('zoom in', async () => {
|
||||
await ui.app.setup()
|
||||
const c1 = await runtime.getCamera()
|
||||
await ui.minimap.zoomIn()
|
||||
|
||||
await browser.waitUntil(async () => {
|
||||
const text = await (await ui.minimap.menuButton()).getText()
|
||||
return text === '200%'
|
||||
})
|
||||
|
||||
const c2 = await runtime.getCamera()
|
||||
expect(c1).not.toMatchObject(c2 as any)
|
||||
expect(c2.z).toBe(2)
|
||||
})
|
||||
|
||||
it('zoom out', async () => {
|
||||
await ui.app.setup()
|
||||
const c1 = await runtime.getCamera()
|
||||
await ui.minimap.zoomOut()
|
||||
|
||||
await browser.waitUntil(async () => {
|
||||
const text = await (await ui.minimap.menuButton()).getText()
|
||||
return text === '50%'
|
||||
})
|
||||
|
||||
const c2 = await runtime.getCamera()
|
||||
expect(c1).not.toMatchObject(c2 as any)
|
||||
expect(c2.z).toBeCloseTo(0.5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('menu', () => {
|
||||
// REMOTE:OK
|
||||
it('zoom in', async () => {
|
||||
await ui.app.setup()
|
||||
const c1 = await runtime.getCamera()
|
||||
await ui.minimap.menu(['zoom-in'])
|
||||
|
||||
await browser.waitUntil(async () => {
|
||||
const text = await (await ui.minimap.menuButton()).getText()
|
||||
return text === '200%'
|
||||
})
|
||||
|
||||
const c2 = await runtime.getCamera()
|
||||
expect(c1).not.toMatchObject(c2 as any)
|
||||
expect(c2.z).toBeCloseTo(2)
|
||||
})
|
||||
|
||||
// REMOTE:OK
|
||||
it('zoom out', async () => {
|
||||
await ui.app.setup()
|
||||
const c1 = await runtime.getCamera()
|
||||
await ui.minimap.menu(['zoom-out'])
|
||||
|
||||
await browser.waitUntil(async () => {
|
||||
const text = await (await ui.minimap.menuButton()).getText()
|
||||
return text === '50%'
|
||||
})
|
||||
|
||||
const c2 = await runtime.getCamera()
|
||||
expect(c1).not.toMatchObject(c2 as any)
|
||||
expect(c2.z).toBeCloseTo(0.5)
|
||||
})
|
||||
|
||||
// REMOTE:OK
|
||||
it('zoom 100%', async () => {
|
||||
await ui.app.setup()
|
||||
await browser.execute(() => {
|
||||
window.app.setCamera(0, 0, 0.5)
|
||||
})
|
||||
const c1 = await runtime.getCamera()
|
||||
expect(c1.z).toBeCloseTo(0.5)
|
||||
|
||||
await ui.minimap.menu(['zoom-to-100'])
|
||||
await browser.waitUntil(async () => {
|
||||
const text = await (await ui.minimap.menuButton()).getText()
|
||||
return text === '100%'
|
||||
})
|
||||
|
||||
const c2 = await runtime.getCamera()
|
||||
expect(c2.z).toBeCloseTo(1)
|
||||
})
|
||||
|
||||
it('zoom to fit', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(10, 10, 60, 60)
|
||||
|
||||
await browser.execute(() => {
|
||||
window.app.setCamera(0, 0, 0.5)
|
||||
})
|
||||
const c1 = await runtime.getCamera()
|
||||
expect(c1.z).toBeCloseTo(0.5)
|
||||
|
||||
await ui.minimap.menu(['zoom-to-fit'])
|
||||
await browser.waitUntil(async () => {
|
||||
const text = await (await ui.minimap.menuButton()).getText()
|
||||
return text === '800%'
|
||||
})
|
||||
|
||||
const c2 = await runtime.getCamera()
|
||||
expect(c2.z).toBeCloseTo(8)
|
||||
})
|
||||
|
||||
it('zoom to selection', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(10, 10, 60, 60)
|
||||
|
||||
await browser.execute(() => {
|
||||
window.app.setCamera(0, 0, 0.5)
|
||||
})
|
||||
const c1 = await runtime.getCamera()
|
||||
expect(c1.z).toBeCloseTo(0.5)
|
||||
|
||||
await ui.minimap.menu(['zoom-to-selection'])
|
||||
await browser.waitUntil(async () => {
|
||||
const text = await (await ui.minimap.menuButton()).getText()
|
||||
return text === '100%'
|
||||
})
|
||||
|
||||
const c2 = await runtime.getCamera()
|
||||
expect(c2.z).toBeCloseTo(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
|
@ -1,17 +0,0 @@
|
|||
export const SHAPES = [
|
||||
{ type: 'geo', tool: 'rectangle' },
|
||||
// { type: 'geo', tool: 'ellipse' },
|
||||
// { type: 'geo', tool: 'triangle' },
|
||||
// { type: 'geo', tool: 'diamond' },
|
||||
// { type: 'geo', tool: 'pentagon' },
|
||||
// { type: 'geo', tool: 'hexagon' },
|
||||
// { type: 'geo', tool: 'octagon' },
|
||||
// { type: 'geo', tool: 'star' },
|
||||
// { type: 'geo', tool: 'rhombus' },
|
||||
// { type: 'geo', tool: 'oval' },
|
||||
// { type: 'geo', tool: 'trapezoid' },
|
||||
// { type: 'geo', tool: 'arrow-right' },
|
||||
// { type: 'geo', tool: 'arrow-left' },
|
||||
// { type: 'geo', tool: 'arrow-up' },
|
||||
// { type: 'geo', tool: 'arrow-down' },
|
||||
]
|
|
@ -1,172 +0,0 @@
|
|||
import { runtime, ui, util } from '../helpers'
|
||||
import { describe, env, it } from '../mocha-ext'
|
||||
|
||||
describe('export', () => {
|
||||
before(async () => {
|
||||
await ui.app.open()
|
||||
})
|
||||
|
||||
const createShape = async () => {
|
||||
await ui.tools.click('text')
|
||||
await ui.canvas.brush(70, 200, 250, 200)
|
||||
await browser.keys('testing')
|
||||
}
|
||||
|
||||
describe.skip('export-as', () => {
|
||||
for (const mode of ['main' /*, 'context'*/]) {
|
||||
describe(`${mode} menu`, () => {
|
||||
env(
|
||||
// It turns out we can't grab the file on mobile devices... urgh!
|
||||
{
|
||||
device: 'desktop',
|
||||
skipBrowsers: ['firefox'],
|
||||
},
|
||||
() => {
|
||||
const fileNameFromShape = async (shape) => {
|
||||
return await browser.execute((shapeId) => {
|
||||
return window.app.getShapeById(shapeId)?.id.replace(/:/, '_')
|
||||
}, shape.id)
|
||||
}
|
||||
|
||||
it('svg', async () => {
|
||||
await util.grantPermissions(['clipboard-read'])
|
||||
await util.clearClipboard()
|
||||
await ui.app.setup()
|
||||
await createShape()
|
||||
|
||||
if (mode === 'context') {
|
||||
await ui.canvas.contextMenu(100, 120, ['export-as', 'export-as-svg'])
|
||||
} else if (mode === 'main') {
|
||||
await ui.main.menu(['edit', 'export-as', 'export-as-svg'])
|
||||
}
|
||||
|
||||
// FIXME: This shouldn't be a timer... but what to do???
|
||||
await browser.execute(() => new Promise((resolve) => setTimeout(resolve, 3000)))
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
const fileName = await fileNameFromShape(allShapes[0])
|
||||
const file = await util.getDownloadFile(fileName + '.svg')
|
||||
|
||||
// TODO: Also check the buffer is correct here...
|
||||
expect(file).toExist()
|
||||
})
|
||||
|
||||
it('png', async () => {
|
||||
await util.grantPermissions(['clipboard-read'])
|
||||
await util.clearClipboard()
|
||||
await ui.app.setup()
|
||||
await createShape()
|
||||
|
||||
if (mode === 'context') {
|
||||
await ui.canvas.contextMenu(100, 120, ['export-as', 'export-as-png'])
|
||||
} else if (mode === 'main') {
|
||||
await ui.main.menu(['edit', 'export-as', 'export-as-png'])
|
||||
}
|
||||
|
||||
// FIXME: This shouldn't be a timer... but what to do???
|
||||
await browser.execute(() => new Promise((resolve) => setTimeout(resolve, 3000)))
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
const fileName = await fileNameFromShape(allShapes[0])
|
||||
const file = await util.getDownloadFile(fileName + '.png')
|
||||
|
||||
// TODO: Also check the buffer is correct here...
|
||||
expect(file).toExist()
|
||||
})
|
||||
|
||||
it('json', async () => {
|
||||
await util.grantPermissions(['clipboard-read'])
|
||||
await util.clearClipboard()
|
||||
await ui.app.setup()
|
||||
await createShape()
|
||||
|
||||
if (mode === 'context') {
|
||||
await ui.canvas.contextMenu(100, 120, ['export-as', 'export-as-json'])
|
||||
} else if (mode === 'main') {
|
||||
await ui.main.menu(['edit', 'export-as', 'export-as-json'])
|
||||
}
|
||||
|
||||
// FIXME: This shouldn't be a timer... but what to do???
|
||||
await browser.execute(() => new Promise((resolve) => setTimeout(resolve, 3000)))
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
const fileName = await fileNameFromShape(allShapes[0])
|
||||
const file = await util.getDownloadFile(fileName + '.json')
|
||||
|
||||
// TODO: Also check the buffer is correct here...
|
||||
expect(file).toExist()
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('copy-as', () => {
|
||||
for (const mode of ['main' /*, 'context'*/]) {
|
||||
describe(`${mode} menu`, () => {
|
||||
env(
|
||||
{
|
||||
// NOTE: Will be abled once mobile browsers support the '/permissions' API endpoint.
|
||||
device: 'desktop',
|
||||
// FIXME
|
||||
skipBrowsers: ['firefox', 'vscode'],
|
||||
},
|
||||
() => {
|
||||
it('svg', async () => {
|
||||
await util.grantPermissions(['clipboard-read'])
|
||||
await ui.app.setup()
|
||||
await util.clearClipboard()
|
||||
await createShape()
|
||||
|
||||
if (mode === 'context') {
|
||||
await ui.canvas.contextMenu(100, 120, ['copy-as', 'copy-as-svg'])
|
||||
} else if (mode === 'main') {
|
||||
await ui.main.menu(['edit', 'copy-as', 'copy-as-svg'])
|
||||
}
|
||||
|
||||
await util.waitForClipboardContents()
|
||||
const clipboardContents = await util.clipboardContents()
|
||||
expect(clipboardContents.length).toEqual(1)
|
||||
expect(clipboardContents[0]['text/plain']).toMatch(/<svg/)
|
||||
})
|
||||
|
||||
it('png', async () => {
|
||||
await util.grantPermissions(['clipboard-read'])
|
||||
await ui.app.setup()
|
||||
await util.clearClipboard()
|
||||
await createShape()
|
||||
|
||||
if (mode === 'context') {
|
||||
await ui.canvas.contextMenu(100, 120, ['copy-as', 'copy-as-png'])
|
||||
} else if (mode === 'main') {
|
||||
await ui.main.menu(['edit', 'copy-as', 'copy-as-png'])
|
||||
}
|
||||
|
||||
await util.waitForClipboardContents()
|
||||
const clipboardContents = await util.clipboardContents()
|
||||
expect(clipboardContents.length).toEqual(1)
|
||||
expect(clipboardContents[0]['image/png']).toBeDefined()
|
||||
})
|
||||
|
||||
it('json', async () => {
|
||||
await util.grantPermissions(['clipboard-read'])
|
||||
await ui.app.setup()
|
||||
await util.clearClipboard()
|
||||
await createShape()
|
||||
|
||||
if (mode === 'context') {
|
||||
await ui.canvas.contextMenu(100, 120, ['copy-as', 'copy-as-json'])
|
||||
} else if (mode === 'main') {
|
||||
await ui.main.menu(['edit', 'copy-as', 'copy-as-json'])
|
||||
}
|
||||
|
||||
await util.waitForClipboardContents()
|
||||
const clipboardContents = await util.clipboardContents()
|
||||
expect(clipboardContents.length).toEqual(1)
|
||||
expect(clipboardContents[0]['text/plain']).toBeDefined()
|
||||
expect(clipboardContents[0]['text/plain']).toMatch(/^{/)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
|
@ -1,6 +0,0 @@
|
|||
import { describe } from '../mocha-ext'
|
||||
|
||||
describe('grouping', () => {
|
||||
describe('group', () => {})
|
||||
describe('ungroup', () => {})
|
||||
})
|
|
@ -1,27 +0,0 @@
|
|||
import { ui } from '../helpers/index'
|
||||
import './arrange'
|
||||
import './camera'
|
||||
import './export'
|
||||
import './grouping'
|
||||
import './pages'
|
||||
import './reorder'
|
||||
import './screenshots'
|
||||
import './shortcuts'
|
||||
import './smoke'
|
||||
import './styling'
|
||||
import './text'
|
||||
|
||||
before(async () => {
|
||||
await browser.waitUntil(
|
||||
async () => {
|
||||
try {
|
||||
await ui.app.open()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
{ timeout: 30 * 1000 }
|
||||
)
|
||||
})
|
|
@ -1,15 +0,0 @@
|
|||
import { describe } from '../mocha-ext'
|
||||
|
||||
describe('pages', () => {
|
||||
describe('switch-page', () => {})
|
||||
describe('create', () => {})
|
||||
describe('edit-pages', () => {
|
||||
describe('go-to-page', () => {})
|
||||
describe('duplicate', () => {})
|
||||
describe('move', () => {})
|
||||
describe('delete', () => {})
|
||||
describe('close', () => {})
|
||||
describe('rename-page', () => {})
|
||||
describe('create-page', () => {})
|
||||
})
|
||||
})
|
|
@ -1,114 +0,0 @@
|
|||
import { sortByIndex } from '@tldraw/indices'
|
||||
import { runtime, ui } from '../helpers'
|
||||
import { app } from '../helpers/ui'
|
||||
import { describe, it } from '../mocha-ext'
|
||||
|
||||
describe('reorder', () => {
|
||||
const createShapes = async () => {
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(10, 10, 60, 60)
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(30, 30, 80, 80)
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(50, 50, 100, 100)
|
||||
|
||||
return (await runtime.getAllShapes()).sort(sortByIndex).map((s) => s.id)
|
||||
}
|
||||
|
||||
const MODES = [/*'context', */ 'action']
|
||||
|
||||
describe('bring-to-front', () => {
|
||||
for (const mode of MODES) {
|
||||
it(`${mode} menu`, async () => {
|
||||
await ui.app.setup()
|
||||
const ids = await createShapes()
|
||||
|
||||
// TODO: Move back to front
|
||||
await ui.canvas.click(11, 11)
|
||||
const point = await app.pointWithinActiveArea(11, 11)
|
||||
|
||||
if (mode === 'action') {
|
||||
await ui.main.actionMenu(['bring-to-front'])
|
||||
} else if (mode === 'context') {
|
||||
await ui.canvas.contextMenu(point.x, point.y, ['reorder', 'bring-to-front'])
|
||||
}
|
||||
|
||||
const shapes = (await runtime.getAllShapes()).sort(sortByIndex)
|
||||
expect(shapes[0].id).toBe(ids[1])
|
||||
expect(shapes[1].id).toBe(ids[2])
|
||||
expect(shapes[2].id).toBe(ids[0])
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('bring-forward', () => {
|
||||
for (const mode of MODES) {
|
||||
it(`${mode} menu`, async () => {
|
||||
await ui.app.setup()
|
||||
const ids = await createShapes()
|
||||
|
||||
// TODO: Move back to front
|
||||
await ui.canvas.click(11, 11)
|
||||
const point = await app.pointWithinActiveArea(11, 11)
|
||||
if (mode === 'action') {
|
||||
await ui.main.actionMenu(['bring-forward'])
|
||||
} else if (mode === 'context') {
|
||||
await ui.canvas.contextMenu(point.x, point.y, ['reorder', 'bring-forward'])
|
||||
}
|
||||
|
||||
const shapes = (await runtime.getAllShapes()).sort(sortByIndex)
|
||||
expect(shapes[0].id).toBe(ids[1])
|
||||
expect(shapes[1].id).toBe(ids[0])
|
||||
expect(shapes[2].id).toBe(ids[2])
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('send-backward', () => {
|
||||
for (const mode of MODES) {
|
||||
it(`${mode} menu`, async () => {
|
||||
await ui.app.setup()
|
||||
const ids = await createShapes()
|
||||
|
||||
// TODO: Move back to front
|
||||
await ui.canvas.click(51, 51)
|
||||
const point = await app.pointWithinActiveArea(51, 51)
|
||||
if (mode === 'action') {
|
||||
await ui.main.actionMenu(['send-backward'])
|
||||
} else if (mode === 'context') {
|
||||
await ui.canvas.contextMenu(point.x, point.y, ['reorder', 'send-backward'])
|
||||
}
|
||||
|
||||
const shapes = (await runtime.getAllShapes()).sort(sortByIndex)
|
||||
expect(shapes[0].id).toBe(ids[0])
|
||||
expect(shapes[1].id).toBe(ids[2])
|
||||
expect(shapes[2].id).toBe(ids[1])
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('send-to-back', () => {
|
||||
for (const mode of MODES) {
|
||||
it(`${mode} menu`, async () => {
|
||||
await ui.app.setup()
|
||||
const ids = await createShapes()
|
||||
|
||||
// TODO: Move back to front
|
||||
await ui.canvas.click(51, 51)
|
||||
const point = await app.pointWithinActiveArea(51, 51)
|
||||
if (mode === 'action') {
|
||||
await ui.main.actionMenu(['send-to-back'])
|
||||
} else if (mode === 'context') {
|
||||
await ui.canvas.contextMenu(point.x, point.y, ['reorder', 'send-to-back'])
|
||||
}
|
||||
|
||||
const shapes = (await runtime.getAllShapes()).sort(sortByIndex)
|
||||
expect(shapes[0].id).toBe(ids[2])
|
||||
expect(shapes[1].id).toBe(ids[0])
|
||||
expect(shapes[2].id).toBe(ids[1])
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
|
@ -1 +0,0 @@
|
|||
export {}
|
|
@ -1,154 +0,0 @@
|
|||
import { runtime, ui } from '../helpers'
|
||||
import { describe, env, it } from '../mocha-ext'
|
||||
|
||||
describe('basic keyboard shortcuts', () => {
|
||||
env({ device: 'desktop' }, () => {
|
||||
// If this one works, the others will work as well.
|
||||
it('draw — D', async () => {
|
||||
await ui.app.setup()
|
||||
await browser.keys(['d'])
|
||||
await browser.waitUntil(async () => {
|
||||
return await runtime.isIn('draw.idle')
|
||||
})
|
||||
})
|
||||
|
||||
// Tools
|
||||
it.todo('select — V', async () => {
|
||||
await ui.app.setup()
|
||||
await browser.keys(['v'])
|
||||
})
|
||||
|
||||
it('draw — D', async () => {
|
||||
await ui.app.setup()
|
||||
await browser.keys(['d'])
|
||||
await browser.waitUntil(async () => {
|
||||
return await runtime.isIn('draw.idle')
|
||||
})
|
||||
})
|
||||
|
||||
it('eraser — E', async () => {
|
||||
await ui.app.setup()
|
||||
await browser.keys(['e'])
|
||||
await browser.waitUntil(async () => {
|
||||
return await runtime.isIn('eraser.idle')
|
||||
})
|
||||
})
|
||||
|
||||
it('hand — H', async () => {
|
||||
await ui.app.setup()
|
||||
await browser.keys(['h'])
|
||||
await browser.waitUntil(async () => {
|
||||
return await runtime.isIn('hand.idle')
|
||||
})
|
||||
})
|
||||
|
||||
it('rectangle — R', async () => {
|
||||
await ui.app.setup()
|
||||
await browser.keys(['r'])
|
||||
await browser.waitUntil(async () => {
|
||||
return (
|
||||
(await runtime.isIn('geo.idle')) &&
|
||||
(await runtime.propsForNextShape()).geo === 'rectangle'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('ellipse — O', async () => {
|
||||
await ui.app.setup()
|
||||
await browser.keys(['o'])
|
||||
await browser.waitUntil(async () => {
|
||||
return (
|
||||
(await runtime.isIn('geo.idle')) && (await runtime.propsForNextShape()).geo === 'ellipse'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it.fails('diamond — P', async () => {
|
||||
await ui.app.setup()
|
||||
await browser.keys(['p'])
|
||||
await browser.waitUntil(async () => {
|
||||
return (
|
||||
(await runtime.isIn('geo.idle')) && (await runtime.propsForNextShape()).geo === 'diamond'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('arrow — A', async () => {
|
||||
await ui.app.setup()
|
||||
await browser.keys(['a'])
|
||||
await browser.waitUntil(async () => {
|
||||
return await runtime.isIn('arrow.idle')
|
||||
})
|
||||
})
|
||||
|
||||
it('line — L', async () => {
|
||||
await ui.app.setup()
|
||||
await browser.keys(['l'])
|
||||
await browser.waitUntil(async () => {
|
||||
return await runtime.isIn('line.idle')
|
||||
})
|
||||
})
|
||||
|
||||
it('text — T', async () => {
|
||||
await ui.app.setup()
|
||||
await browser.keys(['t'])
|
||||
await browser.waitUntil(async () => {
|
||||
return await runtime.isIn('text.idle')
|
||||
})
|
||||
})
|
||||
|
||||
it('frame — F', async () => {
|
||||
await ui.app.setup()
|
||||
await browser.keys(['f'])
|
||||
await browser.waitUntil(async () => {
|
||||
return await runtime.isIn('frame.idle')
|
||||
})
|
||||
})
|
||||
|
||||
it('sticky — N', async () => {
|
||||
await ui.app.setup()
|
||||
await browser.keys(['n'])
|
||||
await browser.waitUntil(async () => {
|
||||
return await runtime.isIn('note.idle')
|
||||
})
|
||||
})
|
||||
|
||||
// View
|
||||
it.todo('zoom-in — ⌘+', () => {})
|
||||
it.todo('zoom-in — ⌘-', () => {})
|
||||
it.todo('zoom-in — ⌘0', () => {})
|
||||
it.todo('zoom-in — ⌘1', () => {})
|
||||
it.todo('zoom-in — ⌘2', () => {})
|
||||
it.todo('zoom-in — ⌘/', () => {})
|
||||
it.todo('zoom-in — ⌘.', () => {})
|
||||
it.todo("zoom-in — ⌘'", () => {})
|
||||
|
||||
// Transform
|
||||
it.todo('flip-h — ⇧H', () => {})
|
||||
it.todo('flip-v — ⇧V', () => {})
|
||||
it.todo('lock/unlock — ⌘L', () => {})
|
||||
it.todo('move-to-front — ]', () => {})
|
||||
it.todo('move-forward — ⌥]', () => {})
|
||||
it.todo('move-backward — ⌥[', () => {})
|
||||
it.todo('move-to-back — [', () => {})
|
||||
it.todo('group — ⌘G', () => {})
|
||||
it.todo('ungroup — ⌘⇧G', () => {})
|
||||
|
||||
// File
|
||||
it.todo('new-project — ⌘N', () => {})
|
||||
it.todo('open — ⌘O', () => {})
|
||||
it.todo('save — ⌘S', () => {})
|
||||
it.todo('save-as — ⌘⇧S', () => {})
|
||||
it.todo('upload-media — ⌘I', () => {})
|
||||
|
||||
// Edit
|
||||
it.todo('undo — ⌘Z', () => {})
|
||||
it.todo('redo — ⌘⇧Z', () => {})
|
||||
it.todo('cut — ⌘X', () => {})
|
||||
it.todo('copy — ⌘C', () => {})
|
||||
it.todo('paste — ⌘V', () => {})
|
||||
it.todo('select-all — ⌘A', () => {})
|
||||
it.todo('delete — ⌫', () => {})
|
||||
it.todo('duplicate — ⌘D', () => {})
|
||||
})
|
||||
})
|
|
@ -1,326 +0,0 @@
|
|||
import { TLGeoShape } from '@tldraw/editor'
|
||||
import { runtime, ui, util } from '../helpers'
|
||||
import { describe, env, it } from '../mocha-ext'
|
||||
import { SHAPES } from './constants'
|
||||
|
||||
describe('smoke', () => {
|
||||
env(
|
||||
{
|
||||
// FIXME
|
||||
skipBrowsers: ['firefox'],
|
||||
},
|
||||
() => {
|
||||
it('startup in correct state', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(110, 210, 160, 260)
|
||||
|
||||
await browser.waitUntil(async () => {
|
||||
const isInGeoIdle = await browser.execute(() => window.app.isIn('select.idle'))
|
||||
return isInGeoIdle === true
|
||||
})
|
||||
expect(await browser.execute(() => window.app.isIn('select.idle'))).toBe(true)
|
||||
expect(await browser.execute(() => window.app.shapesArray.length)).toBe(1)
|
||||
})
|
||||
|
||||
it('click/tap create/delete some shapes', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
for (const shape of SHAPES) {
|
||||
await ui.tools.click(shape.tool)
|
||||
await ui.canvas.click(10, 10)
|
||||
await ui.tools.click(shape.tool)
|
||||
await ui.canvas.click(110, 210)
|
||||
await ui.tools.click(shape.tool)
|
||||
await ui.canvas.click(210, 310)
|
||||
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
expect(allShapes.length).toBe(3)
|
||||
expect(allShapes.every((s) => s.type === shape.type))
|
||||
|
||||
await ui.main.menu(['edit', 'select-all'])
|
||||
await ui.main.menu(['edit', 'delete'])
|
||||
}
|
||||
})
|
||||
|
||||
it('brush create/delete some shapes', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
for (const shape of SHAPES) {
|
||||
await ui.tools.click(shape.tool)
|
||||
await ui.canvas.brush(10, 10, 60, 160)
|
||||
await ui.tools.click(shape.tool)
|
||||
await ui.canvas.brush(110, 210, 160, 260)
|
||||
await ui.tools.click(shape.tool)
|
||||
await ui.canvas.brush(210, 310, 260, 360)
|
||||
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
expect(allShapes.length).toBe(3)
|
||||
expect(allShapes.every((s) => s.type === shape.type))
|
||||
|
||||
await ui.main.menu(['edit', 'select-all'])
|
||||
await ui.main.menu(['edit', 'delete'])
|
||||
}
|
||||
// -----------------------------
|
||||
|
||||
// Text
|
||||
// await ui.tools.click('text')
|
||||
// // TODO: This fails if you don't hide the modal first
|
||||
// await ui.canvas.brush(10, 200, 250, 200)
|
||||
// await tldraw.app.activeInput()
|
||||
// await browser.keys("testing");
|
||||
|
||||
// await ui.tools.click('text')
|
||||
// await ui.canvas.brush(10, 200, 250, 200)
|
||||
// await tldraw.app.activeInput()
|
||||
// await browser.keys("testing");
|
||||
|
||||
// await ui.tools.click('text')
|
||||
// await ui.canvas.brush(10, 300, 250, 300)
|
||||
// await tldraw.app.activeInput()
|
||||
// await browser.keys("testing");
|
||||
|
||||
// const allShapes = await runtime.getShapesOfType();
|
||||
// expect(allShapes.length).toBe(3)
|
||||
// expect(allShapes.every(s => s.type === 'text'));
|
||||
// expect(allShapes.every(s => s.props.text === 'testing'));
|
||||
|
||||
// await cleanup();
|
||||
|
||||
// // Note
|
||||
// await ui.tools.click('note')
|
||||
// await ui.canvas.click(100, 100)
|
||||
// await ui.tools.click('note')
|
||||
// await ui.canvas.click(210, 210)
|
||||
// await ui.tools.click('note')
|
||||
// await ui.canvas.click(310, 310)
|
||||
|
||||
// for (const [index, [x, y]] of [
|
||||
// [70, 70],
|
||||
// [180, 180],
|
||||
// [280, 280],
|
||||
// ].entries()) {
|
||||
// // TODO: This only works if there is a small delay
|
||||
// await ui.canvas.doubleClick(x, y)
|
||||
// await browser.keys([`test${index}`])
|
||||
// await browser.action('key').down('\uE03D').down('\uE007').perform(true)
|
||||
// await util.sleep(20)
|
||||
// await browser.action('key').up('\uE007').up('\uE03D').perform(true)
|
||||
// }
|
||||
|
||||
// const allNoteShapes = await runtime.getAllShapes()
|
||||
// expect(allNoteShapes.length).toBe(3)
|
||||
// expect(allNoteShapes.every((s) => s.type === 'note'))
|
||||
// expect(allNoteShapes[0].props.text).toBe('test0')
|
||||
// expect(allNoteShapes[1].props.text).toBe('test1')
|
||||
// expect(allNoteShapes[2].props.text).toBe('test2')
|
||||
|
||||
// await ui.main.menu(['edit', 'select-all'])
|
||||
// await ui.main.menu(['edit', 'delete'])
|
||||
|
||||
// Image
|
||||
// TODO
|
||||
|
||||
// Frame
|
||||
// FIXME: Fails on mobile
|
||||
// await ui.tools.click('frame')
|
||||
// await ui.canvas.brush(10, 10, 60, 160)
|
||||
// await ui.tools.click('frame')
|
||||
// await ui.canvas.brush(110, 210, 160, 260)
|
||||
// await ui.tools.click('frame')
|
||||
// await ui.canvas.brush(210, 310, 260, 360)
|
||||
|
||||
// await ui.canvas.doubleClick(10, 0)
|
||||
// await browser.keys([
|
||||
// 'test1',
|
||||
// '\uE007', // ENTER
|
||||
// ])
|
||||
|
||||
// await ui.canvas.doubleClick(110, 200)
|
||||
// await browser.keys([
|
||||
// 'test2',
|
||||
// '\uE007', // ENTER
|
||||
// ])
|
||||
|
||||
// await ui.canvas.doubleClick(210, 300)
|
||||
// await browser.keys([
|
||||
// 'test3',
|
||||
// '\uE007', // ENTER
|
||||
// ])
|
||||
|
||||
// const allShapes = await runtime.getAllShapes()
|
||||
// expect(allShapes.length).toBe(3)
|
||||
// expect(allShapes.every((s) => s.type === 'frame'))
|
||||
// expect(allShapes[0].props.name).toBe('test1')
|
||||
// expect(allShapes[1].props.name).toBe('test2')
|
||||
// expect(allShapes[2].props.name).toBe('test3')
|
||||
|
||||
// await ui.main.menu(['edit', 'select-all'])
|
||||
// await ui.main.menu(['edit', 'delete'])
|
||||
})
|
||||
|
||||
it.skip('[TODO] resize some shapes', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
// await ui.canvas.brush(10, 10, 100, 100)
|
||||
|
||||
for (const size of [30, 50, 70]) {
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(10, 10, 10 + size, 10 + size)
|
||||
|
||||
await ui.main.menu(['edit', 'select-all'])
|
||||
|
||||
const handle = await ui.canvas.selectionHandle('resize.bottom-right')
|
||||
await ui.canvas.dragBy(handle, 20, 20)
|
||||
|
||||
const allShapes = (await runtime.getAllShapes()) as TLGeoShape[]
|
||||
expect(allShapes.length).toBe(1)
|
||||
expect(allShapes[0].props.w).toBe(size + 20)
|
||||
expect(allShapes[0].props.h).toBe(size + 20)
|
||||
|
||||
await util.deleteAllShapesOnPage()
|
||||
}
|
||||
})
|
||||
|
||||
// REMOTE:OK
|
||||
it.skip('[TODO] rotate some shapes', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
for (const size of [70, 90, 100]) {
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(100, 120, 100 + size, 120 + size)
|
||||
|
||||
await ui.main.menu(['edit', 'select-all'])
|
||||
|
||||
const handle = await ui.canvas.selectionHandle('rotate.mobile', 'rotate.top-right')
|
||||
await ui.canvas.dragBy(handle, size / 2, size / 2)
|
||||
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
expect(allShapes.length).toBe(1)
|
||||
|
||||
const rotation = allShapes[0].rotation
|
||||
|
||||
// TODO: This isn't exact I assume because pixel issues with the DPR and webdriver
|
||||
expect(rotation > 0).toBe(true)
|
||||
|
||||
await util.deleteAllShapesOnPage()
|
||||
}
|
||||
})
|
||||
|
||||
// FIXME: Ok once resolved <https://linear.app/tldraw/issue/TLD-1290/clicking-with-the-selection-tools-creates-a-history-entry>
|
||||
it.skip('undo/redo', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(10, 10, 60, 60)
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(10, 110, 60, 160)
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(10, 210, 60, 260)
|
||||
|
||||
expect((await runtime.getAllShapes()).length).toBe(3)
|
||||
|
||||
await ui.main.click('undo')
|
||||
expect((await runtime.getAllShapes()).length).toBe(2)
|
||||
|
||||
await ui.main.click('undo')
|
||||
expect((await runtime.getAllShapes()).length).toBe(1)
|
||||
|
||||
await ui.main.click('undo')
|
||||
expect((await runtime.getAllShapes()).length).toBe(0)
|
||||
|
||||
await ui.main.click('undo')
|
||||
expect((await runtime.getAllShapes()).length).toBe(0)
|
||||
|
||||
await ui.main.click('redo')
|
||||
expect((await runtime.getAllShapes()).length).toBe(1)
|
||||
|
||||
await ui.main.click('redo')
|
||||
expect((await runtime.getAllShapes()).length).toBe(2)
|
||||
|
||||
await ui.main.click('redo')
|
||||
expect((await runtime.getAllShapes()).length).toBe(3)
|
||||
|
||||
await ui.main.click('redo')
|
||||
expect((await runtime.getAllShapes()).length).toBe(3)
|
||||
})
|
||||
|
||||
it.skip('reorder', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(10, 10, 60, 60)
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(30, 30, 80, 80)
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(50, 50, 100, 100)
|
||||
|
||||
throw new Error('TODO: Not done yet')
|
||||
|
||||
// await tldraw.canvas.contextMenu([x,y], ["reorder", "move-to-front"]);
|
||||
// // Assert order
|
||||
|
||||
// await tldraw.canvas.contextMenu([x,y], ["reorder", "move-to-front"]);
|
||||
// // Assert order
|
||||
|
||||
// await tldraw.canvas.contextMenu([x,y], ["reorder", "move-to-front"]);
|
||||
// // Assert order
|
||||
})
|
||||
|
||||
it.skip('move page', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
// await tldraw.main.pages.create()
|
||||
// await tldraw.main.pages.create()
|
||||
|
||||
// const pages = await tldraw.app.getPages()
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(10, 10, 60, 60)
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(10, 10, 60, 60)
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(10, 10, 60, 60)
|
||||
})
|
||||
|
||||
// REMOTE:OK
|
||||
it('group/ungroup', async () => {
|
||||
await ui.app.setup()
|
||||
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(100, 100, 150, 150)
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(200, 200, 250, 250)
|
||||
await ui.tools.click('rectangle')
|
||||
await ui.canvas.brush(300, 300, 350, 350)
|
||||
|
||||
await ui.main.menu(['edit', 'select-all'])
|
||||
await ui.main.menu(['edit', 'group'])
|
||||
|
||||
const groupShapesBefore = await runtime.getShapesOfType('group')
|
||||
const geoShapesBefore = await runtime.getShapesOfType('geo')
|
||||
expect(groupShapesBefore.length).toBe(1)
|
||||
expect(geoShapesBefore.length).toBe(3)
|
||||
|
||||
await ui.main.menu(['edit', 'select-all'])
|
||||
await ui.main.menu(['edit', 'ungroup'])
|
||||
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
const groupShapesAfter = allShapes.filter(
|
||||
(s) => s.typeName === 'shape' && s.type === 'group'
|
||||
)
|
||||
const geoShapesAfter = allShapes.filter((s) => s.typeName === 'shape' && s.type === 'geo')
|
||||
|
||||
expect(groupShapesAfter.length).toBe(0)
|
||||
expect(geoShapesAfter.length).toBe(3)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
|
@ -1,315 +0,0 @@
|
|||
import { TLShape } from '@tldraw/editor'
|
||||
import { runtime, ui, util } from '../helpers'
|
||||
import { describe, it } from '../mocha-ext'
|
||||
import { SHAPES } from './constants'
|
||||
|
||||
const LITE_MODE = true
|
||||
|
||||
const assertColors = (createShape: () => Promise<TLShape>) => {
|
||||
return async () => {
|
||||
await ui.app.setup()
|
||||
await createShape()
|
||||
const colors = LITE_MODE
|
||||
? ['black']
|
||||
: [
|
||||
'black',
|
||||
'grey',
|
||||
'light-violet',
|
||||
'violet',
|
||||
'blue',
|
||||
'light-blue',
|
||||
'yellow',
|
||||
'orange',
|
||||
'green',
|
||||
'light-green',
|
||||
'light-red',
|
||||
'red',
|
||||
]
|
||||
|
||||
await ui.props.ifMobileOpenStylesMenu()
|
||||
for (const color of colors) {
|
||||
await ui.props.selectColor(color)
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
expect(allShapes.length).toBe(1)
|
||||
expect('color' in allShapes[0].props && allShapes[0].props.color).toBe(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assertOpacity = (createShape: () => Promise<TLShape>) => {
|
||||
return async () => {
|
||||
await ui.app.setup()
|
||||
await createShape()
|
||||
const opacities = LITE_MODE ? [0.5] : [0.1, 0.25, 0.5, 0.75, 1]
|
||||
|
||||
await ui.props.ifMobileOpenStylesMenu()
|
||||
for (const opacity of opacities) {
|
||||
await ui.props.selectOpacity(opacity)
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
expect(allShapes.length).toBe(1)
|
||||
expect('opacity' in allShapes[0].props && allShapes[0].props.opacity).toBe(opacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assertFill = (createShape: () => Promise<TLShape>) => {
|
||||
return async () => {
|
||||
await ui.app.setup()
|
||||
await createShape()
|
||||
const fills = LITE_MODE ? ['solid'] : ['none', 'semi', 'solid', 'pattern']
|
||||
|
||||
await ui.props.ifMobileOpenStylesMenu()
|
||||
for (const fill of fills) {
|
||||
await ui.props.selectFill(fill)
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
expect(allShapes.length).toBe(1)
|
||||
expect(allShapes[0].props).toHaveProperty('fill', fill)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assertFont = (createShape: () => Promise<TLShape>) => {
|
||||
return async () => {
|
||||
await ui.app.setup()
|
||||
await createShape()
|
||||
const fonts = LITE_MODE ? ['sans'] : ['draw', 'sans', 'serif', 'mono']
|
||||
|
||||
await ui.props.ifMobileOpenStylesMenu()
|
||||
for (const font of fonts) {
|
||||
await ui.props.selectFont(font)
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
expect(allShapes.length).toBe(1)
|
||||
expect(allShapes[0].props).toHaveProperty('font', font)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assertAlign = (createShape: () => Promise<TLShape>) => {
|
||||
return async () => {
|
||||
await ui.app.setup()
|
||||
await createShape()
|
||||
const alignments = LITE_MODE ? ['middle'] : ['start', 'middle', 'end']
|
||||
|
||||
await ui.props.ifMobileOpenStylesMenu()
|
||||
for (const alignment of alignments) {
|
||||
await ui.props.selectAlign(alignment)
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
expect(allShapes.length).toBe(1)
|
||||
expect(allShapes[0].props).toHaveProperty('align', alignment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assertStroke = (createShape: () => Promise<TLShape>) => {
|
||||
return async () => {
|
||||
await ui.app.setup()
|
||||
await createShape()
|
||||
const strokes = LITE_MODE ? ['dashed'] : ['draw', 'dashed', 'dotted', 'solid']
|
||||
|
||||
await ui.props.ifMobileOpenStylesMenu()
|
||||
for (const stroke of strokes) {
|
||||
await ui.props.selectStroke(stroke)
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
expect(allShapes.length).toBe(1)
|
||||
expect(allShapes[0].props).toHaveProperty('dash', stroke)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assertSize = (createShape: () => Promise<TLShape>) => {
|
||||
return async () => {
|
||||
await ui.app.setup()
|
||||
await createShape()
|
||||
const sizes = LITE_MODE ? ['xl'] : ['s', 'm', 'l', 'xl']
|
||||
|
||||
await ui.props.ifMobileOpenStylesMenu()
|
||||
for (const size of sizes) {
|
||||
await ui.props.selectSize(size)
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
expect(allShapes.length).toBe(1)
|
||||
expect(allShapes[0].props).toHaveProperty('size', size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assertSpline = (createShape: () => Promise<TLShape>) => {
|
||||
return async () => {
|
||||
await ui.app.setup()
|
||||
await createShape()
|
||||
const types = LITE_MODE ? ['line'] : ['line', 'cubic']
|
||||
|
||||
await ui.props.ifMobileOpenStylesMenu()
|
||||
for (const type of types) {
|
||||
await ui.props.selectSpline(type)
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
expect(allShapes.length).toBe(1)
|
||||
expect(allShapes[0].props).toHaveProperty('spline', type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assertArrowheads = (createShape: () => Promise<TLShape>) => {
|
||||
return async () => {
|
||||
await ui.app.setup()
|
||||
await createShape()
|
||||
const types = LITE_MODE
|
||||
? ['triangle']
|
||||
: ['none', 'arrow', 'triangle', 'square', 'dot', 'diamond', 'inverted', 'bar']
|
||||
|
||||
await ui.props.ifMobileOpenStylesMenu()
|
||||
for (const startType of types) {
|
||||
for (const endType of types) {
|
||||
await ui.props.selectArrowheadStart(startType)
|
||||
await ui.props.selectArrowheadEnd(endType)
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
expect(allShapes.length).toBe(1)
|
||||
expect(allShapes[0].props).toHaveProperty('arrowheadStart', startType)
|
||||
expect(allShapes[0].props).toHaveProperty('arrowheadEnd', endType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe.skip('styling', () => {
|
||||
describe('draw', () => {
|
||||
const createShape = async () => {
|
||||
await ui.tools.click('draw')
|
||||
await ui.canvas.draw([
|
||||
{ x: 50, y: 50 },
|
||||
{ x: 300, y: 50 },
|
||||
{ x: 300, y: 300 },
|
||||
{ x: 50, y: 300 },
|
||||
{ x: 50, y: 50 },
|
||||
])
|
||||
await ui.tools.click('select')
|
||||
|
||||
return (await runtime.getAllShapes())[0]
|
||||
}
|
||||
it('color', assertColors(createShape))
|
||||
it.todo('opacity', assertOpacity(createShape))
|
||||
it('fill', assertFill(createShape))
|
||||
it('stroke', assertStroke(createShape))
|
||||
it('size', assertSize(createShape))
|
||||
})
|
||||
|
||||
describe('arrow', () => {
|
||||
const createShape = async () => {
|
||||
await ui.tools.click('arrow')
|
||||
await ui.canvas.brush(50, 50, 200, 200)
|
||||
|
||||
await ui.canvas.doubleClick((200 - 50) / 2, (200 - 50) / 2)
|
||||
await browser.keys(['test'])
|
||||
await browser.action('key').down('\uE03D').down('\uE007').perform(true)
|
||||
await util.sleep(20)
|
||||
await browser.action('key').up('\uE007').up('\uE03D').perform()
|
||||
|
||||
return (await runtime.getAllShapes())[0]
|
||||
}
|
||||
it('color', assertColors(createShape))
|
||||
it.todo('opacity', assertOpacity(createShape))
|
||||
it('fill', assertFill(createShape))
|
||||
it('stroke', assertStroke(createShape))
|
||||
it('size', assertSize(createShape))
|
||||
it('arrowheads', assertArrowheads(createShape))
|
||||
it('font', assertFont(createShape))
|
||||
})
|
||||
|
||||
describe('line', () => {
|
||||
const createShape = async () => {
|
||||
await ui.tools.click('line')
|
||||
await ui.canvas.brush(50, 50, 200, 200)
|
||||
|
||||
return (await runtime.getAllShapes())[0]
|
||||
}
|
||||
it('color', assertColors(createShape))
|
||||
it.todo('opacity', assertOpacity(createShape))
|
||||
it('stroke', assertStroke(createShape))
|
||||
it('size', assertSize(createShape))
|
||||
it('spline', assertSpline(createShape))
|
||||
})
|
||||
|
||||
SHAPES.map((shapeDef) => {
|
||||
describe(shapeDef.tool, () => {
|
||||
const createShape = async () => {
|
||||
await ui.tools.click(shapeDef.tool)
|
||||
await ui.canvas.brush(60, 60, 210, 210)
|
||||
|
||||
await ui.canvas.doubleClick(60 + (210 - 60) / 2, 60 + (210 - 60) / 2)
|
||||
await browser.keys(['test'])
|
||||
await browser.action('key').down('\uE03D').down('\uE007').perform(true)
|
||||
await util.sleep(20)
|
||||
await browser.action('key').up('\uE007').up('\uE03D').perform(true)
|
||||
|
||||
return (await runtime.getAllShapes())[0]
|
||||
}
|
||||
|
||||
it('color', assertColors(createShape))
|
||||
it.todo('opacity', () => {})
|
||||
it('fill', assertFill(createShape))
|
||||
it('stroke', assertStroke(createShape))
|
||||
it('size', assertSize(createShape))
|
||||
it('font', assertFont(createShape))
|
||||
it('align', assertAlign(createShape))
|
||||
})
|
||||
})
|
||||
|
||||
describe('text', () => {
|
||||
const createShape = async () => {
|
||||
await ui.tools.click('select')
|
||||
await ui.tools.click('text')
|
||||
await ui.canvas.click(100, 100)
|
||||
await browser.keys('testing')
|
||||
|
||||
return (await runtime.getAllShapes())[0]
|
||||
}
|
||||
it('color', assertColors(createShape))
|
||||
it.todo('opacity', assertOpacity(createShape))
|
||||
it('size', assertSize(createShape))
|
||||
it('font', assertFont(createShape))
|
||||
it('align', assertAlign(createShape))
|
||||
})
|
||||
|
||||
describe('frame', () => {
|
||||
const createShape = async () => {
|
||||
await ui.tools.click('frame')
|
||||
await ui.canvas.brush(10, 10, 60, 160)
|
||||
|
||||
await ui.canvas.doubleClick(10, 0)
|
||||
await browser.keys([
|
||||
'test',
|
||||
'\uE007', // ENTER
|
||||
])
|
||||
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
expect(allShapes.length).toBe(1)
|
||||
expect(allShapes[0].type).toBe('frame')
|
||||
expect(allShapes[0].props).toHaveProperty('name', 'test')
|
||||
|
||||
return allShapes[0]
|
||||
}
|
||||
it.todo('opacity', assertOpacity(createShape))
|
||||
})
|
||||
|
||||
describe.skip('note', () => {
|
||||
const createShape = async () => {
|
||||
await ui.tools.click('note')
|
||||
await ui.canvas.click(100, 100)
|
||||
await browser.keys(['test'])
|
||||
await browser.action('key').down('\uE03D').down('\uE007').perform(true)
|
||||
await util.sleep(20)
|
||||
await browser.action('key').up('\uE007').up('\uE03D').perform(true)
|
||||
|
||||
const allShapes = await runtime.getAllShapes()
|
||||
expect(allShapes.length).toBe(1)
|
||||
expect(allShapes[0].type).toBe('note')
|
||||
expect(allShapes[0].props).toHaveProperty('text', 'test')
|
||||
|
||||
return allShapes[0]
|
||||
}
|
||||
it('color', assertColors(createShape))
|
||||
it.todo('opacity', assertOpacity(createShape))
|
||||
it('size', assertSize(createShape))
|
||||
it('font', assertFont(createShape))
|
||||
it('align', assertAlign(createShape))
|
||||
})
|
||||
})
|
|
@ -1,278 +0,0 @@
|
|||
import { Box2dModel } from '@tldraw/editor'
|
||||
import { runtime, ui } from '../helpers'
|
||||
import { diffScreenshot, takeRegionScreenshot } from '../helpers/webdriver'
|
||||
import { describe, env, it } from '../mocha-ext'
|
||||
|
||||
describe('text', () => {
|
||||
env(
|
||||
{
|
||||
// This can be removed once bugs resolved on mobile.
|
||||
// Tracked in <https://linear.app/tldraw/issue/TLD-1300/re-enable-text-rendering-tests-on-mobile>
|
||||
device: 'desktop',
|
||||
},
|
||||
() => {
|
||||
const tests = [
|
||||
{
|
||||
name: 'multiline (align center)',
|
||||
fails: false,
|
||||
handler: async () => {
|
||||
await ui.tools.click('select')
|
||||
await ui.tools.click('text')
|
||||
await ui.canvas.brush(100, 0, 150, 150)
|
||||
await browser.keys('testing\ntesting\n1, 2, 3')
|
||||
},
|
||||
},
|
||||
// {
|
||||
// name: 'diacritics (align center)',
|
||||
// fails: false,
|
||||
// handler: async () => {
|
||||
// await ui.tools.click('text')
|
||||
// await ui.canvas.brush(50, 100, 150, 150)
|
||||
// await browser.keys('âéīôù')
|
||||
// },
|
||||
// },
|
||||
]
|
||||
|
||||
for (const test of tests) {
|
||||
const { name } = test
|
||||
const slugName = name.replace(/ /g, '-').replace(/[)(]/g, '')
|
||||
const prefix = [
|
||||
global.webdriverService,
|
||||
global.tldrawOptions.os,
|
||||
global.tldrawOptions.browser,
|
||||
global.tldrawOptions.ui,
|
||||
slugName,
|
||||
].join('-')
|
||||
|
||||
const cleanUp = async () => {
|
||||
await ui.main.menu(['edit', 'select-all'])
|
||||
await ui.main.menu(['edit', 'delete'])
|
||||
}
|
||||
|
||||
const testHandler = async () => {
|
||||
await ui.app.setup()
|
||||
await test.handler()
|
||||
|
||||
await ui.main.menu(['edit', 'select-all'])
|
||||
const selectionBounds = await runtime.selectionBounds()
|
||||
await ui.main.menu(['edit', 'select-none'])
|
||||
|
||||
const screenshotResults = await takeRegionScreenshot(selectionBounds, {
|
||||
writeTo: {
|
||||
path: `${__dirname}/../../screenshots/`,
|
||||
prefix,
|
||||
},
|
||||
})
|
||||
|
||||
const { pxielDiff } = await diffScreenshot(screenshotResults, {
|
||||
writeTo: {
|
||||
path: `${__dirname}/../../screenshots/`,
|
||||
prefix,
|
||||
},
|
||||
})
|
||||
|
||||
await cleanUp()
|
||||
expect(pxielDiff).toBeLessThan(70)
|
||||
}
|
||||
|
||||
it[test.fails ? 'fails' : 'ok']('text: ' + test.name, testHandler)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('text measurement', () => {
|
||||
const measureTextOptions = {
|
||||
width: 'fit-content',
|
||||
fontFamily: 'var(--tl-font-draw)',
|
||||
fontSize: 24,
|
||||
lineHeight: 1.35,
|
||||
fontWeight: 'normal',
|
||||
fontStyle: 'normal',
|
||||
padding: '0px',
|
||||
maxWidth: 'auto',
|
||||
}
|
||||
|
||||
const measureTextSpansOptions = {
|
||||
width: 100,
|
||||
height: 1000,
|
||||
overflow: 'wrap' as const,
|
||||
padding: 0,
|
||||
fontSize: 24,
|
||||
fontWeight: 'normal',
|
||||
fontFamily: 'var(--tl-font-draw)',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 1.35,
|
||||
textAlign: 'start' as 'start' | 'middle' | 'end',
|
||||
}
|
||||
|
||||
function formatLines(spans: { box: Box2dModel; text: string }[]) {
|
||||
const lines = []
|
||||
|
||||
let currentLine = null
|
||||
let currentLineTop = null
|
||||
for (const span of spans) {
|
||||
if (currentLineTop !== span.box.y) {
|
||||
if (currentLine !== null) {
|
||||
lines.push(currentLine)
|
||||
}
|
||||
currentLine = []
|
||||
currentLineTop = span.box.y
|
||||
}
|
||||
currentLine.push(span.text)
|
||||
}
|
||||
|
||||
if (currentLine !== null) {
|
||||
lines.push(currentLine)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
env({}, () => {
|
||||
it('should measure text', async () => {
|
||||
await ui.app.setup()
|
||||
const { w, h } = await browser.execute((options) => {
|
||||
return window.app.textMeasure.measureText('testing', options)
|
||||
}, measureTextOptions)
|
||||
|
||||
expect(w).toBeCloseTo(85.828125, 0)
|
||||
expect(h).toBeCloseTo(32.3984375, 0)
|
||||
})
|
||||
|
||||
// The text-measurement tests below this point aren't super useful any
|
||||
// more. They were added when we had a different approach to text SVG
|
||||
// exports (trying to replicate browser decisions with our own code) to
|
||||
// what we do now (letting the browser make those decisions then
|
||||
// measuring the results).
|
||||
//
|
||||
// It's hard to write better tests here (e.g. ones where we actually
|
||||
// look at the measured values) because the specifics of text layout
|
||||
// vary from browser to browser. The ideal thing would be to replace
|
||||
// these with visual regression tests for text SVG exports, but we don't
|
||||
// have a way of doing visual regression testing right now.
|
||||
|
||||
it('should get a single text span', async () => {
|
||||
await ui.app.setup()
|
||||
const spans = await browser.execute((options) => {
|
||||
return window.app.textMeasure.measureTextSpans('testing', options)
|
||||
}, measureTextSpansOptions)
|
||||
|
||||
expect(formatLines(spans)).toEqual([['testing']])
|
||||
})
|
||||
|
||||
it('should wrap a word when it has to', async () => {
|
||||
await ui.app.setup()
|
||||
const spans = await browser.execute((options) => {
|
||||
return window.app.textMeasure.measureTextSpans('testing', { ...options, width: 50 })
|
||||
}, measureTextSpansOptions)
|
||||
|
||||
expect(formatLines(spans)).toEqual([['test'], ['ing']])
|
||||
})
|
||||
|
||||
it('should wrap between words when it has to', async () => {
|
||||
await ui.app.setup()
|
||||
const spans = await browser.execute((options) => {
|
||||
return window.app.textMeasure.measureTextSpans('testing testing', options)
|
||||
}, measureTextSpansOptions)
|
||||
|
||||
expect(formatLines(spans)).toEqual([['testing', ' '], ['testing']])
|
||||
})
|
||||
|
||||
it('should preserve whitespace at line breaks', async () => {
|
||||
await ui.app.setup()
|
||||
const spans = await browser.execute((options) => {
|
||||
return window.app.textMeasure.measureTextSpans('testing testing', options)
|
||||
}, measureTextSpansOptions)
|
||||
|
||||
expect(formatLines(spans)).toEqual([['testing', ' '], ['testing']])
|
||||
})
|
||||
|
||||
it('should preserve whitespace at the end of wrapped lines', async () => {
|
||||
await ui.app.setup()
|
||||
const spans = await browser.execute((options) => {
|
||||
return window.app.textMeasure.measureTextSpans('testing testing ', options)
|
||||
}, measureTextSpansOptions)
|
||||
|
||||
expect(formatLines(spans)).toEqual([
|
||||
['testing', ' '],
|
||||
['testing', ' '],
|
||||
])
|
||||
})
|
||||
|
||||
it('preserves whitespace at the end of unwrapped lines', async () => {
|
||||
await ui.app.setup()
|
||||
const spans = await browser.execute((options) => {
|
||||
return window.app.textMeasure.measureTextSpans('testing testing ', {
|
||||
...options,
|
||||
width: 200,
|
||||
})
|
||||
}, measureTextSpansOptions)
|
||||
|
||||
expect(formatLines(spans)).toEqual([['testing', ' ', 'testing', ' ']])
|
||||
})
|
||||
|
||||
it('preserves whitespace at the start of an unwrapped line', async () => {
|
||||
await ui.app.setup()
|
||||
const spans = await browser.execute((options) => {
|
||||
return window.app.textMeasure.measureTextSpans(' testing testing', {
|
||||
...options,
|
||||
width: 200,
|
||||
})
|
||||
}, measureTextSpansOptions)
|
||||
|
||||
expect(formatLines(spans)).toEqual([[' ', 'testing', ' ', 'testing']])
|
||||
})
|
||||
|
||||
it('should place starting whitespace on its own line if it has to', async () => {
|
||||
await ui.app.setup()
|
||||
const spans = await browser.execute((options) => {
|
||||
return window.app.textMeasure.measureTextSpans(' testing testing', options)
|
||||
}, measureTextSpansOptions)
|
||||
|
||||
expect(formatLines(spans)).toEqual([[' '], ['testing', ' '], ['testing']])
|
||||
})
|
||||
|
||||
it('should handle multiline text', async () => {
|
||||
await ui.app.setup()
|
||||
const spans = await browser.execute((options) => {
|
||||
return window.app.textMeasure.measureTextSpans(' test\ning testing \n t', options)
|
||||
}, measureTextSpansOptions)
|
||||
|
||||
expect(formatLines(spans)).toEqual([
|
||||
[' ', 'test', '\n'],
|
||||
['ing', ' '],
|
||||
['testing', ' \n'],
|
||||
[' ', 't'],
|
||||
])
|
||||
})
|
||||
|
||||
it('should break long strings of text', async () => {
|
||||
await ui.app.setup()
|
||||
const spans = await browser.execute((options) => {
|
||||
return window.app.textMeasure.measureTextSpans(
|
||||
'testingtestingtestingtestingtestingtesting',
|
||||
options
|
||||
)
|
||||
}, measureTextSpansOptions)
|
||||
|
||||
expect(formatLines(spans)).toEqual([
|
||||
['testingt'],
|
||||
['estingte'],
|
||||
['stingtes'],
|
||||
['tingtest'],
|
||||
['ingtesti'],
|
||||
['ng'],
|
||||
])
|
||||
})
|
||||
|
||||
it('should return an empty array if the text is empty', async () => {
|
||||
await ui.app.setup()
|
||||
const spans = await browser.execute((options) => {
|
||||
return window.app.textMeasure.measureTextSpans('', options)
|
||||
}, measureTextSpansOptions)
|
||||
|
||||
expect(formatLines(spans)).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,12 +0,0 @@
|
|||
export const IS_MAC_OS = process.platform === 'darwin'
|
||||
|
||||
export const WINDOW_SIZES = {
|
||||
MOBILE_PORTRAIT: {
|
||||
width: 414,
|
||||
height: 796,
|
||||
},
|
||||
DESKTOP: {
|
||||
width: 1200,
|
||||
height: 800,
|
||||
},
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
{
|
||||
"include": ["test"],
|
||||
"exclude": ["node_modules", "dist", ".tsbuild*"],
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"importHelpers": true,
|
||||
"lib": ["dom", "esnext"],
|
||||
"esModuleInterop": true,
|
||||
"target": "ESNext",
|
||||
"moduleResolution": "node16",
|
||||
"types": [
|
||||
"node",
|
||||
"@wdio/globals/types",
|
||||
"@wdio/mocha-framework",
|
||||
"@types/mocha",
|
||||
"wdio-vscode-service"
|
||||
],
|
||||
"noEmitOnError": false,
|
||||
"noEmit": true
|
||||
},
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"module": "CommonJS"
|
||||
}
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../packages/editor" },
|
||||
{ "path": "../packages/indices" },
|
||||
{ "path": "../packages/tlschema" },
|
||||
{ "path": "../packages/tlstore" },
|
||||
{ "path": "../packages/primitives" }
|
||||
]
|
||||
}
|
|
@ -1,282 +0,0 @@
|
|||
const { BUILD_NAME, logBrowserstackUrl, filterCapabilities } = require('./wdio.util')
|
||||
|
||||
global.webdriverService = 'browserstack'
|
||||
global.webdriverTestUrl = 'http://localhost:5420/'
|
||||
|
||||
const capabilities = [
|
||||
/**
|
||||
* ====================================================================
|
||||
* Windows 11
|
||||
* ====================================================================
|
||||
*/
|
||||
{
|
||||
'bstack:options': {
|
||||
os: 'Windows',
|
||||
osVersion: '11',
|
||||
browserVersion: 'latest',
|
||||
seleniumVersion: '3.14.0',
|
||||
},
|
||||
browserName: 'Chrome',
|
||||
'tldraw:options': {
|
||||
browser: 'chrome',
|
||||
os: 'win32',
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
{
|
||||
'bstack:options': {
|
||||
os: 'Windows',
|
||||
osVersion: '11',
|
||||
browserVersion: 'latest',
|
||||
seleniumVersion: '4.6.0',
|
||||
},
|
||||
acceptInsecureCerts: 'true',
|
||||
browserName: 'Edge',
|
||||
'tldraw:options': {
|
||||
browser: 'edge',
|
||||
os: 'win32',
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
{
|
||||
'bstack:options': {
|
||||
os: 'Windows',
|
||||
osVersion: '11',
|
||||
browserVersion: 'latest',
|
||||
seleniumVersion: '4.6.0',
|
||||
},
|
||||
acceptInsecureCerts: 'true',
|
||||
browserName: 'Firefox',
|
||||
'tldraw:options': {
|
||||
browser: 'firefox',
|
||||
os: 'win32',
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
/**
|
||||
* ====================================================================
|
||||
* MacOS
|
||||
* ====================================================================
|
||||
*/
|
||||
// {
|
||||
// 'bstack:options' : {
|
||||
// "os" : "OS X",
|
||||
// "osVersion" : "Ventura",
|
||||
// "browserVersion" : "16.0",
|
||||
// "seleniumVersion" : "4.6.0",
|
||||
// },
|
||||
// "acceptInsecureCerts" : "true",
|
||||
// "browserName" : "Safari",
|
||||
// },
|
||||
{
|
||||
'bstack:options': {
|
||||
os: 'OS X',
|
||||
osVersion: 'Ventura',
|
||||
browserVersion: 'latest',
|
||||
seleniumVersion: '4.6.0',
|
||||
},
|
||||
browserName: 'Chrome',
|
||||
'tldraw:options': {
|
||||
browser: 'chrome',
|
||||
os: 'darwin',
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
{
|
||||
'bstack:options': {
|
||||
os: 'OS X',
|
||||
osVersion: 'Ventura',
|
||||
browserVersion: 'latest',
|
||||
seleniumVersion: '4.6.0',
|
||||
},
|
||||
acceptInsecureCerts: 'true',
|
||||
browserName: 'Firefox',
|
||||
'tldraw:options': {
|
||||
browser: 'firefox',
|
||||
os: 'darwin',
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
{
|
||||
'bstack:options': {
|
||||
os: 'OS X',
|
||||
osVersion: 'Ventura',
|
||||
browserVersion: 'latest',
|
||||
seleniumVersion: '4.6.0',
|
||||
},
|
||||
acceptInsecureCerts: 'true',
|
||||
browserName: 'Edge',
|
||||
'tldraw:options': {
|
||||
browser: 'edge',
|
||||
os: 'darwin',
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
/**
|
||||
// * ====================================================================
|
||||
// * Android
|
||||
// * ====================================================================
|
||||
// */
|
||||
{
|
||||
'bstack:options': {
|
||||
osVersion: '13.0',
|
||||
deviceName: 'Google Pixel 7',
|
||||
appiumVersion: '1.22.0',
|
||||
},
|
||||
browserName: 'chrome',
|
||||
'tldraw:options': {
|
||||
appium: true,
|
||||
browser: 'chrome',
|
||||
os: 'android',
|
||||
ui: 'mobile',
|
||||
device: 'mobile',
|
||||
input: ['touch'],
|
||||
},
|
||||
},
|
||||
{
|
||||
'bstack:options': {
|
||||
osVersion: '11.0',
|
||||
deviceName: 'Samsung Galaxy S21',
|
||||
appiumVersion: '1.22.0',
|
||||
},
|
||||
acceptInsecureCerts: 'true',
|
||||
browserName: 'samsung',
|
||||
'tldraw:options': {
|
||||
appium: true,
|
||||
browser: 'samsung',
|
||||
os: 'android',
|
||||
ui: 'mobile',
|
||||
device: 'mobile',
|
||||
input: ['touch'],
|
||||
},
|
||||
},
|
||||
{
|
||||
'bstack:options': {
|
||||
osVersion: '11.0',
|
||||
deviceName: 'Samsung Galaxy S21',
|
||||
appiumVersion: '1.22.0',
|
||||
},
|
||||
acceptInsecureCerts: 'true',
|
||||
browserName: 'chrome',
|
||||
'tldraw:options': {
|
||||
appium: true,
|
||||
browser: 'chrome',
|
||||
os: 'android',
|
||||
ui: 'mobile',
|
||||
device: 'mobile',
|
||||
input: ['touch'],
|
||||
},
|
||||
},
|
||||
/**
|
||||
* ====================================================================
|
||||
* iOS
|
||||
* ====================================================================
|
||||
*/
|
||||
// {
|
||||
// 'bstack:options': {
|
||||
// "osVersion" : "16",
|
||||
// "deviceName" : "iPhone 14",
|
||||
// "appiumVersion": "1.22.0"
|
||||
// },
|
||||
// "acceptInsecureCerts" : "true",
|
||||
// "browserName" : "safari",
|
||||
// },
|
||||
// {
|
||||
// 'bstack:options': {
|
||||
// "osVersion" : "16",
|
||||
// "deviceName" : "iPad Pro 12.9 2022",
|
||||
// "appiumVersion": "1.22.0"
|
||||
// },
|
||||
// "acceptInsecureCerts" : "true",
|
||||
// "browserName" : "safari",
|
||||
// },
|
||||
].map((capability) => {
|
||||
return {
|
||||
...capability,
|
||||
acceptInsecureCerts: true,
|
||||
'bstack:options': {
|
||||
...capability['bstack:options'],
|
||||
projectName: 'tldraw',
|
||||
buildName: BUILD_NAME,
|
||||
consoleLogs: 'verbose',
|
||||
},
|
||||
'tldraw:options': {
|
||||
...capability['tldraw:options'],
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
exports.config = {
|
||||
user: process.env.BROWSERSTACK_USER,
|
||||
key: process.env.BROWSERSTACK_KEY,
|
||||
hostname: 'hub.browserstack.com',
|
||||
specs: ['./test/specs/index.ts'],
|
||||
services: [
|
||||
[
|
||||
'browserstack',
|
||||
{
|
||||
browserstackLocal: true,
|
||||
testObservability: true,
|
||||
testObservabilityOptions: {
|
||||
projectName: 'tldraw',
|
||||
buildName: BUILD_NAME,
|
||||
buildTag: process.env.GITHUB_SHA || 'local',
|
||||
},
|
||||
opts: {
|
||||
verbose: 'true',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
exclude: [],
|
||||
maxInstances: 1,
|
||||
waitforInterval: 200,
|
||||
/**
|
||||
* Capabilities can be configured via <https://www.browserstack.com/automate/capabilities>
|
||||
*
|
||||
* The once commented out currently fail on because of insecure certs, details <https://www.browserstack.com/guide/how-to-test-https-websites-from-localhost>
|
||||
*/
|
||||
capabilities: filterCapabilities(capabilities),
|
||||
bail: 0,
|
||||
waitforTimeout: 10000,
|
||||
connectionRetryTimeout: 120000,
|
||||
connectionRetryCount: 3,
|
||||
framework: 'mocha',
|
||||
reporters: ['spec'],
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
timeout: 5 * 60 * 1000,
|
||||
},
|
||||
logLevel: process.env.WD_LOG_LEVEL ?? 'info',
|
||||
coloredLogs: true,
|
||||
screenshotPath: './errorShots/',
|
||||
waitforTimeout: 30000,
|
||||
connectionRetryTimeout: 90000,
|
||||
connectionRetryCount: 3,
|
||||
beforeSession: (_config, capabilities) => {
|
||||
global.tldrawOptions = capabilities['tldraw:options']
|
||||
},
|
||||
afterSession: async (_config, capabilities, _specs) => {
|
||||
await logBrowserstackUrl()
|
||||
},
|
||||
autoCompileOpts: {
|
||||
autoCompile: true,
|
||||
tsNodeOpts: {
|
||||
transpileOnly: true,
|
||||
swc: true,
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
},
|
||||
}
|
|
@ -1,247 +0,0 @@
|
|||
const edgeDriver = require('@sitespeed.io/edgedriver')
|
||||
const { filterCapabilities } = require('./wdio.util')
|
||||
|
||||
const CURRENT_OS = process.platform
|
||||
|
||||
global.webdriverService = 'local'
|
||||
global.webdriverTestUrl = process.env.TEST_URL ?? 'http://localhost:5420/'
|
||||
|
||||
let capabilities
|
||||
if (process.env.CI === 'true') {
|
||||
capabilities = [
|
||||
// {
|
||||
// maxInstances: 1,
|
||||
// browserName: 'chrome',
|
||||
// acceptInsecureCerts: true,
|
||||
// 'goog:chromeOptions': {
|
||||
// mobileEmulation: {
|
||||
// deviceName: 'iPhone XR',
|
||||
// },
|
||||
// prefs: {
|
||||
// download: {
|
||||
// default_directory: __dirname + '/downloads/',
|
||||
// prompt_for_download: false,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// 'tldraw:options': {
|
||||
// browser: 'chrome',
|
||||
// os: CURRENT_OS,
|
||||
// ui: 'mobile',
|
||||
// device: 'mobile',
|
||||
// input: ['touch'],
|
||||
// },
|
||||
// },
|
||||
{
|
||||
maxInstances: 1,
|
||||
browserName: 'chrome',
|
||||
acceptInsecureCerts: true,
|
||||
'goog:chromeOptions': {
|
||||
prefs: {
|
||||
download: {
|
||||
default_directory: __dirname + '/downloads/',
|
||||
prompt_for_download: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
'tldraw:options': {
|
||||
browser: 'chrome',
|
||||
os: CURRENT_OS,
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
]
|
||||
} else {
|
||||
capabilities = [
|
||||
{
|
||||
maxInstances: 1,
|
||||
browserName: 'chrome',
|
||||
acceptInsecureCerts: true,
|
||||
'goog:chromeOptions': {
|
||||
// Network emulation requires device mode, which is only enabled when mobile emulation is on
|
||||
mobileEmulation: {
|
||||
deviceName: 'iPhone XR',
|
||||
},
|
||||
prefs: {
|
||||
download: {
|
||||
default_directory: __dirname + '/downloads/',
|
||||
prompt_for_download: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
'tldraw:options': {
|
||||
browser: 'chrome',
|
||||
os: CURRENT_OS,
|
||||
ui: 'mobile',
|
||||
device: 'mobile',
|
||||
input: ['touch'],
|
||||
},
|
||||
},
|
||||
{
|
||||
maxInstances: 1,
|
||||
browserName: 'vscode',
|
||||
browserVersion: 'stable',
|
||||
acceptInsecureCerts: true,
|
||||
'wdio:vscodeOptions': {
|
||||
extensionPath: __dirname + '../bublic/apps/vscode/extension/dist/web',
|
||||
userSettings: {
|
||||
'editor.fontSize': 14,
|
||||
},
|
||||
},
|
||||
'tldraw:options': {
|
||||
browser: 'vscode',
|
||||
os: CURRENT_OS,
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
windowSize: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
maxInstances: 1,
|
||||
browserName: 'chrome',
|
||||
acceptInsecureCerts: true,
|
||||
'goog:chromeOptions': {
|
||||
prefs: {
|
||||
download: {
|
||||
default_directory: __dirname + '/downloads/',
|
||||
prompt_for_download: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
'tldraw:options': {
|
||||
browser: 'chrome',
|
||||
os: CURRENT_OS,
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
{
|
||||
maxInstances: 1,
|
||||
browserName: 'safari',
|
||||
acceptInsecureCerts: true,
|
||||
'tldraw:options': {
|
||||
browser: 'safari',
|
||||
os: CURRENT_OS,
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
{
|
||||
maxInstances: 1,
|
||||
browserName: 'firefox',
|
||||
acceptInsecureCerts: true,
|
||||
'tldraw:options': {
|
||||
browser: 'firefox',
|
||||
os: CURRENT_OS,
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
{
|
||||
maxInstances: 1,
|
||||
browserName: 'MicrosoftEdge',
|
||||
acceptInsecureCerts: true,
|
||||
'tldraw:options': {
|
||||
browser: 'edge',
|
||||
os: CURRENT_OS,
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
{
|
||||
maxInstances: 1,
|
||||
browserName: 'firefox',
|
||||
platformName: 'Linux',
|
||||
acceptInsecureCerts: true,
|
||||
'tldraw:options': {
|
||||
browser: 'firefox',
|
||||
os: 'linux',
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
exports.config = {
|
||||
specs: ['./test/specs/index.ts'],
|
||||
hostname: process.env.DOCKER_HOST || 'localhost',
|
||||
exclude: [],
|
||||
services: process.env.DOCKER_HOST
|
||||
? []
|
||||
: [
|
||||
['vscode', { verboseLogging: true }],
|
||||
[
|
||||
'geckodriver',
|
||||
{
|
||||
outputDir: './driver-logs',
|
||||
logFileName: 'wdio-geckodriver.log',
|
||||
},
|
||||
],
|
||||
[
|
||||
'safaridriver',
|
||||
{
|
||||
outputDir: './driver-logs',
|
||||
logFileName: 'wdio-safaridriver.log',
|
||||
},
|
||||
],
|
||||
[
|
||||
'chromedriver',
|
||||
{
|
||||
logFileName: 'wdio-chromedriver.log',
|
||||
outputDir: './driver-logs',
|
||||
args: ['--silent'],
|
||||
// NOTE: Must be on a different port that 7676 otherwise it conflicts with 'vscode' service.
|
||||
port: 7677,
|
||||
},
|
||||
],
|
||||
// HACK: If we don't have edge as a capability but we do have
|
||||
// this service then `wdio-edgedriver-service` throws an scary
|
||||
// error (which doesn't actually effect anything)
|
||||
...(!process.env.BROWSERS.split(',').includes('edge')
|
||||
? []
|
||||
: [
|
||||
[
|
||||
'edgedriver',
|
||||
{
|
||||
port: 17556, // default for EdgeDriver
|
||||
logFileName: 'wdio-edgedriver.log',
|
||||
outputDir: './driver-logs',
|
||||
edgedriverCustomPath: edgeDriver.binPath(),
|
||||
},
|
||||
],
|
||||
]),
|
||||
],
|
||||
maxInstances: 1,
|
||||
capabilities: filterCapabilities(capabilities),
|
||||
logLevel: process.env.WD_LOG_LEVEL ?? 'error',
|
||||
bail: 0,
|
||||
baseUrl: 'http://localhost',
|
||||
waitforTimeout: 10000,
|
||||
connectionRetryTimeout: 120000,
|
||||
connectionRetryCount: 3,
|
||||
framework: 'mocha',
|
||||
reporters: ['spec'],
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
timeout: 60000,
|
||||
},
|
||||
beforeSession: (_config, capabilities) => {
|
||||
global.tldrawOptions = capabilities['tldraw:options']
|
||||
},
|
||||
autoCompileOpts: {
|
||||
autoCompile: true,
|
||||
tsNodeOpts: {
|
||||
transpileOnly: true,
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
},
|
||||
}
|
|
@ -1,289 +0,0 @@
|
|||
const { BUILD_NAME, logBrowserstackUrl } = require('./wdio.util')
|
||||
|
||||
global.webdriverService = 'browserstack'
|
||||
global.webdriverTestUrl = 'http://localhost:5420/'
|
||||
|
||||
exports.config = {
|
||||
user: process.env.BROWSERSTACK_USER,
|
||||
key: process.env.BROWSERSTACK_KEY,
|
||||
hostname: 'hub.browserstack.com',
|
||||
specs: ['./test/specs/index.ts'],
|
||||
services: [
|
||||
[
|
||||
'browserstack',
|
||||
{
|
||||
browserstackLocal: true,
|
||||
testObservability: true,
|
||||
testObservabilityOptions: {
|
||||
projectName: 'tldraw',
|
||||
buildName: BUILD_NAME,
|
||||
buildTag: process.env.GITHUB_SHA || 'local',
|
||||
},
|
||||
opts: {
|
||||
verbose: 'true',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
exclude: [],
|
||||
maxInstances: 1,
|
||||
waitforInterval: 200,
|
||||
/**
|
||||
* Capabilities can be configured via <https://www.browserstack.com/automate/capabilities>
|
||||
*
|
||||
* The once commented out currently fail on because of insecure certs, details <https://www.browserstack.com/guide/how-to-test-https-websites-from-localhost>
|
||||
*/
|
||||
capabilities: [
|
||||
/**
|
||||
* ====================================================================
|
||||
* Windows 11
|
||||
* ====================================================================
|
||||
*/
|
||||
{
|
||||
'bstack:options': {
|
||||
os: 'Windows',
|
||||
osVersion: '11',
|
||||
browserVersion: 'latest',
|
||||
seleniumVersion: '3.14.0',
|
||||
},
|
||||
browserName: 'Chrome',
|
||||
'tldraw:options': {
|
||||
browser: 'chrome',
|
||||
os: 'windows',
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
{
|
||||
'bstack:options': {
|
||||
os: 'Windows',
|
||||
osVersion: '11',
|
||||
browserVersion: 'latest',
|
||||
seleniumVersion: '4.6.0',
|
||||
},
|
||||
acceptInsecureCerts: 'true',
|
||||
browserName: 'Edge',
|
||||
'tldraw:options': {
|
||||
browser: 'edge',
|
||||
os: 'windows',
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
{
|
||||
'bstack:options': {
|
||||
os: 'Windows',
|
||||
osVersion: '11',
|
||||
browserVersion: 'latest',
|
||||
seleniumVersion: '4.6.0',
|
||||
},
|
||||
acceptInsecureCerts: 'true',
|
||||
browserName: 'Firefox',
|
||||
'tldraw:options': {
|
||||
browser: 'firefox',
|
||||
os: 'windows',
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
/**
|
||||
* ====================================================================
|
||||
* MacOS
|
||||
* ====================================================================
|
||||
*/
|
||||
// {
|
||||
// 'bstack:options' : {
|
||||
// "os" : "OS X",
|
||||
// "osVersion" : "Ventura",
|
||||
// "browserVersion" : "16.0",
|
||||
// "seleniumVersion" : "4.6.0",
|
||||
// },
|
||||
// "acceptInsecureCerts" : "true",
|
||||
// "browserName" : "Safari",
|
||||
// },
|
||||
{
|
||||
'bstack:options': {
|
||||
os: 'OS X',
|
||||
osVersion: 'Ventura',
|
||||
browserVersion: 'latest',
|
||||
seleniumVersion: '4.6.0',
|
||||
},
|
||||
browserName: 'Chrome',
|
||||
'tldraw:options': {
|
||||
browser: 'chrome',
|
||||
os: 'macos',
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
{
|
||||
'bstack:options': {
|
||||
os: 'OS X',
|
||||
osVersion: 'Ventura',
|
||||
browserVersion: 'latest',
|
||||
seleniumVersion: '4.6.0',
|
||||
},
|
||||
acceptInsecureCerts: 'true',
|
||||
browserName: 'Firefox',
|
||||
'tldraw:options': {
|
||||
browser: 'firefox',
|
||||
os: 'macos',
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
{
|
||||
'bstack:options': {
|
||||
os: 'OS X',
|
||||
osVersion: 'Ventura',
|
||||
browserVersion: 'latest',
|
||||
seleniumVersion: '4.6.0',
|
||||
},
|
||||
acceptInsecureCerts: 'true',
|
||||
browserName: 'Edge',
|
||||
'tldraw:options': {
|
||||
browser: 'edge',
|
||||
os: 'macos',
|
||||
ui: 'desktop',
|
||||
device: 'desktop',
|
||||
input: ['mouse'],
|
||||
},
|
||||
},
|
||||
/**
|
||||
// * ====================================================================
|
||||
// * Android
|
||||
// * ====================================================================
|
||||
// */
|
||||
{
|
||||
'bstack:options': {
|
||||
osVersion: '13.0',
|
||||
deviceName: 'Google Pixel 7',
|
||||
appiumVersion: '1.22.0',
|
||||
},
|
||||
browserName: 'chrome',
|
||||
'tldraw:options': {
|
||||
appium: true,
|
||||
browser: 'chrome',
|
||||
os: 'android',
|
||||
ui: 'mobile',
|
||||
device: 'mobile',
|
||||
input: ['touch'],
|
||||
},
|
||||
},
|
||||
{
|
||||
'bstack:options': {
|
||||
osVersion: '11.0',
|
||||
deviceName: 'Samsung Galaxy S21',
|
||||
appiumVersion: '1.22.0',
|
||||
},
|
||||
acceptInsecureCerts: 'true',
|
||||
browserName: 'samsung',
|
||||
'tldraw:options': {
|
||||
appium: true,
|
||||
browser: 'samsung',
|
||||
os: 'android',
|
||||
ui: 'mobile',
|
||||
device: 'mobile',
|
||||
input: ['touch'],
|
||||
},
|
||||
},
|
||||
{
|
||||
'bstack:options': {
|
||||
osVersion: '11.0',
|
||||
deviceName: 'Samsung Galaxy S21',
|
||||
appiumVersion: '1.22.0',
|
||||
},
|
||||
acceptInsecureCerts: 'true',
|
||||
browserName: 'chrome',
|
||||
'tldraw:options': {
|
||||
appium: true,
|
||||
browser: 'chrome',
|
||||
os: 'android',
|
||||
ui: 'mobile',
|
||||
device: 'mobile',
|
||||
input: ['touch'],
|
||||
},
|
||||
},
|
||||
/**
|
||||
* ====================================================================
|
||||
* iOS
|
||||
* ====================================================================
|
||||
*/
|
||||
// {
|
||||
// 'bstack:options': {
|
||||
// "osVersion" : "16",
|
||||
// "deviceName" : "iPhone 14",
|
||||
// "appiumVersion": "1.22.0"
|
||||
// },
|
||||
// "acceptInsecureCerts" : "true",
|
||||
// "browserName" : "safari",
|
||||
// },
|
||||
// {
|
||||
// 'bstack:options': {
|
||||
// "osVersion" : "16",
|
||||
// "deviceName" : "iPad Pro 12.9 2022",
|
||||
// "appiumVersion": "1.22.0"
|
||||
// },
|
||||
// "acceptInsecureCerts" : "true",
|
||||
// "browserName" : "safari",
|
||||
// },
|
||||
]
|
||||
.map((capability) => {
|
||||
return {
|
||||
...capability,
|
||||
acceptInsecureCerts: true,
|
||||
'bstack:options': {
|
||||
...capability['bstack:options'],
|
||||
projectName: 'tldraw',
|
||||
buildName: BUILD_NAME,
|
||||
consoleLogs: 'verbose',
|
||||
},
|
||||
'tldraw:options': {
|
||||
...capability['tldraw:options'],
|
||||
},
|
||||
}
|
||||
})
|
||||
.filter((capability) => {
|
||||
const { os, browser } = capability['tldraw:options']
|
||||
const envOsKey = `WD_OS_${os.toUpperCase()}`
|
||||
const envBrowserKey = `WD_BROWSER_${browser.toUpperCase()}`
|
||||
const envOsValue = process.env[envOsKey]
|
||||
const envBrowserValue = process.env[envBrowserKey]
|
||||
return !(envOsValue === 'false' || envBrowserValue === 'false')
|
||||
}),
|
||||
bail: 0,
|
||||
waitforTimeout: 10000,
|
||||
connectionRetryTimeout: 120000,
|
||||
connectionRetryCount: 3,
|
||||
framework: 'mocha',
|
||||
reporters: ['spec'],
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
timeout: 5 * 60 * 1000,
|
||||
},
|
||||
logLevel: process.env.WD_LOG_LEVEL ?? 'info',
|
||||
coloredLogs: true,
|
||||
screenshotPath: './errorShots/',
|
||||
waitforTimeout: 30000,
|
||||
connectionRetryTimeout: 90000,
|
||||
connectionRetryCount: 3,
|
||||
beforeSession: (_config, capabilities) => {
|
||||
global.tldrawOptions = capabilities['tldraw:options']
|
||||
},
|
||||
afterSession: async (_config, capabilities, _specs) => {
|
||||
await logBrowserstackUrl()
|
||||
},
|
||||
autoCompileOpts: {
|
||||
autoCompile: true,
|
||||
tsNodeOpts: {
|
||||
transpileOnly: true,
|
||||
swc: true,
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
},
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
let BUILD_NAME = 'e2e'
|
||||
if (process.env.GH_EVENT_NAME === 'pull_request') {
|
||||
BUILD_NAME += `-pr-${process.env.GH_PR_NUMBER}`
|
||||
} else if (process.env.WB_BUILD_NAME) {
|
||||
BUILD_NAME += `-${process.env.WB_BUILD_NAME}`
|
||||
}
|
||||
|
||||
async function logBrowserstackUrl() {
|
||||
const sessionId = capabilities['webdriver.remote.sessionid']
|
||||
|
||||
const headers = new Headers()
|
||||
headers.set(
|
||||
'Authorization',
|
||||
'Basic ' + btoa(process.env.BROWSERSTACK_USER + ':' + process.env.BROWSERSTACK_KEY)
|
||||
)
|
||||
|
||||
const resp = await fetch(`https://api.browserstack.com/automate/sessions/${sessionId}.json`, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
})
|
||||
|
||||
const respJson = await resp.json()
|
||||
console.log(`==================================
|
||||
browser_url: <${respJson.automation_session.browser_url}>
|
||||
==================================`)
|
||||
}
|
||||
|
||||
function filterCapabilities(capabilities) {
|
||||
let browsers = (process.env.BROWSERS || 'chrome').split(',').map((b) => b.trim())
|
||||
const validBrowsers = ['chrome', 'safari', 'firefox', 'edge', 'vscode']
|
||||
const skippedBrowsers = []
|
||||
|
||||
if (browsers.includes('safari')) {
|
||||
console.log(
|
||||
'NOTE: In safari you need to run `safaridriver --enable`, see <https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari> for details.'
|
||||
)
|
||||
}
|
||||
|
||||
for (const browser of browsers) {
|
||||
if (!validBrowsers.includes(browser)) {
|
||||
throw new Error(`'${browser}' not a valid browser name`)
|
||||
}
|
||||
if (skippedBrowsers.includes(browser)) {
|
||||
console.error(`'${browser}' not currently supported`)
|
||||
}
|
||||
}
|
||||
|
||||
// let oses = (process.env.OS || process.platform).split(',').map((b) => b.trim())
|
||||
// const validOses = ['darwin', 'win32', 'linux']
|
||||
|
||||
// for (const os of oses) {
|
||||
// if (!validOses.includes(os)) {
|
||||
// throw new Error(`'${os}' not a valid OS name`)
|
||||
// }
|
||||
// }
|
||||
|
||||
const filterFn = (capability) => {
|
||||
return browsers.includes(capability['tldraw:options'].browser)
|
||||
// oses.includes(capability['tldraw:options'].os)
|
||||
}
|
||||
|
||||
return capabilities.filter(filterFn)
|
||||
}
|
||||
|
||||
module.exports = { BUILD_NAME, logBrowserstackUrl, filterCapabilities }
|
|
@ -51,7 +51,7 @@
|
|||
"check-scripts": "tsx scripts/check-scripts.ts",
|
||||
"api-check": "lazy api-check",
|
||||
"test": "lazy test",
|
||||
"e2e": "tsx scripts/e2e/index.ts"
|
||||
"e2e": "lazy e2e --filter='{,bublic/}apps/examples'"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=7.0.0"
|
||||
|
@ -89,6 +89,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/api-extractor": "^7.34.1",
|
||||
"@playwright/test": "^1.34.3",
|
||||
"@swc/core": "^1.3.55",
|
||||
"@swc/jest": "^0.2.26",
|
||||
"@types/glob": "^8.1.0",
|
||||
|
|
|
@ -216,6 +216,8 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
get editingId(): null | TLShapeId;
|
||||
// (undocumented)
|
||||
get editingShape(): null | TLUnknownShape;
|
||||
// (undocumented)
|
||||
enableAnimations: boolean;
|
||||
get erasingIds(): TLShapeId[];
|
||||
get erasingIdsSet(): Set<TLShapeId>;
|
||||
findAncestor(shape: TLShape, predicate: (parent: TLShape) => boolean): TLShape | undefined;
|
||||
|
|
|
@ -7877,6 +7877,8 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
return this
|
||||
}
|
||||
|
||||
enableAnimations = true
|
||||
|
||||
/**
|
||||
* Animate the camera.
|
||||
*
|
||||
|
@ -7909,6 +7911,10 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
|
||||
const targetViewport = new Box2d(-x, -y, w, h)
|
||||
|
||||
if (!this.enableAnimations) {
|
||||
return this.setCamera(x, y, this.viewportScreenBounds.width / w)
|
||||
}
|
||||
|
||||
return this._animateToViewport(targetViewport, opts)
|
||||
}
|
||||
|
||||
|
|
|
@ -134,6 +134,7 @@ export class Drawing extends StateNode {
|
|||
const lastPoint = lastSegment.points[lastSegment.points.length - 1]
|
||||
|
||||
return (
|
||||
firstPoint !== lastPoint &&
|
||||
this.currentLineLength > strokeWidth * 4 &&
|
||||
Vec2d.Dist(firstPoint, lastPoint) < strokeWidth * 2
|
||||
)
|
||||
|
@ -378,7 +379,8 @@ export class Drawing extends StateNode {
|
|||
],
|
||||
}
|
||||
|
||||
this.currentLineLength = this.getLineLength(segments)
|
||||
const finalSegments = [...newSegments, newFreeSegment]
|
||||
this.currentLineLength = this.getLineLength(finalSegments)
|
||||
|
||||
this.app.updateShapes(
|
||||
[
|
||||
|
@ -386,8 +388,8 @@ export class Drawing extends StateNode {
|
|||
id,
|
||||
type: 'draw',
|
||||
props: {
|
||||
segments: [...newSegments, newFreeSegment],
|
||||
isClosed: this.getIsClosed(segments, size),
|
||||
segments: finalSegments,
|
||||
isClosed: this.getIsClosed(finalSegments, size),
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -559,6 +561,8 @@ export class Drawing extends StateNode {
|
|||
points: newPoints,
|
||||
}
|
||||
|
||||
this.currentLineLength = this.getLineLength(newSegments)
|
||||
|
||||
this.app.updateShapes(
|
||||
[
|
||||
{
|
||||
|
|
|
@ -98,7 +98,7 @@ export const Canvas = track(function Canvas({
|
|||
}, [])
|
||||
|
||||
return (
|
||||
<div ref={rCanvas} draggable={false} className="tl-canvas" data-wd="canvas" {...events}>
|
||||
<div ref={rCanvas} draggable={false} className="tl-canvas" data-testid="canvas" {...events}>
|
||||
{Background && <Background />}
|
||||
<GridWrapper />
|
||||
<UiLogger />
|
||||
|
|
|
@ -22,7 +22,7 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
|
|||
${toDomPrecision(0 - offset)},${toDomPrecision(0 - offset)}
|
||||
${toDomPrecision(size)},${toDomPrecision(0 - offset)}`}
|
||||
strokeWidth={cropStrokeWidth}
|
||||
data-wd="selection.crop.top_left"
|
||||
data-testid="selection.crop.top_left"
|
||||
aria-label="top_left handle"
|
||||
/>
|
||||
{/* Top */}
|
||||
|
@ -35,7 +35,7 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
|
|||
x2={toDomPrecision(width / 2 + size)}
|
||||
y2={toDomPrecision(0 - offset)}
|
||||
strokeWidth={cropStrokeWidth}
|
||||
data-wd="selection.crop.top"
|
||||
data-testid="selection.crop.top"
|
||||
aria-label="top handle"
|
||||
/>
|
||||
{/* Top right */}
|
||||
|
@ -48,7 +48,7 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
|
|||
${toDomPrecision(width + offset)},${toDomPrecision(0 - offset)}
|
||||
${toDomPrecision(width + offset)},${toDomPrecision(size)}`}
|
||||
strokeWidth={cropStrokeWidth}
|
||||
data-wd="selection.crop.top_right"
|
||||
data-testid="selection.crop.top_right"
|
||||
aria-label="top_right handle"
|
||||
/>
|
||||
{/* Right */}
|
||||
|
@ -61,7 +61,7 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
|
|||
x2={toDomPrecision(width + offset)}
|
||||
y2={toDomPrecision(height / 2 + size)}
|
||||
strokeWidth={cropStrokeWidth}
|
||||
data-wd="selection.crop.right"
|
||||
data-testid="selection.crop.right"
|
||||
aria-label="right handle"
|
||||
/>
|
||||
{/* Bottom right */}
|
||||
|
@ -72,7 +72,7 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
|
|||
${toDomPrecision(width + offset)},${toDomPrecision(height + offset)}
|
||||
${toDomPrecision(width - size)},${toDomPrecision(height + offset)}`}
|
||||
strokeWidth={cropStrokeWidth}
|
||||
data-wd="selection.crop.bottom_right"
|
||||
data-testid="selection.crop.bottom_right"
|
||||
aria-label="bottom_right handle"
|
||||
/>
|
||||
{/* Bottom */}
|
||||
|
@ -85,7 +85,7 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
|
|||
x2={toDomPrecision(width / 2 + size)}
|
||||
y2={toDomPrecision(height + offset)}
|
||||
strokeWidth={cropStrokeWidth}
|
||||
data-wd="selection.crop.bottom"
|
||||
data-testid="selection.crop.bottom"
|
||||
aria-label="bottom handle"
|
||||
/>
|
||||
{/* Bottom left */}
|
||||
|
@ -98,7 +98,7 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
|
|||
${toDomPrecision(0 - offset)},${toDomPrecision(height + offset)}
|
||||
${toDomPrecision(0 - offset)},${toDomPrecision(height - size)}`}
|
||||
strokeWidth={cropStrokeWidth}
|
||||
data-wd="selection.crop.bottom_left"
|
||||
data-testid="selection.crop.bottom_left"
|
||||
aria-label="bottom_left handle"
|
||||
/>
|
||||
{/* Left */}
|
||||
|
@ -111,7 +111,7 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
|
|||
x2={toDomPrecision(0 - offset)}
|
||||
y2={toDomPrecision(height / 2 + size)}
|
||||
strokeWidth={cropStrokeWidth}
|
||||
data-wd="selection.crop.left"
|
||||
data-testid="selection.crop.left"
|
||||
aria-label="left handle"
|
||||
/>
|
||||
</svg>
|
||||
|
|
|
@ -173,7 +173,11 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
textHandleHeight * zoom >= 4
|
||||
|
||||
return (
|
||||
<svg ref={rSvg} className="tl-overlays__item tl-selection__fg" data-wd="selection-foreground">
|
||||
<svg
|
||||
ref={rSvg}
|
||||
className="tl-overlays__item tl-selection__fg"
|
||||
data-testid="selection-foreground"
|
||||
>
|
||||
{shouldDisplayBox && (
|
||||
<rect
|
||||
className={classNames('tl-selection__fg__outline')}
|
||||
|
@ -182,7 +186,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
/>
|
||||
)}
|
||||
<RotateCornerHandle
|
||||
data-wd="selection.rotate.top-left"
|
||||
data-testid="selection.rotate.top-left"
|
||||
cx={0}
|
||||
cy={0}
|
||||
targetSize={targetSize}
|
||||
|
@ -191,7 +195,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
isHidden={hideRotateCornerHandles}
|
||||
/>
|
||||
<RotateCornerHandle
|
||||
data-wd="selection.rotate.top-right"
|
||||
data-testid="selection.rotate.top-right"
|
||||
cx={width + targetSize * 3}
|
||||
cy={0}
|
||||
targetSize={targetSize}
|
||||
|
@ -200,7 +204,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
isHidden={hideRotateCornerHandles}
|
||||
/>
|
||||
<RotateCornerHandle
|
||||
data-wd="selection.rotate.bottom-left"
|
||||
data-testid="selection.rotate.bottom-left"
|
||||
cx={0}
|
||||
cy={height + targetSize * 3}
|
||||
targetSize={targetSize}
|
||||
|
@ -209,7 +213,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
isHidden={hideRotateCornerHandles}
|
||||
/>
|
||||
<RotateCornerHandle
|
||||
data-wd="selection.rotate.bottom-right"
|
||||
data-testid="selection.rotate.bottom-right"
|
||||
cx={width + targetSize * 3}
|
||||
cy={height + targetSize * 3}
|
||||
targetSize={targetSize}
|
||||
|
@ -218,7 +222,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
isHidden={hideRotateCornerHandles}
|
||||
/>{' '}
|
||||
<MobileRotateHandle
|
||||
data-wd="selection.rotate.mobile"
|
||||
data-testid="selection.rotate.mobile"
|
||||
cx={isSmallX ? -targetSize * 1.5 : width / 2}
|
||||
cy={isSmallX ? height / 2 : -targetSize * 1.5}
|
||||
size={size}
|
||||
|
@ -229,7 +233,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
className={classNames('tl-transparent', {
|
||||
'tl-hidden': hideEdgeTargets,
|
||||
})}
|
||||
data-wd="selection.resize.top"
|
||||
data-testid="selection.resize.top"
|
||||
aria-label="top target"
|
||||
pointerEvents="all"
|
||||
x={0}
|
||||
|
@ -243,7 +247,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
className={classNames('tl-transparent', {
|
||||
'tl-hidden': hideEdgeTargets,
|
||||
})}
|
||||
data-wd="selection.resize.right"
|
||||
data-testid="selection.resize.right"
|
||||
aria-label="right target"
|
||||
pointerEvents="all"
|
||||
x={toDomPrecision(width - (isSmallX ? 0 : targetSizeX))}
|
||||
|
@ -257,7 +261,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
className={classNames('tl-transparent', {
|
||||
'tl-hidden': hideEdgeTargets,
|
||||
})}
|
||||
data-wd="selection.resize.bottom"
|
||||
data-testid="selection.resize.bottom"
|
||||
aria-label="bottom target"
|
||||
pointerEvents="all"
|
||||
x={0}
|
||||
|
@ -271,7 +275,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
className={classNames('tl-transparent', {
|
||||
'tl-hidden': hideEdgeTargets,
|
||||
})}
|
||||
data-wd="selection.resize.left"
|
||||
data-testid="selection.resize.left"
|
||||
aria-label="left target"
|
||||
pointerEvents="all"
|
||||
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX))}
|
||||
|
@ -286,7 +290,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
className={classNames('tl-transparent', {
|
||||
'tl-hidden': hideTopLeftCorner,
|
||||
})}
|
||||
data-wd="selection.target.top-left"
|
||||
data-testid="selection.target.top-left"
|
||||
aria-label="top-left target"
|
||||
pointerEvents="all"
|
||||
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX * 1.5))}
|
||||
|
@ -300,7 +304,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
className={classNames('tl-transparent', {
|
||||
'tl-hidden': hideTopRightCorner,
|
||||
})}
|
||||
data-wd="selection.target.top-right"
|
||||
data-testid="selection.target.top-right"
|
||||
aria-label="top-right target"
|
||||
pointerEvents="all"
|
||||
x={toDomPrecision(width - (isSmallX ? 0 : targetSizeX * 1.5))}
|
||||
|
@ -314,7 +318,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
className={classNames('tl-transparent', {
|
||||
'tl-hidden': hideBottomRightCorner,
|
||||
})}
|
||||
data-wd="selection.target.bottom-right"
|
||||
data-testid="selection.target.bottom-right"
|
||||
aria-label="bottom-right target"
|
||||
pointerEvents="all"
|
||||
x={toDomPrecision(width - (isSmallX ? targetSizeX : targetSizeX * 1.5))}
|
||||
|
@ -328,7 +332,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
className={classNames('tl-transparent', {
|
||||
'tl-hidden': hideBottomLeftCorner,
|
||||
})}
|
||||
data-wd="selection.target.bottom-left"
|
||||
data-testid="selection.target.bottom-left"
|
||||
aria-label="bottom-left target"
|
||||
pointerEvents="all"
|
||||
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 3 : targetSizeX * 1.5))}
|
||||
|
@ -342,7 +346,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
{showResizeHandles && (
|
||||
<>
|
||||
<rect
|
||||
data-wd="selection.resize.top-left"
|
||||
data-testid="selection.resize.top-left"
|
||||
className={classNames('tl-corner-handle', {
|
||||
'tl-hidden': hideTopLeftCorner,
|
||||
})}
|
||||
|
@ -353,7 +357,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
height={toDomPrecision(size)}
|
||||
/>
|
||||
<rect
|
||||
data-wd="selection.resize.top-right"
|
||||
data-testid="selection.resize.top-right"
|
||||
className={classNames('tl-corner-handle', {
|
||||
'tl-hidden': hideTopRightCorner,
|
||||
})}
|
||||
|
@ -364,7 +368,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
height={toDomPrecision(size)}
|
||||
/>
|
||||
<rect
|
||||
data-wd="selection.resize.bottom-right"
|
||||
data-testid="selection.resize.bottom-right"
|
||||
className={classNames('tl-corner-handle', {
|
||||
'tl-hidden': hideBottomRightCorner,
|
||||
})}
|
||||
|
@ -375,7 +379,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
height={toDomPrecision(size)}
|
||||
/>
|
||||
<rect
|
||||
data-wd="selection.resize.bottom-left"
|
||||
data-testid="selection.resize.bottom-left"
|
||||
className={classNames('tl-corner-handle', {
|
||||
'tl-hidden': hideBottomLeftCorner,
|
||||
})}
|
||||
|
@ -390,7 +394,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
{showTextResizeHandles && (
|
||||
<>
|
||||
<rect
|
||||
data-wd="selection.text-resize.left.handle"
|
||||
data-testid="selection.text-resize.left.handle"
|
||||
className="tl-text-handle"
|
||||
aria-label="bottom_left handle"
|
||||
x={toDomPrecision(0 - size / 4)}
|
||||
|
@ -400,7 +404,7 @@ export const SelectionFg = track(function SelectionFg() {
|
|||
height={toDomPrecision(textHandleHeight)}
|
||||
/>
|
||||
<rect
|
||||
data-wd="selection.text-resize.right.handle"
|
||||
data-testid="selection.text-resize.right.handle"
|
||||
className="tl-text-handle"
|
||||
aria-label="bottom_left handle"
|
||||
rx={size / 4}
|
||||
|
@ -433,7 +437,7 @@ export const RotateCornerHandle = function RotateCornerHandle({
|
|||
corner,
|
||||
cursor,
|
||||
isHidden,
|
||||
'data-wd': dataWd,
|
||||
'data-testid': testId,
|
||||
}: {
|
||||
cx: number
|
||||
cy: number
|
||||
|
@ -441,13 +445,13 @@ export const RotateCornerHandle = function RotateCornerHandle({
|
|||
corner: RotateCorner
|
||||
cursor?: string
|
||||
isHidden: boolean
|
||||
'data-wd'?: string
|
||||
'data-testid'?: string
|
||||
}) {
|
||||
const events = useSelectionEvents(corner)
|
||||
return (
|
||||
<rect
|
||||
className={classNames('tl-transparent', 'tl-rotate-corner', { 'tl-hidden': isHidden })}
|
||||
data-wd={dataWd}
|
||||
data-testid={testId}
|
||||
aria-label={`${corner} target`}
|
||||
pointerEvents="all"
|
||||
x={toDomPrecision(cx - targetSize * 3)}
|
||||
|
@ -467,20 +471,20 @@ export const MobileRotateHandle = function RotateHandle({
|
|||
cy,
|
||||
size,
|
||||
isHidden,
|
||||
'data-wd': dataWd,
|
||||
'data-testid': testId,
|
||||
}: {
|
||||
cx: number
|
||||
cy: number
|
||||
size: number
|
||||
isHidden: boolean
|
||||
'data-wd'?: string
|
||||
'data-testid'?: string
|
||||
}) {
|
||||
const events = useSelectionEvents('mobile_rotate')
|
||||
|
||||
return (
|
||||
<g>
|
||||
<circle
|
||||
data-wd={dataWd}
|
||||
data-testid={testId}
|
||||
pointerEvents="all"
|
||||
className={classNames('tl-transparent', 'tl-mobile-rotate__bg', { 'tl-hidden': isHidden })}
|
||||
cx={cx}
|
||||
|
|
|
@ -113,7 +113,7 @@ export const ButtonPicker: React_3.MemoExoticComponent<typeof _ButtonPicker>;
|
|||
// @public (undocumented)
|
||||
export interface ButtonPickerProps<T extends TLStyleItem> {
|
||||
// (undocumented)
|
||||
'data-wd'?: string;
|
||||
'data-testid'?: string;
|
||||
// (undocumented)
|
||||
columns?: 2 | 3 | 4;
|
||||
// (undocumented)
|
||||
|
@ -557,7 +557,7 @@ export function Slider(props: SliderProps): JSX.Element;
|
|||
// @public (undocumented)
|
||||
export interface SliderProps {
|
||||
// (undocumented)
|
||||
'data-wd'?: string;
|
||||
'data-testid'?: string;
|
||||
// (undocumented)
|
||||
label: string;
|
||||
// (undocumented)
|
||||
|
@ -597,9 +597,9 @@ export type SubMenu = {
|
|||
};
|
||||
|
||||
// @public (undocumented)
|
||||
function SubTrigger({ label, 'data-wd': dataWd, 'data-direction': dataDirection, }: {
|
||||
function SubTrigger({ label, 'data-testid': testId, 'data-direction': dataDirection, }: {
|
||||
label: TLTranslationKey;
|
||||
'data-wd'?: string;
|
||||
'data-testid'?: string;
|
||||
'data-direction'?: 'left' | 'right';
|
||||
}): JSX.Element;
|
||||
|
||||
|
@ -840,9 +840,9 @@ export interface TranslationProviderProps {
|
|||
}
|
||||
|
||||
// @public (undocumented)
|
||||
function Trigger({ children, 'data-wd': dataWd }: {
|
||||
function Trigger({ children, 'data-testid': testId, }: {
|
||||
children: any;
|
||||
'data-wd'?: string;
|
||||
'data-testid'?: string;
|
||||
}): JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
|
|
|
@ -26,7 +26,7 @@ export const ActionsMenu = memo(function ActionsMenu() {
|
|||
<Button
|
||||
key={id}
|
||||
className="tlui-button-grid__button"
|
||||
data-wd={`menu-item.${item.id}`}
|
||||
data-testid={`menu-item.${item.id}`}
|
||||
icon={icon}
|
||||
title={
|
||||
label
|
||||
|
@ -50,7 +50,7 @@ export const ActionsMenu = memo(function ActionsMenu() {
|
|||
<PopoverTrigger>
|
||||
<Button
|
||||
className="tlui-menu__trigger"
|
||||
data-wd="main.action-menu"
|
||||
data-testid="main.action-menu"
|
||||
icon="dots-vertical"
|
||||
title={msg('actions-menu.title')}
|
||||
smallIcon
|
||||
|
|
|
@ -84,7 +84,7 @@ function ContextMenuContent() {
|
|||
className={classNames('tlui-menu__group', {
|
||||
'tlui-menu__group__small': parent?.type === 'submenu',
|
||||
})}
|
||||
data-wd={`menu-item.${item.id}`}
|
||||
data-testid={`menu-item.${item.id}`}
|
||||
key={item.id}
|
||||
>
|
||||
{item.children.map((child) => getContextMenuItem(app, child, item, depth + 1))}
|
||||
|
@ -98,7 +98,7 @@ function ContextMenuContent() {
|
|||
<Button
|
||||
className="tlui-menu__button"
|
||||
label={item.label}
|
||||
data-wd={`menu-item.${item.id}`}
|
||||
data-testid={`menu-item.${item.id}`}
|
||||
icon="chevron-right"
|
||||
/>
|
||||
</_ContextMenu.SubTrigger>
|
||||
|
@ -152,7 +152,7 @@ function ContextMenuContent() {
|
|||
<_ContextMenu.Item key={id} dir="ltr" asChild>
|
||||
<Button
|
||||
className="tlui-menu__button"
|
||||
data-wd={`menu-item.${id}`}
|
||||
data-testid={`menu-item.${id}`}
|
||||
kbd={kbd}
|
||||
label={labelToUse}
|
||||
disabled={item.disabled}
|
||||
|
|
|
@ -18,7 +18,7 @@ export const Menu = React.memo(function Menu() {
|
|||
<M.Trigger>
|
||||
<Button
|
||||
className="tlui-menu__trigger"
|
||||
data-wd="main.menu"
|
||||
data-testid="main.menu"
|
||||
title={msg('menu.title')}
|
||||
icon="menu"
|
||||
/>
|
||||
|
@ -71,7 +71,7 @@ function MenuContent() {
|
|||
|
||||
return (
|
||||
<M.Sub id={`main menu ${parent ? parent.id + ' ' : ''}${item.id}`} key={item.id}>
|
||||
<M.SubTrigger label={item.label} data-wd={`menu-item.${item.id}`} />
|
||||
<M.SubTrigger label={item.label} data-testid={`menu-item.${item.id}`} />
|
||||
<M.SubContent sideOffset={-4} alignOffset={-1}>
|
||||
{item.children.map((child) => getMenuItem(app, child, item, depth + 1))}
|
||||
</M.SubContent>
|
||||
|
@ -105,7 +105,7 @@ function MenuContent() {
|
|||
return (
|
||||
<M.Item
|
||||
key={id}
|
||||
data-wd={`menu-item.${item.id}`}
|
||||
data-testid={`menu-item.${item.id}`}
|
||||
kbd={kbd}
|
||||
label={labelToUse}
|
||||
onClick={() => onSelect('menu')}
|
||||
|
|
|
@ -39,8 +39,8 @@ export function MobileStylePanel() {
|
|||
<Popover id="style menu" onOpenChange={handleStylesOpenChange}>
|
||||
<PopoverTrigger disabled={disableStylePanel}>
|
||||
<Button
|
||||
className="tlui-toolbar__tools__button tlui-toolbar__styles__button tlui-popover__trigger"
|
||||
data-wd="mobile.styles"
|
||||
className="tlui-toolbar__tools__button tlui-toolbar__styles__button"
|
||||
data-testid="mobile.styles"
|
||||
style={{ color: currentColor ?? 'var(--color-text)' }}
|
||||
title={msg('style-panel.title')}
|
||||
>
|
||||
|
|
|
@ -19,7 +19,7 @@ export const MoveToPageMenu = track(function MoveToPageMenu() {
|
|||
<Button
|
||||
className="tlui-menu__button"
|
||||
label="context-menu.move-to-page"
|
||||
data-wd="menu-item.move-to-page"
|
||||
data-testid="menu-item.move-to-page"
|
||||
icon="chevron-right"
|
||||
/>
|
||||
</_ContextMenu.SubTrigger>
|
||||
|
@ -28,7 +28,7 @@ export const MoveToPageMenu = track(function MoveToPageMenu() {
|
|||
<_ContextMenu.Group
|
||||
dir="ltr"
|
||||
className={'tlui-menu__group'}
|
||||
data-wd={`menu-item.pages`}
|
||||
data-testid={`menu-item.pages`}
|
||||
key="pages"
|
||||
>
|
||||
{pages.map((page) => (
|
||||
|
@ -72,7 +72,7 @@ export const MoveToPageMenu = track(function MoveToPageMenu() {
|
|||
<_ContextMenu.Group
|
||||
dir="ltr"
|
||||
className={'tlui-menu__group'}
|
||||
data-wd={`menu-item.new-page`}
|
||||
data-testid={`menu-item.new-page`}
|
||||
key="new-page"
|
||||
>
|
||||
<_ContextMenu.Item
|
||||
|
|
|
@ -35,7 +35,7 @@ export const NavigationZone = memo(function NavigationZone() {
|
|||
<Button
|
||||
title={msg('navigation-zone.toggle-minimap')}
|
||||
className="tlui-navigation-zone__toggle"
|
||||
data-wd="minimap.toggle"
|
||||
data-testid="minimap.toggle"
|
||||
onClick={toggleMinimap}
|
||||
icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'}
|
||||
/>
|
||||
|
@ -44,14 +44,14 @@ export const NavigationZone = memo(function NavigationZone() {
|
|||
<>
|
||||
<Button
|
||||
icon="minus"
|
||||
data-wd="minimap.zoom-out"
|
||||
data-testid="minimap.zoom-out"
|
||||
title={`${msg(actions['zoom-out'].label!)} ${kbdStr(actions['zoom-out'].kbd!)}`}
|
||||
onClick={() => actions['zoom-out'].onSelect('navigation-zone')}
|
||||
/>
|
||||
<ZoomMenu />
|
||||
<Button
|
||||
icon="plus"
|
||||
data-wd="minimap.zoom-in"
|
||||
data-testid="minimap.zoom-in"
|
||||
title={`${msg(actions['zoom-in'].label!)} ${kbdStr(actions['zoom-in'].kbd!)}`}
|
||||
onClick={() => actions['zoom-in'].onSelect('navigation-zone')}
|
||||
/>
|
||||
|
|
|
@ -26,7 +26,7 @@ export const ZoomMenu = track(function ZoomMenu() {
|
|||
<M.Trigger>
|
||||
<Button
|
||||
title={`${msg('navigation-zone.zoom')}`}
|
||||
data-wd="minimap.zoom-menu"
|
||||
data-testid="minimap.zoom-menu"
|
||||
className={breakpoint < 5 ? 'tlui-zoom-menu__button' : 'tlui-zoom-menu__button__pct'}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
icon={breakpoint < 4 ? 'zoom-in' : undefined}
|
||||
|
@ -38,24 +38,24 @@ export const ZoomMenu = track(function ZoomMenu() {
|
|||
</M.Trigger>
|
||||
<M.Content side="top" align="start" alignOffset={0}>
|
||||
<M.Group>
|
||||
<ZoomMenuItem action="zoom-in" data-wd="minimap.zoom-menu.zoom-in" noClose />
|
||||
<ZoomMenuItem action="zoom-out" data-wd="minimap.zoom-menu.zoom-out" noClose />
|
||||
<ZoomMenuItem action="zoom-in" data-testid="minimap.zoom-menu.zoom-in" noClose />
|
||||
<ZoomMenuItem action="zoom-out" data-testid="minimap.zoom-menu.zoom-out" noClose />
|
||||
<ZoomMenuItem
|
||||
action="zoom-to-100"
|
||||
data-wd="minimap.zoom-menu.zoom-to-100"
|
||||
data-testid="minimap.zoom-menu.zoom-to-100"
|
||||
noClose
|
||||
disabled={isZoomedTo100}
|
||||
/>
|
||||
<ZoomMenuItem
|
||||
action="zoom-to-fit"
|
||||
disabled={!hasShapes}
|
||||
data-wd="minimap.zoom-menu.zoom-to-fit"
|
||||
data-testid="minimap.zoom-menu.zoom-to-fit"
|
||||
noClose
|
||||
/>
|
||||
<ZoomMenuItem
|
||||
action="zoom-to-selection"
|
||||
disabled={!hasSelected}
|
||||
data-wd="minimap.zoom-menu.zoom-to-selection"
|
||||
data-testid="minimap.zoom-menu.zoom-to-selection"
|
||||
noClose
|
||||
/>
|
||||
</M.Group>
|
||||
|
@ -68,7 +68,7 @@ function ZoomMenuItem(props: {
|
|||
action: string
|
||||
disabled?: boolean
|
||||
noClose?: boolean
|
||||
'data-wd'?: string
|
||||
'data-testid'?: string
|
||||
}) {
|
||||
const { action, disabled = false, noClose = false } = props
|
||||
const actions = useActions()
|
||||
|
@ -77,7 +77,7 @@ function ZoomMenuItem(props: {
|
|||
<M.Item
|
||||
label={actions[action].label}
|
||||
kbd={actions[action].kbd}
|
||||
data-wd={props['data-wd']}
|
||||
data-testid={props['data-testid']}
|
||||
onClick={() => actions[action].onSelect('zoom-menu')}
|
||||
noClose={noClose}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -75,7 +75,7 @@ export const PageMenu = function PageMenu() {
|
|||
if (!isOpen) return
|
||||
requestAnimationFrame(() => {
|
||||
const elm = document.querySelector(
|
||||
`[data-wd="page-menu-item-${currentPage.id}"]`
|
||||
`[data-testid="page-menu-item-${currentPage.id}"]`
|
||||
) as HTMLDivElement
|
||||
|
||||
if (elm) {
|
||||
|
@ -240,7 +240,7 @@ export const PageMenu = function PageMenu() {
|
|||
<PopoverTrigger>
|
||||
<Button
|
||||
className="tlui-page-menu__trigger tlui-menu__trigger"
|
||||
data-wd="main.page-menu"
|
||||
data-testid="main.page-menu"
|
||||
icon="chevron-down"
|
||||
title={currentPage.name}
|
||||
>
|
||||
|
@ -254,13 +254,13 @@ export const PageMenu = function PageMenu() {
|
|||
{!isReadonlyMode && (
|
||||
<>
|
||||
<Button
|
||||
data-wd="page-menu.edit"
|
||||
data-testid="page-menu.edit"
|
||||
title={msg(isEditing ? 'page-menu.edit-done' : 'page-menu.edit-start')}
|
||||
icon={isEditing ? 'check' : 'edit'}
|
||||
onClick={toggleEditing}
|
||||
/>
|
||||
<Button
|
||||
data-wd="page-menu.create"
|
||||
data-testid="page-menu.create"
|
||||
icon="plus"
|
||||
title={msg(
|
||||
maxPageCountReached
|
||||
|
@ -287,7 +287,7 @@ export const PageMenu = function PageMenu() {
|
|||
return isEditing ? (
|
||||
<div
|
||||
key={page.id + '_editing'}
|
||||
data-wd={`page-menu-item-${page.id}`}
|
||||
data-testid={`page-menu-item-${page.id}`}
|
||||
className="tlui-page_menu__item__sortable"
|
||||
style={{
|
||||
zIndex: page.id === currentPage.id ? 888 : index,
|
||||
|
@ -326,7 +326,7 @@ export const PageMenu = function PageMenu() {
|
|||
) : (
|
||||
<div
|
||||
id={`page-menu-item-${page.id}`}
|
||||
data-wd={`page-menu-item-${page.id}`}
|
||||
data-testid={`page-menu-item-${page.id}`}
|
||||
className="tlui-page_menu__item__sortable__title"
|
||||
style={{ height: ITEM_HEIGHT }}
|
||||
>
|
||||
|
@ -346,7 +346,7 @@ export const PageMenu = function PageMenu() {
|
|||
) : (
|
||||
<div
|
||||
key={page.id}
|
||||
data-wd={`page-menu-item-${page.id}`}
|
||||
data-testid={`page-menu-item-${page.id}`}
|
||||
className="tlui-page-menu__item"
|
||||
>
|
||||
<Button
|
||||
|
|
|
@ -14,7 +14,7 @@ export const RedoButton = memo(function RedoButton() {
|
|||
|
||||
return (
|
||||
<Button
|
||||
data-wd="main.redo"
|
||||
data-testid="main.redo"
|
||||
icon={redo.icon}
|
||||
title={`${msg(redo.label!)} ${kbdStr(redo.kbd!)}`}
|
||||
disabled={!canRedo}
|
||||
|
|
|
@ -21,13 +21,13 @@ interface DoubleDropdownPickerProps<T extends AllStyles[keyof AllStyles][number]
|
|||
valueA: T['id'] | null | undefined
|
||||
valueB: T['id'] | null | undefined
|
||||
onValueChange: (value: TLStyleItem, squashing: boolean) => void
|
||||
'data-wd'?: string
|
||||
'data-testid'?: string
|
||||
}
|
||||
|
||||
export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<
|
||||
T extends AllStyles[keyof AllStyles][number]
|
||||
>({
|
||||
'data-wd': dataWd,
|
||||
'data-testid': testId,
|
||||
label,
|
||||
labelA,
|
||||
labelB,
|
||||
|
@ -52,8 +52,8 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<
|
|||
|
||||
if (valueA === undefined && valueB === undefined) return null
|
||||
|
||||
const startWdPrefix = `${dataWd}.start`
|
||||
const endWdPrefix = `${dataWd}.end`
|
||||
const startWdPrefix = `${testId}.start`
|
||||
const endWdPrefix = `${testId}.end`
|
||||
|
||||
return (
|
||||
<div className="tlui-style-panel__double-select-picker">
|
||||
|
@ -63,7 +63,7 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<
|
|||
<DropdownMenu.Root id={`style panel ${styleTypeA}`}>
|
||||
<Trigger asChild>
|
||||
<Button
|
||||
data-wd={startWdPrefix}
|
||||
data-testid={startWdPrefix}
|
||||
title={
|
||||
msg(labelA) +
|
||||
' — ' +
|
||||
|
@ -90,7 +90,7 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<
|
|||
title={
|
||||
msg(labelA) + ' — ' + msg(`${styleTypeA}-style.${item.id}` as TLTranslationKey)
|
||||
}
|
||||
data-wd={`${startWdPrefix}.${item.id}`}
|
||||
data-testid={`${startWdPrefix}.${item.id}`}
|
||||
key={item.id}
|
||||
icon={item.icon as TLUiIconType}
|
||||
onClick={() => onValueChange(item as TLStyleItem, false)}
|
||||
|
@ -104,7 +104,7 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<
|
|||
<DropdownMenu.Root id={`style panel ${styleTypeB}`}>
|
||||
<Trigger asChild>
|
||||
<Button
|
||||
data-wd={endWdPrefix}
|
||||
data-testid={endWdPrefix}
|
||||
title={
|
||||
msg(labelB) +
|
||||
' — ' +
|
||||
|
@ -130,7 +130,7 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<
|
|||
title={
|
||||
msg(labelB) + ' — ' + msg(`${styleTypeB}-style.${item.id}` as TLTranslationKey)
|
||||
}
|
||||
data-wd={`${endWdPrefix}.${item.id}`}
|
||||
data-testid={`${endWdPrefix}.${item.id}`}
|
||||
key={item.id}
|
||||
icon={item.icon as TLUiIconType}
|
||||
onClick={() => onValueChange(item as TLStyleItem, false)}
|
||||
|
|
|
@ -16,7 +16,7 @@ interface DropdownPickerProps<T extends AllStyles[keyof AllStyles][number]> {
|
|||
items: T[]
|
||||
styleType: TLStyleType
|
||||
value: T['id'] | null
|
||||
'data-wd'?: string
|
||||
'data-testid'?: string
|
||||
onValueChange: (value: TLStyleItem, squashing: boolean) => void
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ export const DropdownPicker = React.memo(function DropdownPicker<
|
|||
label,
|
||||
value,
|
||||
onValueChange,
|
||||
'data-wd': dataWd,
|
||||
'data-testid': testId,
|
||||
}: DropdownPickerProps<T>) {
|
||||
const msg = useTranslation()
|
||||
|
||||
|
@ -39,7 +39,7 @@ export const DropdownPicker = React.memo(function DropdownPicker<
|
|||
<DropdownMenu.Root id={`style panel ${id}`}>
|
||||
<Trigger asChild>
|
||||
<Button
|
||||
data-wd={dataWd}
|
||||
data-testid={testId}
|
||||
title={
|
||||
value === null
|
||||
? msg('style-panel.mixed')
|
||||
|
@ -61,7 +61,7 @@ export const DropdownPicker = React.memo(function DropdownPicker<
|
|||
return (
|
||||
<DropdownMenu.Item
|
||||
className="tlui-button-grid__button"
|
||||
data-wd={`${dataWd}.${item.id}`}
|
||||
data-testid={`${testId}.${item.id}`}
|
||||
title={msg(`${styleType}-style.${item.id}` as TLTranslationKey)}
|
||||
key={item.id}
|
||||
icon={item.icon as TLUiIconType}
|
||||
|
|
|
@ -103,7 +103,7 @@ function CommonStylePickerSet({ props }: { props: TLNullableShapeProps }) {
|
|||
<ButtonPicker
|
||||
title={msg('style-panel.color')}
|
||||
styleType="color"
|
||||
data-wd="style.color"
|
||||
data-testid="style.color"
|
||||
items={styles.color}
|
||||
value={color}
|
||||
onValueChange={handleValueChange}
|
||||
|
@ -111,7 +111,7 @@ function CommonStylePickerSet({ props }: { props: TLNullableShapeProps }) {
|
|||
)}
|
||||
{opacity === undefined ? null : (
|
||||
<Slider
|
||||
data-wd="style.opacity"
|
||||
data-testid="style.opacity"
|
||||
value={opacityIndex >= 0 ? opacityIndex : styles.opacity.length - 1}
|
||||
label={opacity ? `opacity-style.${opacity}` : 'style-panel.mixed'}
|
||||
onValueChange={handleOpacityValueChange}
|
||||
|
@ -126,7 +126,7 @@ function CommonStylePickerSet({ props }: { props: TLNullableShapeProps }) {
|
|||
<ButtonPicker
|
||||
title={msg('style-panel.fill')}
|
||||
styleType="fill"
|
||||
data-wd="style.fill"
|
||||
data-testid="style.fill"
|
||||
items={styles.fill}
|
||||
value={fill}
|
||||
onValueChange={handleValueChange}
|
||||
|
@ -136,7 +136,7 @@ function CommonStylePickerSet({ props }: { props: TLNullableShapeProps }) {
|
|||
<ButtonPicker
|
||||
title={msg('style-panel.dash')}
|
||||
styleType="dash"
|
||||
data-wd="style.dash"
|
||||
data-testid="style.dash"
|
||||
items={styles.dash}
|
||||
value={dash}
|
||||
onValueChange={handleValueChange}
|
||||
|
@ -146,7 +146,7 @@ function CommonStylePickerSet({ props }: { props: TLNullableShapeProps }) {
|
|||
<ButtonPicker
|
||||
title={msg('style-panel.size')}
|
||||
styleType="size"
|
||||
data-wd="style.size"
|
||||
data-testid="style.size"
|
||||
items={styles.size}
|
||||
value={size}
|
||||
onValueChange={handleValueChange}
|
||||
|
@ -173,7 +173,7 @@ function TextStylePickerSet({ props }: { props: TLNullableShapeProps }) {
|
|||
<ButtonPicker
|
||||
title={msg('style-panel.font')}
|
||||
styleType="font"
|
||||
data-wd="font"
|
||||
data-testid="font"
|
||||
items={styles.font}
|
||||
value={font}
|
||||
onValueChange={handleValueChange}
|
||||
|
@ -185,7 +185,7 @@ function TextStylePickerSet({ props }: { props: TLNullableShapeProps }) {
|
|||
<ButtonPicker
|
||||
title={msg('style-panel.align')}
|
||||
styleType="align"
|
||||
data-wd="align"
|
||||
data-testid="align"
|
||||
items={styles.align}
|
||||
value={align}
|
||||
onValueChange={handleValueChange}
|
||||
|
@ -193,7 +193,7 @@ function TextStylePickerSet({ props }: { props: TLNullableShapeProps }) {
|
|||
{verticalAlign === undefined ? (
|
||||
<Button
|
||||
title={msg('style-panel.vertical-align')}
|
||||
data-wd="vertical-align"
|
||||
data-testid="vertical-align"
|
||||
icon="vertical-align-center"
|
||||
disabled
|
||||
/>
|
||||
|
@ -201,7 +201,7 @@ function TextStylePickerSet({ props }: { props: TLNullableShapeProps }) {
|
|||
<DropdownPicker
|
||||
id="geo-vertical-alignment"
|
||||
styleType="verticalAlign"
|
||||
data-wd="style-panel.geo-vertical-align"
|
||||
data-testid="style-panel.geo-vertical-align"
|
||||
items={styles.verticalAlign}
|
||||
value={verticalAlign}
|
||||
onValueChange={handleValueChange}
|
||||
|
@ -226,7 +226,7 @@ function GeoStylePickerSet({ props }: { props: TLNullableShapeProps }) {
|
|||
id="geo"
|
||||
label={'style-panel.geo'}
|
||||
styleType="geo"
|
||||
data-wd="style-panel.geo"
|
||||
data-testid="style-panel.geo"
|
||||
items={styles.geo}
|
||||
value={geo}
|
||||
onValueChange={handleValueChange}
|
||||
|
@ -247,7 +247,7 @@ function SplineStylePickerSet({ props }: { props: TLNullableShapeProps }) {
|
|||
id="spline"
|
||||
label={'style-panel.spline'}
|
||||
styleType="spline"
|
||||
data-wd="style.spline"
|
||||
data-testid="style.spline"
|
||||
items={styles.spline}
|
||||
value={spline}
|
||||
onValueChange={handleValueChange}
|
||||
|
@ -267,7 +267,7 @@ function ArrowheadStylePickerSet({ props }: { props: TLNullableShapeProps }) {
|
|||
<DoubleDropdownPicker
|
||||
label={'style-panel.arrowheads'}
|
||||
styleTypeA="arrowheadStart"
|
||||
data-wd="style.arrowheads"
|
||||
data-testid="style.arrowheads"
|
||||
itemsA={styles.arrowheadStart}
|
||||
valueA={arrowheadStart}
|
||||
styleTypeB="arrowheadEnd"
|
||||
|
|
|
@ -193,7 +193,7 @@ export const Toolbar = function Toolbar() {
|
|||
<Button
|
||||
className="tlui-toolbar__tools__button tlui-toolbar__overflow"
|
||||
icon="chevron-up"
|
||||
data-wd="tools.more"
|
||||
data-testid="tools.more"
|
||||
title={msg('tool-panel.more')}
|
||||
/>
|
||||
</M.Trigger>
|
||||
|
@ -231,7 +231,7 @@ const OverflowToolsContent = track(function OverflowToolsContent({
|
|||
<M.Item
|
||||
key={id}
|
||||
className="tlui-button-grid__button"
|
||||
data-wd={`tools.${id}`}
|
||||
data-testid={`tools.${id}`}
|
||||
data-tool={id}
|
||||
data-geo={meta?.geo ?? ''}
|
||||
aria-label={label}
|
||||
|
@ -257,7 +257,7 @@ function ToolbarButton({
|
|||
return (
|
||||
<Button
|
||||
className="tlui-toolbar__tools__button"
|
||||
data-wd={`tools.${item.id}`}
|
||||
data-testid={`tools.${item.id}`}
|
||||
data-tool={item.id}
|
||||
data-geo={item.meta?.geo ?? ''}
|
||||
aria-label={item.label}
|
||||
|
|
|
@ -14,7 +14,7 @@ export const UndoButton = memo(function UndoButton() {
|
|||
|
||||
return (
|
||||
<Button
|
||||
data-wd="main.undo"
|
||||
data-testid="main.undo"
|
||||
icon={undo.icon}
|
||||
title={`${msg(undo.label!)} ${kbdStr(undo.kbd!)}`}
|
||||
disabled={!canUndo}
|
||||
|
|
|
@ -15,7 +15,7 @@ export interface ButtonPickerProps<T extends TLStyleItem> {
|
|||
styleType: TLStyleType
|
||||
value?: string | number | null
|
||||
columns?: 2 | 3 | 4
|
||||
'data-wd'?: string
|
||||
'data-testid'?: string
|
||||
onValueChange: (item: T, squashing: boolean) => void
|
||||
}
|
||||
|
||||
|
@ -94,7 +94,7 @@ function _ButtonPicker<T extends TLStyleItem>(props: ButtonPickerProps<T>) {
|
|||
<Button
|
||||
key={item.id}
|
||||
data-id={item.id}
|
||||
data-wd={`${props['data-wd']}.${item.id}`}
|
||||
data-testid={`${props['data-testid']}.${item.id}`}
|
||||
aria-label={item.id}
|
||||
data-state={value === item.id ? 'hinted' : undefined}
|
||||
title={title + ' — ' + msg(`${styleType}-style.${item.id}` as TLTranslationKey)}
|
||||
|
|
|
@ -21,7 +21,7 @@ export function Title({ className, children }: { className?: string; children: a
|
|||
export function CloseButton() {
|
||||
return (
|
||||
<div className="tlui-dialog__header__close">
|
||||
<_Dialog.DialogClose data-wd="dialog.close" dir="ltr" asChild>
|
||||
<_Dialog.DialogClose data-testid="dialog.close" dir="ltr" asChild>
|
||||
<Button aria-label="Close" onTouchEnd={(e) => (e.target as HTMLButtonElement).click()}>
|
||||
<Icon small icon="cross-2" />
|
||||
</Button>
|
||||
|
|
|
@ -25,9 +25,15 @@ export function Root({
|
|||
}
|
||||
|
||||
/** @public */
|
||||
export function Trigger({ children, 'data-wd': dataWd }: { children: any; 'data-wd'?: string }) {
|
||||
export function Trigger({
|
||||
children,
|
||||
'data-testid': testId,
|
||||
}: {
|
||||
children: any
|
||||
'data-testid'?: string
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenu.Trigger dir="ltr" data-wd={dataWd} asChild>
|
||||
<DropdownMenu.Trigger dir="ltr" data-testid={testId} asChild>
|
||||
{children}
|
||||
</DropdownMenu.Trigger>
|
||||
)
|
||||
|
@ -79,15 +85,15 @@ export function Sub({ id, children }: { id: string; children: any }) {
|
|||
/** @public */
|
||||
export function SubTrigger({
|
||||
label,
|
||||
'data-wd': dataWd,
|
||||
'data-testid': testId,
|
||||
'data-direction': dataDirection,
|
||||
}: {
|
||||
label: TLTranslationKey
|
||||
'data-wd'?: string
|
||||
'data-testid'?: string
|
||||
'data-direction'?: 'left' | 'right'
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenu.SubTrigger dir="ltr" data-direction={dataDirection} data-wd={dataWd} asChild>
|
||||
<DropdownMenu.SubTrigger dir="ltr" data-direction={dataDirection} data-testid={testId} asChild>
|
||||
<Button
|
||||
className="tlui-menu__button tlui-menu__submenu__trigger"
|
||||
label={label}
|
||||
|
|
|
@ -24,10 +24,10 @@ export const PopoverTrigger: FC<{
|
|||
children: React.ReactNode
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
'data-wd'?: string
|
||||
}> = ({ children, disabled, 'data-wd': dataWd }) => {
|
||||
'data-testid'?: string
|
||||
}> = ({ children, disabled, 'data-testid': testId }) => {
|
||||
return (
|
||||
<PopoverPrimitive.Trigger data-wd={dataWd} disabled={disabled} asChild dir="ltr">
|
||||
<PopoverPrimitive.Trigger data-testid={testId} disabled={disabled} asChild dir="ltr">
|
||||
{children}
|
||||
</PopoverPrimitive.Trigger>
|
||||
)
|
||||
|
|
|
@ -11,7 +11,7 @@ export interface SliderProps {
|
|||
label: string
|
||||
title: string
|
||||
onValueChange: (value: number, emphemeral: boolean) => void
|
||||
'data-wd'?: string
|
||||
'data-testid'?: string
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -39,7 +39,7 @@ export function Slider(props: SliderProps) {
|
|||
return (
|
||||
<div className="tlui-slider__container">
|
||||
<Root
|
||||
data-wd={props['data-wd']}
|
||||
data-testid={props['data-testid']}
|
||||
className="tlui-slider"
|
||||
area-label="Opacity"
|
||||
dir="ltr"
|
||||
|
|
|
@ -470,6 +470,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.distribute-horizontal',
|
||||
contextMenuLabel: 'action.distribute-horizontal.short',
|
||||
icon: 'distribute-horizontal',
|
||||
kbd: '?!h',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
trackEvent('distribute-shapes', { operation: 'horizontal', source })
|
||||
|
@ -482,6 +483,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.distribute-vertical',
|
||||
contextMenuLabel: 'action.distribute-vertical.short',
|
||||
icon: 'distribute-vertical',
|
||||
kbd: '?!V',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
trackEvent('distribute-shapes', { operation: 'vertical', source })
|
||||
|
|
|
@ -493,7 +493,9 @@ const handleNativeOrMenuCopy = (app: App) => {
|
|||
})
|
||||
)
|
||||
|
||||
if (typeof window?.navigator !== 'undefined') {
|
||||
if (typeof navigator === 'undefined') {
|
||||
return
|
||||
} else {
|
||||
// Extract the text from the clipboard
|
||||
const textItems = content.shapes
|
||||
.map((shape) => {
|
||||
|
|
4451
public-yarn.lock
4451
public-yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue