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
286 lines
9.5 KiB
TypeScript
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)
|
|
})
|
|
})
|