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

204 lines
6.4 KiB
TypeScript

import fs from 'fs'
import { setupClerkTestingToken } from '@clerk/testing/playwright'
import { test as base, expect } from '@playwright/test'
import { OTHER_USERS, USERS } from '../consts'
import { Database } from './Database'
import { DeleteFileDialog } from './DeleteFileDialog'
import { Editor } from './Editor'
import { getStorageStateFileName } from './helpers'
import { HomePage } from './HomePage'
import { ImportHelper } from './ImportHelper'
import { ShareMenu } from './ShareMenu'
import { Sidebar } from './Sidebar'
import { SignInDialog } from './SignInDialog'
import { WorkspaceInviteDialog } from './WorkspaceInviteDialog'
interface TlaFixtures {
homePage: HomePage
editor: Editor
sidebar: Sidebar
shareMenu: ShareMenu
database: Database
deleteFileDialog: DeleteFileDialog
workspaceInviteDialog: WorkspaceInviteDialog
signInDialog: SignInDialog
importHelper: ImportHelper
setupAndCleanup: void
retry(fn: () => Promise<void>): Promise<void>
}
interface TlaWorkerFixtures {
workerStorageState: string
}
export const test = base.extend<TlaFixtures, TlaWorkerFixtures>({
sidebar: async ({ page }, testUse) => {
await testUse(new Sidebar(page))
},
editor: async ({ page, sidebar }, testUse) => {
await testUse(new Editor(page, sidebar))
},
homePage: async ({ page, editor }, testUse) => {
await testUse(new HomePage(page, editor))
},
database: async ({ page }, testUse) => {
await testUse(new Database(page, test.info().parallelIndex))
},
shareMenu: async ({ page }, testUse) => {
await testUse(new ShareMenu(page))
},
deleteFileDialog: async ({ page }, testUse) => {
await testUse(new DeleteFileDialog(page))
},
workspaceInviteDialog: async ({ page }, testUse) => {
await testUse(new WorkspaceInviteDialog(page))
},
signInDialog: async ({ page }, testUse) => {
await testUse(new SignInDialog(page))
},
importHelper: async ({ page }, testUse) => {
await testUse(new ImportHelper(page))
},
// This is an auto fixture which makes sure that we are on the home page when the test starts
// and that we clean up when the tests completes
setupAndCleanup: [
async ({ page, homePage, database }, testUse) => {
// All the code before testUse is run before each test
// Make sure we don't get marked as a bot
// https://clerk.com/docs/testing/playwright/overview
await setupClerkTestingToken({ page })
await database.reset()
await homePage.goto()
await homePage.isLoaded()
await testUse()
},
{ auto: true },
],
storageState: ({ workerStorageState }, testUse) => testUse(workerStorageState),
workerStorageState: [
async ({ browser }, testUse) => {
const id = test.info().parallelIndex
const fileName = getStorageStateFileName(id, 'huppy')
if (fs.existsSync(fileName)) {
// Reuse existing authentication state if any.
await testUse(fileName)
return
}
for (const user of ['huppy' as const, 'suppy' as const]) {
const fileName = getStorageStateFileName(id, user)
// Important: make sure we authenticate in a clean environment by unsetting storage state.
const page = await browser.newPage({ storageState: undefined })
let email: string
if (user === 'huppy') {
email = USERS[id]
} else {
email = OTHER_USERS[id]
}
const sidebar = new Sidebar(page)
const editor = new Editor(page, sidebar)
const homePage = new HomePage(page, editor)
await homePage.loginAs(email)
await expect(page.getByTestId('tla-sidebar-layout')).toBeVisible()
await page.context().storageState({ path: fileName })
await page.close()
}
await testUse(fileName)
},
{ scope: 'worker' },
],
})
export { expect } from '@playwright/test'
/** Stage 3 / TS 5.2+ method decorator context (subset; avoids coupling to a specific TS lib version). */
interface StepMethodDecoratorContext {
kind: 'method'
name: string | symbol
}
function isStage3MethodDecoratorContext(value: unknown): value is StepMethodDecoratorContext {
return (
typeof value === 'object' &&
value !== null &&
'kind' in value &&
(value as StepMethodDecoratorContext).kind === 'method'
)
}
/** Legacy method decorator (`experimentalDecorators`). */
export function step<T extends (...args: any[]) => any>(
target: object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
): TypedPropertyDescriptor<T>
/** Stage 3 method decorator (e.g. Node `--no-strip-types`). */
export function step<T extends (...args: any[]) => any>(
originalMethod: T,
context: StepMethodDecoratorContext
): T
/**
* Wraps page-object methods in `test.step` for clearer Playwright reports.
* Supports both legacy (`experimentalDecorators`) and Stage 3 method decorators
* (e.g. when tests run under Node's `--no-strip-types` + modern emit).
*/
export function step(
target: object | ((...args: any[]) => any),
propertyKeyOrContext: string | symbol | StepMethodDecoratorContext,
descriptor?: TypedPropertyDescriptor<(...args: any[]) => any>
): any {
if (
descriptor === undefined &&
isStage3MethodDecoratorContext(propertyKeyOrContext) &&
typeof target === 'function'
) {
const original = target as (...args: any[]) => any
const ctx = propertyKeyOrContext
const key = typeof ctx.name === 'string' ? ctx.name : String(ctx.name)
return async function (this: object, ...args: any[]) {
return await test.step(`${(this as { constructor: { name: string } }).constructor.name}.${key}`, async () => {
return original.apply(this, args)
})
}
}
if (!descriptor?.value) {
throw new Error(
'step: expected a legacy method decorator with descriptor.value, or a Stage 3 method decorator'
)
}
const propertyKey = propertyKeyOrContext as string
const original = descriptor.value
descriptor.value = async function (this: object, ...args: any[]) {
return await test.step(`${(this as { constructor: { name: string } }).constructor.name}.${String(propertyKey)}`, async () => {
return original.apply(this, args)
})
}
return descriptor
}
export function repeatTest(
name: string,
fn: (...args: any) => Promise<void>,
{ times = 5, only = false } = {}
) {
const getName = (i: number) => `${name} (${i + 1} of ${times})`
for (let i = 0; i < times; i++) {
if (only) {
// eslint-disable-next-line tldraw/no-focused-tests
test.only(getName(i), fn)
} else {
test(getName(i), fn)
}
}
return
}
repeatTest.only = (name: string, fn: (...args: any) => Promise<void>, { times = 5 } = {}) =>
repeatTest(name, fn, { times, only: true })