snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
44
apps/examples/e2e-device/helpers/app.ts
Normal file
44
apps/examples/e2e-device/helpers/app.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { Editor } from 'tldraw'
|
||||
|
||||
declare const editor: Editor
|
||||
|
||||
const NATIVE_CONTEXT = 'NATIVE_APP'
|
||||
|
||||
/**
|
||||
* Open the `/end-to-end` example (which exposes `window.editor`), wait for the
|
||||
* canvas, and reset to a known state: no animations, zoom at 1, no shapes.
|
||||
*/
|
||||
export async function openEditor() {
|
||||
const webContext = await currentWebContext()
|
||||
await browser.switchContext(webContext)
|
||||
|
||||
if (!(await browser.getUrl()).includes('end-to-end')) {
|
||||
await browser.url(`${browser.options.baseUrl}/end-to-end`)
|
||||
}
|
||||
await browser.$('.tl-canvas').waitForExist({ timeout: 30_000 })
|
||||
|
||||
await browser.execute(() => {
|
||||
editor.user.updateUserPreferences({ animationSpeed: 0 })
|
||||
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
|
||||
editor.setCurrentTool('select')
|
||||
editor.resetZoom()
|
||||
})
|
||||
}
|
||||
|
||||
/** Read the current zoom level from the live editor. */
|
||||
export function getZoom(): Promise<number> {
|
||||
return browser.execute(() => editor.getZoomLevel())
|
||||
}
|
||||
|
||||
/** Read the currently selected shape ids. */
|
||||
export function getSelectedShapeIds(): Promise<string[]> {
|
||||
return browser.execute(() => editor.getSelectedShapeIds())
|
||||
}
|
||||
|
||||
/** The webview context id changes per platform/run; find it rather than hardcode. */
|
||||
export async function currentWebContext(): Promise<string> {
|
||||
const contexts = (await browser.getContexts()) as string[]
|
||||
const web = contexts.find((c) => c !== NATIVE_CONTEXT)
|
||||
if (!web) throw new Error(`No web context found. Available: ${contexts.join(', ')}`)
|
||||
return web
|
||||
}
|
||||
118
apps/examples/e2e-device/helpers/pinch.ts
Normal file
118
apps/examples/e2e-device/helpers/pinch.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { currentWebContext } from './app'
|
||||
|
||||
const NATIVE_CONTEXT = 'NATIVE_APP'
|
||||
|
||||
/**
|
||||
* Two-finger pinch over the tldraw canvas.
|
||||
*
|
||||
* Why this is the way it is: on both iOS (XCUITest) and Android (UiAutomator2),
|
||||
* multi-touch W3C actions only operate in the NATIVE context, and only on native
|
||||
* screen coordinates. So we:
|
||||
*
|
||||
* 1. (web context) measure the canvas rect + viewport in CSS pixels,
|
||||
* 2. (native context) translate those to device coordinates and run two
|
||||
* simultaneous touch pointers from the centre outwards (or inwards),
|
||||
* 3. (web context) hand back so the caller can assert on `editor` state.
|
||||
*
|
||||
* The touches enter through the OS, so Safari/Chrome's own gesture recogniser
|
||||
* sees them — that is the whole point, and what synthetic DOM touch can't do.
|
||||
*
|
||||
* @param scale >1 spreads the fingers apart (zoom in), <1 brings them together
|
||||
* (zoom out).
|
||||
*/
|
||||
export async function pinchCanvas({ scale, steps = 12 }: { scale: number; steps?: number }) {
|
||||
const web = await currentWebContext()
|
||||
|
||||
// 1. Measure in the web context (CSS pixels relative to the layout viewport).
|
||||
await browser.switchContext(web)
|
||||
const rect = await browser.execute(() => {
|
||||
const el = document.querySelector('.tl-canvas')!
|
||||
const r = el.getBoundingClientRect()
|
||||
return {
|
||||
cx: r.left + r.width / 2,
|
||||
cy: r.top + r.height / 2,
|
||||
dpr: window.devicePixelRatio,
|
||||
// Offset of the web viewport inside the native window (browser chrome).
|
||||
// visualViewport.offsetTop is the cleanest signal we have for this.
|
||||
offsetTop: window.visualViewport?.offsetTop ?? 0,
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Translate CSS px -> native device coordinates.
|
||||
//
|
||||
// iOS reports native coordinates in POINTS (already CSS-px-equivalent), so the
|
||||
// scale factor is 1 and we only add the top inset for the Safari toolbar.
|
||||
// Android UiAutomator2 reports PIXELS, so we multiply by devicePixelRatio.
|
||||
//
|
||||
// `TOP_INSET` is the one bit that may need a per-device tweak — the height of
|
||||
// the browser chrome above the web viewport. Calibrate once per device class.
|
||||
const isAndroid = browser.isAndroid
|
||||
const pxScale = isAndroid ? rect.dpr : 1
|
||||
const TOP_INSET = Number(process.env.DEVICE_TOP_INSET ?? (isAndroid ? 0 : 0))
|
||||
|
||||
const cx = rect.cx * pxScale
|
||||
const cy = (rect.cy + rect.offsetTop) * pxScale + TOP_INSET
|
||||
|
||||
// Fingers start 60px either side of centre, end at 60*scale either side.
|
||||
const startGap = 60 * pxScale
|
||||
const endGap = startGap * scale
|
||||
|
||||
const finger = (id: string, dir: -1 | 1) =>
|
||||
buildPinchPointer({
|
||||
id,
|
||||
cx,
|
||||
cy,
|
||||
startGap: dir * startGap,
|
||||
endGap: dir * endGap,
|
||||
steps,
|
||||
})
|
||||
|
||||
// 3. Perform the gesture natively, then return to the web context.
|
||||
await browser.switchContext(NATIVE_CONTEXT)
|
||||
await browser.performActions([finger('finger1', -1), finger('finger2', 1)])
|
||||
await browser.releaseActions()
|
||||
await browser.switchContext(web)
|
||||
}
|
||||
|
||||
/**
|
||||
* One touch pointer that presses at `cx + startGap`, then moves in `steps` hops
|
||||
* to `cx + endGap` along the x axis (so two mirrored pointers spread/contract).
|
||||
*/
|
||||
function buildPinchPointer({
|
||||
id,
|
||||
cx,
|
||||
cy,
|
||||
startGap,
|
||||
endGap,
|
||||
steps,
|
||||
}: {
|
||||
id: string
|
||||
cx: number
|
||||
cy: number
|
||||
startGap: number
|
||||
endGap: number
|
||||
steps: number
|
||||
}) {
|
||||
const actions: Array<Record<string, unknown>> = [
|
||||
{ type: 'pointerMove', duration: 0, x: Math.round(cx + startGap), y: Math.round(cy) },
|
||||
{ type: 'pointerDown', button: 0 },
|
||||
{ type: 'pause', duration: 50 },
|
||||
]
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
const gap = startGap + ((endGap - startGap) * i) / steps
|
||||
actions.push({
|
||||
type: 'pointerMove',
|
||||
duration: 25,
|
||||
x: Math.round(cx + gap),
|
||||
y: Math.round(cy),
|
||||
})
|
||||
}
|
||||
actions.push({ type: 'pointerUp', button: 0 })
|
||||
|
||||
return {
|
||||
type: 'pointer',
|
||||
id,
|
||||
parameters: { pointerType: 'touch' },
|
||||
actions,
|
||||
}
|
||||
}
|
||||
70
apps/examples/e2e-device/helpers/stroke.ts
Normal file
70
apps/examples/e2e-device/helpers/stroke.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { currentWebContext } from './app'
|
||||
|
||||
const NATIVE_CONTEXT = 'NATIVE_APP'
|
||||
|
||||
/**
|
||||
* Draw a single-finger freehand stroke across the canvas through the real OS
|
||||
* touch pipeline (same native-context + coordinate-translation approach as
|
||||
* pinch.ts). `from`/`to` are CSS-pixel offsets relative to the canvas centre.
|
||||
*/
|
||||
export async function drawTouchStroke({
|
||||
from,
|
||||
to,
|
||||
steps = 12,
|
||||
}: {
|
||||
from: { x: number; y: number }
|
||||
to: { x: number; y: number }
|
||||
steps?: number
|
||||
}) {
|
||||
const web = await currentWebContext()
|
||||
|
||||
// Measure in the web context (CSS pixels relative to the layout viewport).
|
||||
await browser.switchContext(web)
|
||||
const rect = await browser.execute(() => {
|
||||
const el = document.querySelector('.tl-canvas')!
|
||||
const r = el.getBoundingClientRect()
|
||||
return {
|
||||
cx: r.left + r.width / 2,
|
||||
cy: r.top + r.height / 2,
|
||||
dpr: window.devicePixelRatio,
|
||||
offsetTop: window.visualViewport?.offsetTop ?? 0,
|
||||
}
|
||||
})
|
||||
|
||||
// CSS px -> native device coordinates (iOS reports points, Android pixels).
|
||||
const isAndroid = browser.isAndroid
|
||||
const pxScale = isAndroid ? rect.dpr : 1
|
||||
const TOP_INSET = Number(process.env.DEVICE_TOP_INSET ?? 0)
|
||||
const cx = rect.cx * pxScale
|
||||
const cy = (rect.cy + rect.offsetTop) * pxScale + TOP_INSET
|
||||
|
||||
const point = (p: { x: number; y: number }) => ({
|
||||
x: Math.round(cx + p.x * pxScale),
|
||||
y: Math.round(cy + p.y * pxScale),
|
||||
})
|
||||
|
||||
const start = point(from)
|
||||
const actions: Array<Record<string, unknown>> = [
|
||||
{ type: 'pointerMove', duration: 0, ...start },
|
||||
{ type: 'pointerDown', button: 0 },
|
||||
{ type: 'pause', duration: 50 },
|
||||
]
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
actions.push({
|
||||
type: 'pointerMove',
|
||||
duration: 25,
|
||||
...point({
|
||||
x: from.x + ((to.x - from.x) * i) / steps,
|
||||
y: from.y + ((to.y - from.y) * i) / steps,
|
||||
}),
|
||||
})
|
||||
}
|
||||
actions.push({ type: 'pointerUp', button: 0 })
|
||||
|
||||
await browser.switchContext(NATIVE_CONTEXT)
|
||||
await browser.performActions([
|
||||
{ type: 'pointer', id: 'finger1', parameters: { pointerType: 'touch' }, actions },
|
||||
])
|
||||
await browser.releaseActions()
|
||||
await browser.switchContext(web)
|
||||
}
|
||||
78
apps/examples/e2e-device/tests/draw-pressure-encoding.e2e.ts
Normal file
78
apps/examples/e2e-device/tests/draw-pressure-encoding.e2e.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { expect } from '@wdio/globals'
|
||||
import type { Editor } from 'tldraw'
|
||||
import { openEditor } from '../helpers/app'
|
||||
import { drawTouchStroke } from '../helpers/stroke'
|
||||
|
||||
declare const editor: Editor
|
||||
|
||||
/**
|
||||
* Guards the device-side contract behind 2D draw-shape encoding (#8963/#8879):
|
||||
* tldraw drops the constant pressure channel (z) from mouse/trackpad/finger
|
||||
* strokes, keyed off `isPenOrStylus` — which is inferred from the pressure the
|
||||
* BROWSER reports for the input device (Drawing.ts). The unit tests pick the
|
||||
* pressure values they feed in, so they verify the decision logic but not what
|
||||
* real engines actually report for a finger.
|
||||
*
|
||||
* That engine contract is the fragile part: 3D Touch iPhones really did report
|
||||
* varying force for plain finger touches. If an engine (re)introduces that,
|
||||
* finger strokes silently flip back to 3D — losing the storage win and making
|
||||
* stroke width track finger pressure — with no unit or Playwright test failing,
|
||||
* because those layers synthesize their own pressure values. Drawing through
|
||||
* the real OS touch pipeline and asserting the 2D encoding is the only
|
||||
* automated check that can catch it.
|
||||
*
|
||||
* Skips (rather than fails) on builds whose schema predates the segment `dim`
|
||||
* field, so the suite stays green until #8963 merges.
|
||||
*/
|
||||
describe('draw shape pressure encoding (#8963)', () => {
|
||||
beforeEach(async () => {
|
||||
await openEditor()
|
||||
})
|
||||
|
||||
it('a finger-drawn stroke uses the 2D (no-pressure) encoding', async function () {
|
||||
// Feature-detect the `dim` field: on builds that predate #8963 the segment
|
||||
// validator rejects it, so probe with a throwaway shape and skip if absent.
|
||||
const dimSupported = await browser.execute(() => {
|
||||
try {
|
||||
editor.createShapes([
|
||||
{
|
||||
id: 'shape:dimprobe',
|
||||
type: 'draw',
|
||||
props: { segments: [{ type: 'free', path: '', dim: 2 }] },
|
||||
},
|
||||
] as any)
|
||||
editor.deleteShapes(['shape:dimprobe'] as any)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
if (!dimSupported) this.skip()
|
||||
|
||||
// Block body: a concise arrow would implicitly return the (circular)
|
||||
// Editor instance, which can't be serialized over the WebDriver wire.
|
||||
await browser.execute(() => {
|
||||
editor.setCurrentTool('draw')
|
||||
})
|
||||
await drawTouchStroke({ from: { x: -80, y: -40 }, to: { x: 80, y: 40 } })
|
||||
|
||||
const segments = await browser.execute(() => {
|
||||
const shape = editor.getCurrentPageShapes().find((s) => s.type === 'draw')
|
||||
if (!shape) return null
|
||||
return (shape as any).props.segments.map((s: any) => ({
|
||||
dim: s.dim,
|
||||
pathLength: s.path.length,
|
||||
}))
|
||||
})
|
||||
|
||||
// The stroke produced a real draw shape with encoded points...
|
||||
expect(segments).not.toBeNull()
|
||||
expect(segments!.length).toBeGreaterThan(0)
|
||||
expect(segments!.every((s) => s.pathLength > 0)).toBe(true)
|
||||
// ...and the engine reported finger input as non-pressure, so every
|
||||
// segment took the 2D encoding. If this fails with dim undefined, the
|
||||
// browser has started reporting varying pressure for plain touches and
|
||||
// the classification in Drawing.ts needs revisiting.
|
||||
expect(segments!.every((s) => s.dim === 2)).toBe(true)
|
||||
})
|
||||
})
|
||||
49
apps/examples/e2e-device/tests/pinch.e2e.ts
Normal file
49
apps/examples/e2e-device/tests/pinch.e2e.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { expect } from '@wdio/globals'
|
||||
import type { Editor } from 'tldraw'
|
||||
import { getSelectedShapeIds, getZoom, openEditor } from '../helpers/app'
|
||||
import { pinchCanvas } from '../helpers/pinch'
|
||||
|
||||
declare const editor: Editor
|
||||
|
||||
/**
|
||||
* Device-realistic pinch tests. These mirror the intent of the Playwright camera
|
||||
* tests (apps/examples/e2e/tests/test-camera.spec.ts) but run through the real
|
||||
* OS touch pipeline on a simulator/emulator, so they cover the browser's own
|
||||
* gesture recogniser — the part synthetic CDP touch can't reach.
|
||||
*/
|
||||
describe('pinch to zoom', () => {
|
||||
beforeEach(async () => {
|
||||
await openEditor()
|
||||
})
|
||||
|
||||
it('spreading two fingers zooms in', async () => {
|
||||
expect(await getZoom()).toBe(1)
|
||||
await pinchCanvas({ scale: 2.5 })
|
||||
expect(await getZoom()).toBeGreaterThan(1)
|
||||
})
|
||||
|
||||
it('pinching two fingers together zooms out', async () => {
|
||||
expect(await getZoom()).toBe(1)
|
||||
await pinchCanvas({ scale: 0.4 })
|
||||
expect(await getZoom()).toBeLessThan(1)
|
||||
})
|
||||
|
||||
it('a two-finger pinch does not change the selection', async () => {
|
||||
// Place a shape under where the pinch lands, plus a second shape that we
|
||||
// pre-select off to the side. The pinch must not steal the selection.
|
||||
await browser.execute(() => {
|
||||
editor.createShapes([
|
||||
{ id: 'shape:a', type: 'geo', x: -400, y: -400, props: { w: 100, h: 100 } },
|
||||
{ id: 'shape:b', type: 'geo', x: 0, y: 0, props: { w: 400, h: 400 } },
|
||||
] as any)
|
||||
editor.setSelectedShapes(['shape:a'] as any)
|
||||
})
|
||||
expect(await getSelectedShapeIds()).toEqual(['shape:a'])
|
||||
|
||||
await pinchCanvas({ scale: 2 })
|
||||
|
||||
// Selection is preserved and the gesture really did zoom.
|
||||
expect(await getSelectedShapeIds()).toEqual(['shape:a'])
|
||||
expect(await getZoom()).not.toBe(1)
|
||||
})
|
||||
})
|
||||
9
apps/examples/e2e-device/tsconfig.json
Normal file
9
apps/examples/e2e-device/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../../internal/config/tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
|
||||
"moduleResolution": "bundler",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["."]
|
||||
}
|
||||
22
apps/examples/e2e-device/wdio.android.conf.ts
Normal file
22
apps/examples/e2e-device/wdio.android.conf.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Options } from '@wdio/types'
|
||||
import { shared } from './wdio.shared.conf'
|
||||
|
||||
/**
|
||||
* Drives real Chrome on the Android emulator via Appium's UiAutomator2 driver.
|
||||
* Multi-touch pinch is injected as real MotionEvents in the NATIVE context
|
||||
* (see helpers/pinch.ts) rather than through bare chromedriver, whose multi-touch
|
||||
* support is unreliable.
|
||||
*/
|
||||
export const config: Options.Testrunner = {
|
||||
...shared,
|
||||
capabilities: [
|
||||
{
|
||||
platformName: 'Android',
|
||||
'appium:automationName': 'UiAutomator2',
|
||||
browserName: 'Chrome',
|
||||
'appium:newCommandTimeout': 120,
|
||||
// Let Appium manage a matching chromedriver for the emulator's Chrome.
|
||||
'appium:chromedriverAutodownload': true,
|
||||
},
|
||||
],
|
||||
}
|
||||
48
apps/examples/e2e-device/wdio.ios.conf.ts
Normal file
48
apps/examples/e2e-device/wdio.ios.conf.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Options } from '@wdio/types'
|
||||
import { shared } from './wdio.shared.conf'
|
||||
|
||||
const deviceName = process.env.APPIUM_DEVICE_NAME ?? 'iPhone 16'
|
||||
const udid = process.env.APPIUM_UDID
|
||||
const derivedDataPath = process.env.APPIUM_DERIVED_DATA_PATH
|
||||
const usePrebuiltWDA = process.env.APPIUM_USE_PREBUILT_WDA === 'true'
|
||||
|
||||
/**
|
||||
* Drives real Mobile Safari in the iOS Simulator via Appium's XCUITest driver.
|
||||
* Touch gestures are performed in the NATIVE context (see helpers/pinch.ts),
|
||||
* which is the only place multi-touch W3C actions work on iOS — they go through
|
||||
* the real Simulator touch system and therefore through Safari's own gesture
|
||||
* recognizer.
|
||||
*/
|
||||
export const config: Options.Testrunner = {
|
||||
...shared,
|
||||
capabilities: [
|
||||
{
|
||||
platformName: 'iOS',
|
||||
'appium:automationName': 'XCUITest',
|
||||
'appium:deviceName': deviceName,
|
||||
// Attach to the simulator CI already booted (see the Boot step), so
|
||||
// XCUITest doesn't create/boot one itself.
|
||||
...(udid ? { 'appium:udid': udid } : {}),
|
||||
// Pin platformVersion in CI to the runtime shipped by the macos-26 image
|
||||
// if you need determinism; left unset here to use the booted default.
|
||||
browserName: 'Safari',
|
||||
'appium:safariInitialUrl': `${shared.baseUrl}/end-to-end`,
|
||||
'appium:newCommandTimeout': 120,
|
||||
// Let Appium own the simulator lifecycle (create/boot the one it uses).
|
||||
// Don't pre-boot in CI — a second simulator just causes boot contention.
|
||||
'appium:noReset': true,
|
||||
// First run builds WebDriverAgent from source, which is slow on a cold
|
||||
// CI simulator. Give the build/launch generous headroom (no signing is
|
||||
// needed on the simulator, so this is just time, not config).
|
||||
'appium:wdaLaunchTimeout': 300_000,
|
||||
'appium:wdaConnectionTimeout': 300_000,
|
||||
'appium:wdaStartupRetries': 2,
|
||||
'appium:wdaStartupRetryInterval': 20_000,
|
||||
'appium:showXcodeLog': false,
|
||||
// Build WDA into a stable, cacheable DerivedData dir; on a CI cache hit
|
||||
// reuse the prebuilt WDA and skip the xcodebuild step entirely.
|
||||
...(derivedDataPath ? { 'appium:derivedDataPath': derivedDataPath } : {}),
|
||||
...(usePrebuiltWDA ? { 'appium:usePrebuiltWDA': true } : {}),
|
||||
},
|
||||
],
|
||||
}
|
||||
58
apps/examples/e2e-device/wdio.shared.conf.ts
Normal file
58
apps/examples/e2e-device/wdio.shared.conf.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { Options } from '@wdio/types'
|
||||
|
||||
const PORT = process.env.EXAMPLES_PORT ?? '5420'
|
||||
|
||||
/**
|
||||
* Base WebdriverIO config shared by the iOS and Android device suites. The
|
||||
* per-platform configs spread this and add their own `capabilities`.
|
||||
*
|
||||
* Appium is started for us by `@wdio/appium-service`; the relevant driver
|
||||
* (xcuitest / uiautomator2) is installed by the CI job before this runs.
|
||||
*/
|
||||
export const shared: Options.Testrunner = {
|
||||
runner: 'local',
|
||||
// Run the built examples app. iOS sim shares the host's localhost; the
|
||||
// Android job forwards the port into the emulator with `adb reverse`.
|
||||
baseUrl: `http://localhost:${PORT}`,
|
||||
|
||||
specs: ['./tests/**/*.e2e.ts'],
|
||||
maxInstances: 1,
|
||||
|
||||
logLevel: 'warn',
|
||||
waitforTimeout: 10_000,
|
||||
// Generous per-request timeout: the first session creation has to build and
|
||||
// launch WebDriverAgent on a cold simulator, which can take several minutes.
|
||||
connectionRetryTimeout: 600_000,
|
||||
// Allow exactly one session-creation retry (1 retry = two attempts). On iOS
|
||||
// the first attempt can fail on Safari's slow web-inspector attach; the retry
|
||||
// reuses the already-booted simulator and running WebDriverAgent (CI attaches
|
||||
// by udid), so it recovers quickly. Don't raise this: when Appium owned the
|
||||
// simulator lifecycle, stacked retries booted multiple simulators at once and
|
||||
// they all timed out.
|
||||
connectionRetryCount: 1,
|
||||
|
||||
framework: 'mocha',
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
timeout: 90_000,
|
||||
// Gestures are inherently a little flaky; let an individual test retry
|
||||
// before failing the run. Re-running the whole job is always an option too.
|
||||
retries: 1,
|
||||
},
|
||||
// Retry an entire spec file once if it fails outright (e.g. a flaky sim boot).
|
||||
specFileRetries: 1,
|
||||
specFileRetriesDeferred: true,
|
||||
|
||||
reporters: ['spec'],
|
||||
|
||||
services: [
|
||||
[
|
||||
'appium',
|
||||
{
|
||||
args: { relaxedSecurity: true },
|
||||
// Appium is installed globally in CI; use that binary.
|
||||
command: 'appium',
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user