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

286 lines
9.5 KiB
TypeScript

import { expect, test } from '../fixtures/scenario-test'
// Live share and workspace membership scenarios.
//
// These scenarios drive two browser contexts and assert on state that propagates between them via
// Zero sync. The membership/file waits gate on the data layer first (see Sidebar), but the heavy
// per-test setup plus genuinely variable cross-client sync latency on CI needs more than the default
// 30s per-test budget, so raise it for the suite.
test.describe.configure({ mode: 'parallel', timeout: 60_000 })
test.describe('live sharing scenarios', () => {
test('owner and visitor see live edits through an edit link', async ({
owner,
visitor,
scenario,
}) => {
await scenario.createGuestEditFile(owner, visitor, scenario.name('live edit file'))
await visitor.editor.expectShapesCount(0)
await owner.page.getByTestId('tools.rectangle').click()
await owner.page.locator('.tl-background').click()
await owner.editor.expectShapesCount(1)
await visitor.editor.expectShapesCount(1)
})
test('owner sees collaborator presence join and leave live', async ({
owner,
visitor,
scenario,
}) => {
await scenario.createGuestEditFile(owner, visitor, scenario.name('presence file'))
await owner.expectCollaboratorCount(1)
await visitor.expectCollaboratorCount(1)
await visitor.close()
await owner.waitForSessionClosed()
})
test('visitor permissions update while the shared file is open', async ({
owner,
visitor,
scenario,
}) => {
await scenario.createGuestEditFile(owner, visitor, scenario.name('permission file'))
await expect(visitor.page.getByTestId('tools.draw')).toBeVisible()
await scenario.setSharedLinkType(owner, 'view')
await expect(visitor.page.getByTestId('tools.draw')).not.toBeVisible({ timeout: 10000 })
await scenario.setSharedLinkType(owner, 'edit')
await expect(visitor.page.getByTestId('tools.draw')).toBeVisible({ timeout: 10000 })
})
test('view-only visitors see live owner changes without edit access', async ({
owner,
visitor,
scenario,
}) => {
await scenario.createGuestViewFile(owner, visitor, scenario.name('view-only guest file'))
await visitor.expectReadonly(true)
await expect(visitor.page.getByTestId('tools.draw')).not.toBeVisible()
await visitor.editor.expectShapesCount(0)
await scenario.createRectangle(owner)
await owner.editor.expectShapesCount(1)
await visitor.editor.expectShapesCount(1)
})
test('signed-in non-member sees shared files as a live guest file', async ({
owner,
member,
scenario,
}) => {
const fileName = scenario.name('signed-in guest file')
await scenario.createGuestEditFile(owner, member, fileName)
await member.editor.ensureSidebarOpen()
await member.sidebar.expectFileVisible(fileName)
await expect(member.page.getByTestId(`guest-badge-${fileName}`)).toBeVisible()
await expect(member.page.getByTestId('tools.draw')).toBeVisible()
await scenario.setSharedLinkType(owner, 'view')
await expect(member.page.getByTestId('tools.draw')).not.toBeVisible({ timeout: 10000 })
})
test('opening a guest file does not trap the member outside their home after they join its workspace', async ({
owner,
member,
scenario,
}) => {
// owner makes a workspace with a (shared) file owned by that workspace, plus an invite.
const { workspaceName, fileName, inviteUrl } = await scenario.createPendingWorkspaceInvite({
owner,
member,
})
const fileUrl = owner.page.url() // owner is left viewing the workspace file, which is shared
// member opens that file via its link while NOT a member of the workspace: it gets mirrored
// into their home as a guest file (visible in their sidebar).
await member.goto(fileUrl)
await member.editor.ensureSidebarOpen()
await member.sidebar.expectFileVisible(fileName)
// member then accepts the invite and joins the workspace.
await member.goto(inviteUrl)
await member.editor.ensureSidebarOpen()
await member.workspaceInviteDialog.acceptInvitation()
await member.sidebar.expectWorkspaceVisible(workspaceName)
// Now switching to the home workspace must stay there. The file is owned by a workspace the
// member belongs to, so it must not be listed in home: before the fix it was, and opening it
// re-activated the workspace, so the member could never get back to their home.
await member.sidebar.switchToHomeWorkspace()
await member.sidebar.expectActiveHomeWorkspace()
await member.sidebar.expectFileNotVisible(fileName)
})
test('unshare removes visitor access while the file is open', async ({
owner,
visitor,
scenario,
}) => {
const { sharedUrl } = await scenario.createSharedFile(
owner,
'edit',
scenario.name('unshare file')
)
await visitor.goto(sharedUrl)
await expect(visitor.shareMenu.shareButton).toBeVisible()
await expect(visitor.page.getByTestId('tla-error')).not.toBeVisible()
await scenario.setSharedLinkType(owner, 'no-access')
await expect(visitor.shareMenu.shareButton).not.toBeVisible({ timeout: 10000 })
await expect(visitor.page.getByTestId('tla-error')).toBeVisible({ timeout: 10000 })
})
test('published snapshots update only after publishing changes', async ({
owner,
visitor,
scenario,
}) => {
const { publishedUrl } = await scenario.createPublishedFile(
owner,
scenario.name('published snapshot file')
)
await visitor.goto(publishedUrl)
await visitor.editor.expectShapesCount(0)
await scenario.createRectangle(owner)
await owner.editor.expectShapesCount(1)
await visitor.page.reload()
await visitor.waitForAppReady()
await visitor.editor.expectShapesCount(0)
await scenario.publishChanges(owner)
await visitor.page.reload()
await visitor.waitForAppReady()
await visitor.editor.expectShapesCount(1)
})
test('workspace file deletion removes the file from active members without reload', async ({
owner,
member,
scenario,
}) => {
const { workspaceName, fileName } = await scenario.createWorkspaceWithMember({
owner,
member,
workspaceName: scenario.name('delete file workspace'),
fileName: scenario.name('live deleted workspace file'),
})
await owner.sidebar.switchToWorkspace(workspaceName)
await owner.sidebar.deleteFileByName(fileName)
await owner.deleteFileDialog.expectIsVisible()
await owner.deleteFileDialog.confirmDeletion()
await owner.deleteFileDialog.expectIsNotVisible()
await owner.sidebar.expectFileNotVisible(fileName)
await member.sidebar.expectFileNotVisible(fileName)
})
test('workspace invite acceptance appears in owner settings without reload', async ({
owner,
member,
scenario,
}) => {
const { workspaceName, fileName, inviteUrl, memberUserId } =
await scenario.createPendingWorkspaceInvite({
owner,
member,
workspaceName: scenario.name('pending invite workspace'),
fileName: scenario.name('pending invite file'),
})
const memberRoleSelect = owner.page.locator(`[id="workspace-member-role-${memberUserId}"]`)
await owner.sidebar.openWorkspaceSettings(workspaceName)
await expect(memberRoleSelect).not.toBeVisible()
await member.goto(inviteUrl)
await member.editor.ensureSidebarOpen()
await member.workspaceInviteDialog.expectIsVisible()
await member.workspaceInviteDialog.acceptInvitation()
await member.sidebar.expectWorkspaceVisible(workspaceName)
await member.sidebar.expectFileVisible(fileName)
await expect(memberRoleSelect).toBeVisible({ timeout: 10000 })
await owner.page.keyboard.press('Escape')
})
test('member removal revokes the active member window without reload', async ({
owner,
member,
scenario,
}) => {
const { workspaceName, fileName } = await scenario.createWorkspaceWithRemovedMember({
owner,
member,
workspaceName: scenario.name('member removal workspace'),
fileName: scenario.name('member removal file'),
})
await member.sidebar.expectWorkspaceNotVisible(workspaceName)
await member.sidebar.expectFileNotVisible(fileName)
})
test('workspace role changes update active members without reload', async ({
owner,
member,
scenario,
}) => {
const { workspaceName, fileName, memberUserId } = await scenario.createWorkspaceWithMember({
owner,
member,
workspaceName: scenario.name('role change workspace'),
fileName: scenario.name('role change file'),
})
await member.sidebar.expectWorkspaceVisible(workspaceName)
await member.sidebar.expectFileVisible(fileName)
await member.sidebar.openWorkspaceSettings(workspaceName)
// Delete lives on the Settings tab and only for owners; promoting the member should
// surface it there reactively, without a reload.
await member.page.getByRole('tab', { name: 'Settings' }).click()
const deleteWorkspaceButton = member.page.getByRole('button', { name: /Delete workspace/ })
await expect(deleteWorkspaceButton).not.toBeVisible()
await scenario.setWorkspaceMemberRole({
owner,
workspaceName,
memberUserId,
role: 'owner',
})
await expect(deleteWorkspaceButton).toBeVisible({ timeout: 10000 })
await member.page.keyboard.press('Escape')
})
test('workspace deletion removes the workspace from active members without reload', async ({
owner,
member,
scenario,
}) => {
const { workspaceName, fileName } = await scenario.createWorkspaceWithMember({
owner,
member,
workspaceName: scenario.name('live workspace'),
fileName: scenario.name('shared workspace file'),
})
await member.sidebar.expectWorkspaceVisible(workspaceName)
await member.sidebar.expectFileVisible(fileName)
await owner.sidebar.deleteWorkspace(workspaceName)
await owner.sidebar.expectWorkspaceNotVisible(workspaceName)
await member.sidebar.expectWorkspaceNotVisible(workspaceName)
await member.sidebar.expectFileNotVisible(fileName)
})
})