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

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
import { expect, test } from '../../fixtures/tla-test'
const COOKIE_CONSENT_KEY = 'tldraw_cookie_consent'
// Exercise the banner as a signed-out visitor so consent relies on localStorage.
test.use({ storageState: { cookies: [], origins: [] } })
test.describe('cookie consent banner', () => {
test.beforeEach(async ({ page }) => {
// Mock the consent check to always require consent
await page.route('https://consent.tldraw.xyz', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ requires_consent: true, country_code: 'TEST' }),
})
})
await page.evaluate((key) => {
// eslint-disable-next-line tldraw/no-direct-storage
window.localStorage.removeItem(key)
}, COOKIE_CONSENT_KEY)
})
test('shows banner and animations work without prefers-reduced-motion', async ({
page,
homePage,
}) => {
await homePage.goto()
await homePage.isLoaded()
const banner = page.getByTestId('tla-cookie-consent')
await expect(banner).toBeVisible()
// Verify animation duration is set (not instant)
const animationDuration = await banner.evaluate((el) => {
const styles = window.getComputedStyle(el)
return styles.animationDuration
})
expect(animationDuration).not.toBe('0s')
await expect(banner.getByRole('button', { name: 'Opt out' })).toBeVisible()
await expect(banner.getByRole('button', { name: 'Settings' })).toBeVisible()
await expect(banner.getByRole('button', { name: 'Accept all' })).toBeVisible()
})
test.describe('with prefers-reduced-motion', () => {
test.beforeEach(async ({ page, homePage }) => {
await page.emulateMedia({ reducedMotion: 'reduce' })
await homePage.goto()
await homePage.isLoaded()
})
test('animations are instant', async ({ page }) => {
const banner = page.getByTestId('tla-cookie-consent')
await expect(banner).toBeVisible()
// Check that animation duration is 0
const animationDuration = await banner.evaluate((el) => {
const styles = window.getComputedStyle(el)
return styles.animationDuration
})
expect(animationDuration).toBe('0s')
})
test('accepting cookies hides banner and persists choice', async ({ page, homePage }) => {
const banner = page.getByTestId('tla-cookie-consent')
await expect(banner).toBeVisible()
await banner.getByRole('button', { name: 'Accept all' }).click()
await expect(banner).not.toBeVisible()
await expect(async () => {
const storedConsent = await page.evaluate(
// eslint-disable-next-line tldraw/no-direct-storage
(key) => window.localStorage.getItem(key),
COOKIE_CONSENT_KEY
)
expect(storedConsent).toBe(JSON.stringify({ analytics: true }))
}).toPass()
await page.reload()
await homePage.isLoaded()
await expect(page.getByTestId('tla-cookie-consent')).not.toBeVisible()
})
test('opting out hides banner and persists choice', async ({ page, homePage }) => {
const banner = page.getByTestId('tla-cookie-consent')
await expect(banner).toBeVisible()
await banner.getByRole('button', { name: 'Opt out' }).click()
await expect(banner).not.toBeVisible()
await expect(async () => {
const storedConsent = await page.evaluate(
// eslint-disable-next-line tldraw/no-direct-storage
(key) => window.localStorage.getItem(key),
COOKIE_CONSENT_KEY
)
expect(storedConsent).toBe(JSON.stringify({ analytics: false }))
}).toPass()
await page.reload()
await homePage.isLoaded()
await expect(page.getByTestId('tla-cookie-consent')).not.toBeVisible()
})
})
})

View File

@@ -0,0 +1,35 @@
import { Editor } from '../../fixtures/Editor'
import { sleep } from '../../fixtures/helpers'
import { HomePage } from '../../fixtures/HomePage'
import { Sidebar } from '../../fixtures/Sidebar'
import { expect, test } from '../../fixtures/tla-test'
test('correctly clears db ', async ({ page, browser, editor, sidebar, database }) => {
const documentsToAdd = 3
await test.step(`create ${documentsToAdd} files file`, async () => {
await editor.ensureSidebarOpen()
const currentCount = await sidebar.getNumberOfFiles()
for (let i = 0; i < documentsToAdd; i++) {
await sleep(1100)
await sidebar.createNewDocument()
}
const newCount = await sidebar.getNumberOfFiles()
expect(newCount).toBe(currentCount + documentsToAdd)
})
await test.step('close the page and reset db', async () => {
await page.close()
await database.reset()
})
await test.step('check that files reset', async () => {
const newPage = await browser.newPage()
const newSidebar = new Sidebar(newPage)
const newEditor = new Editor(newPage, newSidebar)
const newHomePage = new HomePage(newPage, newEditor)
await newHomePage.goto()
await newHomePage.isLoaded()
const afterReset = await newSidebar.getNumberOfFiles()
expect(afterReset).toBe(1)
})
})

View File

@@ -0,0 +1,162 @@
import { expectBeforeAndAfterReload, getRandomName, sleep } from '../../fixtures/helpers'
import { expect, test } from '../../fixtures/tla-test'
test.beforeEach(async ({ editor }) => {
await editor.isLoaded()
})
test('can instantiate', async ({ page }) => {
const dialogCloseButton = page.getByTestId('dialog.close')
// expect this button not to be present; ie that the dialog is not open
await expect(dialogCloseButton).not.toBeVisible()
})
test.describe('preferences', () => {
test('can toggle dark mode', async ({ page, editor, sidebar }) => {
await test.step('is light mode by default', async () => {
await expect(page.locator('div.tla-theme__light.tl-theme__light')).toBeVisible()
await expect(page.locator('div.tla-theme__dark.tl-theme__dark')).not.toBeVisible()
await expect(page.locator('div.tl-background')).toHaveCSS(
'background-color',
'rgb(249, 250, 251)'
)
await expect(page.locator('div.tla-theme-container')).toHaveCSS(
'background-color',
'rgb(252, 252, 252)'
)
})
await test.step('can toggle dark mode', async () => {
await editor.ensureSidebarOpen()
await sidebar.setDarkMode()
})
await test.step('is dark mode', async () => {
await expectBeforeAndAfterReload(async () => {
await expect(page.locator('div.tla-theme__light.tl-theme__light')).not.toBeVisible()
await expect(page.locator('div.tla-theme__dark.tl-theme__dark')).toBeVisible()
await expect(page.locator('div.tl-background')).toHaveCSS(
'background-color',
'rgb(16, 16, 17)'
)
await expect(page.locator('div.tla-theme-container')).toHaveCSS(
'background-color',
'rgb(13, 13, 13)'
)
}, page)
})
})
test('can change language', async ({ sidebar, page, editor }) => {
await editor.ensureSidebarOpen()
await test.step('English by default', async () => {
await sidebar.expectLanguageToBe('Language', 'English')
})
await sidebar.closeAccountMenu()
await test.step('change language', async () => {
await sidebar.setLanguage('Language', 'Français')
})
await test.step('changed to French', async () => {
await sidebar.expectLanguageToBe('Langue', 'Français')
await editor.openPageMenu()
// Make sure the file / page menu is using the correct language
await expect(page.getByText('Charger un média')).toBeVisible()
})
})
})
test.describe('sidebar actions', () => {
test('can pin a file from the file menu', async ({ page, editor, sidebar }) => {
// Create two files so that there are three
await editor.ensureSidebarOpen()
await sidebar.createNewDocument()
await sleep(1000)
await sidebar.createNewDocument()
expect(await sidebar.getNumberOfFiles()).toBe(3)
// Name the files
const fileName0 = getRandomName()
await sidebar.renameFile(0, fileName0)
const fileName1 = getRandomName()
await sidebar.renameFile(1, fileName1)
const fileName2 = getRandomName()
await sidebar.renameFile(2, fileName2)
// The pinned files are also sorted by recency (though not grouped by time)
// When we pin a file, we expect that it will move to the pinned files section
// but that the order of the pinned files will be the same as the order of the files
// Pin the second-most recent file...
await sidebar.pinFromFileMenu(1)
// And expect it to the the first (and only) pinned file
await expectBeforeAndAfterReload(async () => {
await expect(async () => {
await expect(
page.getByTestId('tla-file-link-pinned-0').getByText(fileName1, { exact: true })
).toBeVisible()
await expect(
page.getByTestId('tla-file-link-today-0').getByText(fileName0, { exact: true })
).toBeVisible()
await expect(
page.getByTestId('tla-file-link-today-1').getByText(fileName2, { exact: true })
).toBeVisible()
}).toPass()
}, page)
// Now we pin the most recent file...
await sidebar.pinFromFileMenu(0)
// And expect it to the first pinned file
await expectBeforeAndAfterReload(async () => {
await expect(async () => {
await expect(
page.getByTestId('tla-file-link-pinned-0').getByText(fileName0, { exact: true })
).toBeVisible()
await expect(
page.getByTestId('tla-file-link-pinned-1').getByText(fileName1, { exact: true })
).toBeVisible()
await expect(
page.getByTestId('tla-file-link-today-0').getByText(fileName2, { exact: true })
).toBeVisible()
}).toPass()
}, page)
// Now we unpin the most recent file...
await sidebar.unpinFromFileMenu(0)
await expectBeforeAndAfterReload(async () => {
await expect(async () => {
await expect(
page.getByTestId('tla-file-link-pinned-0').getByText(fileName1, { exact: true })
).toBeVisible()
await expect(
page.getByTestId('tla-file-link-today-0').getByText(fileName0, { exact: true })
).toBeVisible()
await expect(
page.getByTestId('tla-file-link-today-1').getByText(fileName2, { exact: true })
).toBeVisible()
}).toPass()
}, page)
})
})
// Menu bar
test('can rename a file name by clicking the name', async ({ editor, sidebar, page }) => {
const originalName = await editor.getCurrentFileName()
const newName = getRandomName()
await expect(async () => {
await sidebar.expectToContainText(originalName)
await sidebar.expectNotToContainText(newName)
}).toPass()
await editor.rename(newName)
await expectBeforeAndAfterReload(async () => {
await sidebar.expectToContainText(newName)
await sidebar.expectNotToContainText(originalName)
}, page)
})

View File

@@ -0,0 +1,47 @@
import { USERS } from '../../consts'
import { expect, test } from '../../fixtures/tla-test'
// Don't use stored credentials
test.use({ storageState: { cookies: [], origins: [] } })
test('can login', async ({ homePage, editor }) => {
await expect(homePage.signInButton).toBeVisible()
const user = USERS[test.info().parallelIndex]
await homePage.loginAs(user)
await editor.isLoaded()
await expect(homePage.signInButton).not.toBeVisible()
await expect(editor.sidebarToggle).toBeVisible()
})
test('can sign out', async ({ homePage, editor, sidebar }) => {
await test.step('Login', async () => {
const user = USERS[test.info().parallelIndex]
await homePage.loginAs(user)
})
await editor.isLoaded()
await homePage.expectSignInButtonNotVisible()
await editor.ensureSidebarOpen()
await sidebar.signOut()
await homePage.expectSignInButtonVisible()
})
test('shows anonymous SDK content', async ({ page, homePage }) => {
await homePage.expectSignInButtonVisible()
await expect(page.getByTestId('tla-anon-dotdev-link')).toBeVisible()
await page.getByTestId('tla-anon-dotdev-dismiss-button').click()
await expect(page.getByTestId('tla-anon-dotdev-link')).not.toBeVisible()
})
test('shows a comment button that prompts sign in', async ({ page, homePage, signInDialog }) => {
await homePage.expectSignInButtonVisible()
const commentButton = page.getByTestId('quick-actions.comment')
await expect(commentButton).toBeVisible()
await commentButton.click()
await signInDialog.expectInitialElements()
// The comment tool itself is never registered for signed-out visitors, so the button only opens
// the dialog — it can't drop the canvas into a half-mounted tool.
await expect(commentButton).toHaveAttribute('aria-pressed', 'false')
})

View File

@@ -0,0 +1,383 @@
import fs from 'fs'
import path from 'path'
import { getRandomName, openNewTab } from '../../fixtures/helpers'
import { expect, test } from '../../fixtures/tla-test'
test.beforeEach(async ({ context }) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
})
/* --------------------- Sharing -------------------- */
test.describe('signed in user on own file', () => {
test('default share state', async ({ shareMenu }) => {
await shareMenu.open()
expect(await shareMenu.isToggleChecked()).toBe(true)
expect(await shareMenu.getShareType()).toBe('Editor')
})
test('tabs work correctly', async ({ shareMenu }) => {
await shareMenu.open()
expect(await shareMenu.publishTabButton.isVisible()).toBe(true)
expect(await shareMenu.inviteTabButton.isVisible()).toBe(true)
expect(await shareMenu.exportTabButton.isVisible()).toBe(true)
expect(await shareMenu.anonShareTabButton.isVisible()).toBe(false)
// Starts on the invite tab
expect(await shareMenu.inviteTabPage.isVisible()).toBe(true)
expect(await shareMenu.exportTabPage.isVisible()).toBe(false)
expect(await shareMenu.publishTabPage.isVisible()).toBe(false)
expect(await shareMenu.anonShareTabPage.isVisible()).toBe(false)
// Can switch between tabs (export)
await shareMenu.exportTabButton.click()
expect(await shareMenu.inviteTabPage.isVisible()).toBe(false)
expect(await shareMenu.exportTabPage.isVisible()).toBe(true)
expect(await shareMenu.publishTabPage.isVisible()).toBe(false)
// Can switch between tabs (publish)
await shareMenu.publishTabButton.click()
expect(await shareMenu.inviteTabPage.isVisible()).toBe(false)
expect(await shareMenu.exportTabPage.isVisible()).toBe(false)
expect(await shareMenu.publishTabPage.isVisible()).toBe(true)
})
test('a select inside the menu closes without closing the menu', async ({ page, shareMenu }) => {
await shareMenu.open()
await shareMenu.exportTabButton.click()
await expect(shareMenu.exportTabPage).toBeVisible()
const themeSelect = page.locator('#tla-export-theme-select')
// Selecting an option closes the select but leaves the share menu open.
await themeSelect.click()
await expect(page.getByRole('listbox')).toBeVisible()
await page.getByRole('option').last().click()
await expect(page.getByRole('listbox')).toHaveCount(0)
await expect(shareMenu.exportTabPage).toBeVisible()
// Re-clicking the open select's trigger also closes only the select. The trigger
// is non-interactive while the select is open (Radix disables outside pointer
// events), so click its position directly rather than via the locator.
await themeSelect.click()
await expect(page.getByRole('listbox')).toBeVisible()
const box = await themeSelect.boundingBox()
if (!box) throw new Error('theme select not found')
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2)
await expect(page.getByRole('listbox')).toHaveCount(0)
await expect(shareMenu.exportTabPage).toBeVisible()
})
test('can unshare and reshare', async ({ page, browser, shareMenu }) => {
// Copy the link to the current file
await shareMenu.open()
expect(await shareMenu.isInviteButtonVisible()).toBe(true)
const url = await shareMenu.copyLink()
// Open link in an incognito window
const { newPage, newShareMenu, newContext } = await openNewTab(browser, {
url,
allowClipboard: true,
userProps: undefined,
})
// The second page should have the share button and not the error
await expect(newShareMenu.shareButton).toBeVisible()
await expect(newPage.getByTestId('tla-error')).not.toBeVisible()
// Now unshare it it...
await page.getByTestId('shared-link-shared-switch').click()
expect(await shareMenu.isToggleChecked()).toBe(false)
// Reload the second page
newPage.reload()
// The second page should have an error and not the share button
await expect(newShareMenu.shareButton).not.toBeVisible()
await expect(newPage.getByTestId('tla-error')).toBeVisible()
// Now reshare it...
await page.getByTestId('shared-link-shared-switch').click()
expect(await shareMenu.isToggleChecked()).toBe(true)
// Reload the second page again
newPage.reload()
// The second page should have the share button and not the error again
await expect(newShareMenu.shareButton).toBeVisible()
await expect(newPage.getByTestId('tla-error')).not.toBeVisible()
await newContext.close()
})
})
test.describe('signed in user on someone elses file', () => {
test('tabs work correctly', async ({ browser, sidebar, shareMenu }) => {
const newName = getRandomName()
await sidebar.renameFile(0, newName)
await shareMenu.open()
const url = await shareMenu.copyLink()
const parallelIndex = test.info().parallelIndex
// Open link in an incognito window
const { newShareMenu, newPage } = await openNewTab(browser, {
url,
allowClipboard: true,
userProps: { user: 'suppy', index: parallelIndex },
})
await expect(newPage.getByTestId('tla-sidebar').getByText(newName)).toBeVisible()
await newShareMenu.open()
expect(await newShareMenu.publishTabButton.isVisible()).toBe(false)
expect(await newShareMenu.inviteTabButton.isVisible()).toBe(false)
expect(await newShareMenu.exportTabButton.isVisible()).toBe(true)
expect(await newShareMenu.anonShareTabButton.isVisible()).toBe(true)
// Starts on the anon share tab
expect(await newShareMenu.inviteTabPage.isVisible()).toBe(false)
expect(await newShareMenu.exportTabPage.isVisible()).toBe(false)
expect(await newShareMenu.publishTabPage.isVisible()).toBe(false)
expect(await newShareMenu.anonShareTabPage.isVisible()).toBe(true)
// Can switch between tabs (export)
await newShareMenu.exportTabButton.click()
expect(await newShareMenu.inviteTabPage.isVisible()).toBe(false)
expect(await newShareMenu.exportTabPage.isVisible()).toBe(true)
expect(await newShareMenu.publishTabPage.isVisible()).toBe(false)
expect(await newShareMenu.anonShareTabPage.isVisible()).toBe(false)
})
})
test.describe('signed in user on published file', () => {
test('tabs work correctly', async ({ shareMenu, browser }) => {
// Publish the user's current file
await shareMenu.open()
expect(await shareMenu.isInviteButtonVisible()).toBe(true)
await shareMenu.publishFile()
const url = await shareMenu.copyLink()
// Open published file link in other user window
const { newShareMenu, newContext } = await openNewTab(browser, {
url,
allowClipboard: true,
userProps: { user: 'huppy', index: test.info().parallelIndex },
})
await expect(newShareMenu.shareButton).toBeVisible()
await newShareMenu.open()
expect(await newShareMenu.publishTabButton.isVisible()).toBe(false)
expect(await newShareMenu.inviteTabButton.isVisible()).toBe(false)
expect(await newShareMenu.exportTabButton.isVisible()).toBe(true)
expect(await newShareMenu.anonShareTabButton.isVisible()).toBe(true)
expect(await newShareMenu.publishTabPage.isVisible()).toBe(false)
expect(await newShareMenu.inviteTabPage.isVisible()).toBe(false)
expect(await newShareMenu.exportTabPage.isVisible()).toBe(false)
expect(await newShareMenu.anonShareTabPage.isVisible()).toBe(true) // show anon share tab page
// can copy the url
const copiedUrl = await newShareMenu.copyLink()
expect(new URL(copiedUrl).pathname).toBe(new URL(url).pathname)
// Switch to the export share tab
await newShareMenu.exportTabButton.click()
expect(await newShareMenu.exportTabPage.isVisible()).toBe(true)
expect(await newShareMenu.anonShareTabPage.isVisible()).toBe(false)
// Does not see the sidebar or the sidebar button
expect(await newShareMenu.page.getByTestId('tla-sidebar').count()).toBe(0)
expect(await newShareMenu.page.getByTestId('tla-sidebar-toggle').count()).toBe(0)
// Main user does see sidebar
expect(await shareMenu.page.getByTestId('tla-sidebar').count()).toBe(1)
expect(await shareMenu.page.getByTestId('tla-sidebar-toggle').count()).toBe(1)
await newContext.close()
})
})
test.describe('logged out user on scratch file', () => {
test('tabs work correctly', async ({ browser }) => {
const { newShareMenu, newContext } = await openNewTab(browser, {
allowClipboard: true,
userProps: undefined,
})
// In the incognito window, we are on the scratchpad
await expect(newShareMenu.shareButton).toBeVisible()
await newShareMenu.open()
expect(await newShareMenu.inviteTabButton.isVisible()).toBe(false)
expect(await newShareMenu.exportTabButton.isVisible()).toBe(true)
expect(await newShareMenu.publishTabButton.isVisible()).toBe(false)
expect(await newShareMenu.anonShareTabButton.isVisible()).toBe(false) // not shown on scratch
await newContext.close()
})
})
test.describe('logged out user on guest file', () => {
test('tabs work correctly', async ({ shareMenu, browser }) => {
// Share the signed in user's current file
await shareMenu.open()
expect(await shareMenu.isInviteButtonVisible()).toBe(true)
await shareMenu.shareFile()
const url = await shareMenu.copyLink()
// Open file link in an incognito window
const { newShareMenu, newContext } = await openNewTab(browser, {
url,
allowClipboard: true,
userProps: undefined,
})
await expect(newShareMenu.shareButton).toBeVisible()
await newShareMenu.open()
expect(await newShareMenu.publishTabButton.isVisible()).toBe(false)
expect(await newShareMenu.inviteTabButton.isVisible()).toBe(false)
expect(await newShareMenu.exportTabButton.isVisible()).toBe(true)
expect(await newShareMenu.anonShareTabButton.isVisible()).toBe(true)
expect(await newShareMenu.publishTabPage.isVisible()).toBe(false)
expect(await newShareMenu.inviteTabPage.isVisible()).toBe(false)
expect(await newShareMenu.exportTabPage.isVisible()).toBe(false)
expect(await newShareMenu.anonShareTabPage.isVisible()).toBe(true) // show anon share tab page
// can copy the url
const copiedUrl = await shareMenu.copyLink()
expect(copiedUrl).toBe(url)
// Switch to the export share tab
await newShareMenu.exportTabButton.click()
expect(await newShareMenu.exportTabPage.isVisible()).toBe(true)
expect(await newShareMenu.anonShareTabPage.isVisible()).toBe(false)
await newContext.close()
})
})
test.describe('logged out user on published file', () => {
test('tabs work correctly', async ({ shareMenu, browser }) => {
// Publish the user's current file
await shareMenu.open()
expect(await shareMenu.isInviteButtonVisible()).toBe(true)
await shareMenu.publishFile()
const url = await shareMenu.copyLink()
// Open published file link in an incognito window
const { newShareMenu, newContext, newEditor } = await openNewTab(browser, {
url,
allowClipboard: true,
userProps: undefined,
})
await expect(newShareMenu.shareButton).toBeVisible()
await newShareMenu.open()
expect(await newShareMenu.publishTabButton.isVisible()).toBe(false)
expect(await newShareMenu.inviteTabButton.isVisible()).toBe(false)
expect(await newShareMenu.exportTabButton.isVisible()).toBe(true)
expect(await newShareMenu.anonShareTabButton.isVisible()).toBe(true)
expect(await newShareMenu.publishTabPage.isVisible()).toBe(false)
expect(await newShareMenu.inviteTabPage.isVisible()).toBe(false)
expect(await newShareMenu.exportTabPage.isVisible()).toBe(false)
expect(await newShareMenu.anonShareTabPage.isVisible()).toBe(true) // show anon share tab page
// can copy the url
const copiedUrl = await shareMenu.copyLink()
expect(copiedUrl).toBe(url)
// Switch to the export share tab
await newShareMenu.exportTabButton.click()
expect(await newShareMenu.exportTabPage.isVisible()).toBe(true)
expect(await newShareMenu.anonShareTabPage.isVisible()).toBe(false)
// download the file
await newEditor.openPageMenu()
const downloadButton = newEditor.page.getByText('Download')
await expect(downloadButton).toBeVisible()
// check that the file downloaded
const downloadPromise = newEditor.page.waitForEvent('download')
await downloadButton.click()
const download = await downloadPromise
expect(download.suggestedFilename().endsWith('.tldr')).toBe(true)
await newContext.close()
})
})
/* ------------------- Exporting ------------------- */
test('can export a file as an image', async ({ page, shareMenu }) => {
await page.getByTestId('tools.rectangle').click()
await page.locator('.tl-background').click()
const downloadPromise = page.waitForEvent('download')
await shareMenu.open()
await shareMenu.exportFile()
const download = await downloadPromise
const suggestedFilename = download.suggestedFilename()
expect(suggestedFilename.endsWith('.png')).toBe(true)
const filePath = path.join('./test-results/', suggestedFilename)
await download.saveAs(filePath)
expect(fs.existsSync(filePath)).toBeTruthy()
const stats = fs.statSync(filePath)
expect(stats.size).toBeGreaterThan(0)
})
test('can follow a deep link to a never-seen file', async ({ editor, browser, shareMenu }) => {
const text = Math.random().toString(36).substring(2, 15)
await editor.createNewPage()
await editor.createTextShape(text)
await shareMenu.open()
expect(await shareMenu.isInviteButtonVisible()).toBe(true)
const url = await shareMenu.copyLink()
// close the share menu
await shareMenu.page.keyboard.press('Escape')
// create a new empty page
await editor.createNewPage()
const { newEditor } = await openNewTab(browser, {
url,
allowClipboard: true,
userProps: { user: 'suppy', index: test.info().parallelIndex },
})
await newEditor.expectShapesCount(1)
expect(newEditor.page.getByText(text).last()).toBeVisible()
})
test('can follow a deep link to an already-seen file', async ({ editor, shareMenu, browser }) => {
const text = Math.random().toString(36).substring(2, 15)
await shareMenu.open()
await shareMenu.inviteTabButton.click()
// close the share menu
await shareMenu.page.keyboard.press('Escape')
const { newEditor, newShareMenu } = await openNewTab(browser, {
url: editor.page.url(),
allowClipboard: true,
userProps: { user: 'suppy', index: test.info().parallelIndex },
})
await newEditor.createNewPage()
await newEditor.createTextShape(text)
await newEditor.expectShapesCount(1)
await newShareMenu.open()
await newShareMenu.anonShareTabButton.click()
const url = await newShareMenu.copyLink()
await newEditor.createNewPage()
await editor.page.goto(url)
await editor.expectShapesCount(1)
await expect(editor.page.getByText(text).last()).toBeVisible()
})

View File

@@ -0,0 +1,33 @@
import { expect, test } from '../../fixtures/tla-test'
const DOT_DEV_LOCAL_STORAGE_KEY = 'showDotDevLink'
const DOT_DEV_URL =
'https://tldraw.dev?utm_source=dotcom&utm_medium=organic&utm_campaign=sidebar-link'
test.describe('sidebar dot dev link', () => {
test.beforeEach(async ({ page, homePage, editor }) => {
await editor.ensureSidebarOpen()
await page.evaluate((key) => {
// eslint-disable-next-line tldraw/no-direct-storage
window.localStorage.removeItem(key)
}, DOT_DEV_LOCAL_STORAGE_KEY)
await page.reload()
await homePage.isLoaded()
await editor.ensureSidebarOpen()
})
test('is visible with expected link', async ({ page }) => {
const link = page.getByTestId('tla-sidebar-dotdev-link')
await expect(link).toBeVisible()
await expect(link).toHaveAttribute('href', DOT_DEV_URL)
})
test('clicking opens tldraw.dev in a new tab', async ({ page }) => {
const link = page.getByTestId('tla-sidebar-dotdev-link')
// Verify the link would open in a new tab
await expect(link).toHaveAttribute('target', '_blank')
})
})

View File

@@ -0,0 +1,64 @@
import { USERS } from '../../consts'
import { expect, test } from '../../fixtures/tla-test'
// Don't use stored credentials for these dialog-specific tests
test.use({ storageState: { cookies: [], origins: [] } })
test.describe('SignInDialog', () => {
test('can sign in via email code using the custom dialog', async ({
homePage,
editor,
signInDialog,
}) => {
const user = USERS[test.info().parallelIndex]
await homePage.expectSignInButtonVisible()
await homePage.signInButton.click()
await signInDialog.continueWithEmail(user)
await signInDialog.expectCodeStageVisible()
await signInDialog.fillCode('424242')
await editor.isLoaded()
await expect(homePage.signInButton).not.toBeVisible()
await expect(editor.sidebarToggle).toBeVisible()
})
test('resend is available on code stage', async ({ homePage, signInDialog }) => {
const user = USERS[test.info().parallelIndex]
await homePage.expectSignInButtonVisible()
await homePage.signInButton.click()
await signInDialog.continueWithEmail(user)
await signInDialog.expectCodeStageVisible()
await signInDialog.clickResend()
// Code input should still be visible and empty after resend
await expect(signInDialog.codeInput).toBeVisible()
})
// Dialog close behaviors
test('can close dialog during code entry stage', async ({ homePage, signInDialog, page }) => {
const user = USERS[test.info().parallelIndex]
await homePage.expectSignInButtonVisible()
await homePage.signInButton.click()
// Complete email stage
await signInDialog.continueWithEmail(user)
await signInDialog.expectCodeStageVisible()
// Close dialog without entering code
await page.keyboard.press('Escape')
// Verify dialog is closed and user remains signed out
await expect(signInDialog.codeInput).not.toBeVisible()
await homePage.expectSignInButtonVisible()
// Verify user can restart flow by reopening dialog
await homePage.signInButton.click()
await signInDialog.expectInitialElements()
})
})

View File

@@ -0,0 +1,117 @@
import fs from 'fs'
import { USERS } from '../../consts'
import { expect, test } from '../../fixtures/tla-test'
function validateTldrJson(json: unknown) {
expect(json).toMatchObject({
tldrawFileFormatVersion: 1,
schema: expect.objectContaining({ schemaVersion: expect.any(Number) }),
records: expect.arrayContaining([expect.objectContaining({ typeName: 'document' })]),
})
}
// ------------------------------------------------------------------ import --
test.describe('import from URL (signed in)', () => {
test.beforeEach(async ({ editor }) => {
await editor.isLoaded()
})
test('redirects to root when no url param is provided', async ({ page }) => {
await page.goto('http://localhost:3000/import')
// Should redirect away from /import immediately (signed-in users then get
// forwarded on to their most-recent file, so we can't assert a specific URL).
await expect(page).not.toHaveURL(/\/import/)
})
test('shows import-failed toast when the url returns a non-200 response', async ({ page }) => {
const badUrl = 'https://e2e-mock.tldraw.xyz/not-found.tldr'
await page.route(badUrl, (route) => route.fulfill({ status: 404 }))
await page.goto(`http://localhost:3000/import?url=${encodeURIComponent(badUrl)}`)
await expect(page).not.toHaveURL(/\/import/)
await expect(page.getByText('Import failed', { exact: true })).toBeVisible()
})
test('shows import-failed toast when the url returns invalid json', async ({ page }) => {
const badUrl = 'https://e2e-mock.tldraw.xyz/not-a-tldr-file.txt'
await page.route(badUrl, (route) =>
route.fulfill({
status: 200,
contentType: 'text/plain',
headers: { 'Access-Control-Allow-Origin': '*' },
body: 'this is not a tldraw file',
})
)
await page.goto(`http://localhost:3000/import?url=${encodeURIComponent(badUrl)}`)
await expect(page).not.toHaveURL(/\/import/)
await expect(page.getByText('Import failed', { exact: true })).toBeVisible()
})
})
test.describe('import from URL (signed out)', () => {
test.use({ storageState: { cookies: [], origins: [] } })
test('shows sign-in dialog; after email sign-in navigates to the imported file', async ({
page,
signInDialog,
editor,
importHelper,
}) => {
const user = USERS[test.info().parallelIndex]
await importHelper.mockUrl()
// Navigate to the import URL while signed out.
// import.tsx will store the redirect and show the sign-in dialog.
await importHelper.navigate()
await signInDialog.expectInitialElements()
await signInDialog.continueWithEmail(user)
await signInDialog.expectCodeStageVisible()
await signInDialog.fillCode('424242')
// After sign-in, the import flow runs and navigates to the new file.
await page.waitForURL(/\/f\//, { timeout: 20000 })
await editor.isLoaded()
})
})
// ---------------------------------------------------------------- download --
test.describe('download', () => {
test.beforeEach(async ({ editor }) => {
await editor.isLoaded()
})
test('can download a file that is not currently open from the sidebar', async ({
page,
sidebar,
editor,
}) => {
await editor.ensureSidebarOpen()
// Create a second file so the sidebar has two entries.
await sidebar.createNewDocument('download-test-secondary')
// Navigate to the older file (index 1 = the one that was open originally).
await sidebar.getFileLink('today', 1).click()
await editor.isLoaded()
// Download the newer file (index 0) which is not currently open.
const fileLink = sidebar.getFileLink('today', 0)
await fileLink.hover()
await fileLink.getByRole('button').click()
const downloadPromise = page.waitForEvent('download')
await page.getByRole('menuitem', { name: 'Download' }).click()
const download = await downloadPromise
const filePath = await download.path()
expect(filePath).toBeTruthy()
const json = JSON.parse(fs.readFileSync(filePath!, 'utf-8'))
validateTldrJson(json)
expect(download.suggestedFilename()).toContain('download-test-secondary')
expect(download.suggestedFilename()).toMatch(/\.tldr$/)
})
})

View File

@@ -0,0 +1,513 @@
import { OTHER_USERS } from '../../consts'
import { getRandomName, openNewTab } from '../../fixtures/helpers'
import { SignInDialog } from '../../fixtures/SignInDialog'
import { expect, test } from '../../fixtures/tla-test'
// The sidebar has a workspace switcher dropdown at the top (home workspace + the
// user's workspaces + a create item), action rows for the active non-home
// workspace (new board, invite teammates, workspace settings), and the active
// workspace's files below. Switching to a workspace opens its top file — first pinned,
// otherwise most recent (creating one if it's empty) — and creating a
// workspace switches to it.
test.describe('workspaces', () => {
test.beforeEach(async ({ editor }) => {
await editor.isLoaded()
await editor.ensureSidebarOpen()
})
test.describe('basic operations', () => {
test('a workspace can be created from the switcher dropdown', async ({ page, sidebar }) => {
const workspaceName = getRandomName()
await sidebar.createWorkspace(workspaceName)
// creating remains available from the workspace switcher dropdown
await sidebar.openWorkspaceSwitcher()
await expect(page.getByTestId('tla-create-workspace-menu-item')).toBeVisible()
await page.keyboard.press('Escape')
})
test('reopening the switcher right after switching keeps it open', async ({
page,
sidebar,
}) => {
// Regression test for the workspace switcher dismissing itself when reopened
// mid-switch. Selecting a workspace navigates immediately, but the target
// file's canvas loads asynchronously and (deep links are on for the file
// route) rewrites the URL with a `?d=` param once its editor mounts. As the
// incoming canvas mounts it steals focus, and that focus shift used to dismiss
// the just-reopened switcher.
const workspaceName = getRandomName()
await sidebar.createWorkspace(workspaceName)
const trigger = page.getByTestId('tla-workspace-switcher')
const homeItem = page.getByTestId('tla-workspace-switcher-home')
// Begin switching back to Home; the selection closes the menu.
await sidebar.openWorkspaceSwitcher()
await homeItem.click()
await expect(homeItem).toBeHidden()
// Reopen immediately, before the incoming canvas has finished loading. Use a
// single click, not sidebar.openWorkspaceSwitcher(), whose retry loop would
// mask a dismissal by simply reopening the menu.
await trigger.click()
await expect(homeItem).toBeVisible()
// The switcher must stay open while the incoming canvas finishes loading,
// rewrites the URL with `?d=`, and steals focus — that focus steal is what
// used to dismiss it.
await page.waitForURL(/[?&]d=/, { timeout: 15000 })
await sidebar.expectActiveHomeWorkspace()
await expect(homeItem).toBeVisible()
})
test('clicking a file while the switcher is open closes it and opens that file', async ({
page,
sidebar,
}) => {
const file1 = getRandomName()
const file2 = getRandomName()
const file3 = getRandomName()
await sidebar.createNewDocument(file1)
await sidebar.createNewDocument(file2)
await sidebar.createNewDocument(file3)
await sidebar.expectFileActive(file3)
await sidebar.openWorkspaceSwitcher()
const homeItem = page.getByTestId('tla-workspace-switcher-home')
await expect(homeItem).toBeVisible()
// Clicking a file link while the switcher is open should both dismiss the switcher
// and open that file — letting the click fall through to the sidebar is intended.
const menuBox = await page.locator('[role="menu"]').boundingBox()
if (!menuBox) throw new Error('Missing workspace switcher menu')
const target = await page.evaluate((menuBottom) => {
const links = Array.from(
document.querySelectorAll<HTMLElement>('[data-element="file-link"]')
)
for (const link of links) {
if (link.dataset.active === 'true') continue
const rect = link.getBoundingClientRect()
// Pick a link below the open menu so the click lands on the link, not the menu.
if (rect.top < menuBottom) continue
return {
name: link.querySelector('[data-testid*="-name"]')?.textContent?.trim() ?? '',
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
}
}
throw new Error('Could not find a non-active file link below the switcher menu')
}, menuBox.y + menuBox.height)
await page.mouse.click(target.x, target.y)
await expect(homeItem).toBeHidden()
await sidebar.expectFileActive(target.name)
})
test('closing the mobile sidebar closes open sidebar menus', async ({ page, sidebar }) => {
const fileName = getRandomName()
await sidebar.createNewDocument(fileName)
await page.setViewportSize({ width: 390, height: 844 })
const mobileToggle = page.getByTestId('tla-sidebar-toggle-mobile')
const mobileOverlay = page.getByTestId('tla-sidebar-overlay-mobile')
const closeMobileSidebar = async () => {
await mobileOverlay.click({ position: { x: 380, y: 422 } })
await expect(mobileOverlay).toBeHidden()
}
await expect(mobileToggle).toBeVisible()
await mobileToggle.click()
await expect(mobileOverlay).toBeVisible()
// The sidebar only CSS-hides (it doesn't unmount), so an open menu would otherwise
// stay open behind it. Closing the mobile sidebar must close the workspace switcher...
await sidebar.openWorkspaceSwitcher()
await expect(page.getByTestId('tla-workspace-switcher-home')).toBeVisible()
await closeMobileSidebar()
await expect(page.getByTestId('tla-workspace-switcher-home')).toBeHidden()
// ...and a file's context menu.
await mobileToggle.click()
await expect(mobileOverlay).toBeVisible()
const fileLink = sidebar.getFileByName(fileName)
await fileLink.hover()
await fileLink.getByRole('button').click()
await expect(page.getByRole('menuitem', { name: 'Rename' })).toBeVisible()
await closeMobileSidebar()
await expect(page.getByRole('menuitem', { name: 'Rename' })).toBeHidden()
})
test('clicking the canvas closes the workspace switcher without reaching the canvas', async ({
page,
sidebar,
}) => {
const shapeId = await page.evaluate(() => {
const editor = (window as any).editor
editor.createShapes([
{
type: 'geo',
x: 100,
y: 100,
props: { geo: 'rectangle', w: 100, h: 100 },
},
])
const shape = editor.getCurrentPageShapes()[0]
editor.select(shape.id)
return shape.id
})
await sidebar.openWorkspaceSwitcher()
const homeItem = page.getByTestId('tla-workspace-switcher-home')
await expect(homeItem).toBeVisible()
await page.mouse.click(600, 300)
await expect(homeItem).toBeHidden()
expect(
await page.evaluate(() => {
const editor = (window as any).editor
return editor.getSelectedShapeIds()
})
).toEqual([shapeId])
})
test('create file button creates in the active workspace', async ({ sidebar }) => {
const workspaceName = getRandomName()
const file1 = getRandomName()
const file2 = getRandomName()
const homeFileName = getRandomName()
await sidebar.createNewDocument(file1)
await sidebar.createNewDocument(homeFileName)
await sidebar.createWorkspace(workspaceName)
await sidebar.switchToHomeWorkspace()
await sidebar.moveFileToWorkspace(file1, workspaceName)
await sidebar.switchToWorkspace(workspaceName)
await sidebar.createNewDocument(file2)
await sidebar.expectActiveWorkspace(workspaceName)
const filesInWorkspace = await sidebar.getVisibleFiles()
expect(filesInWorkspace).toContain(file1)
expect(filesInWorkspace).toContain(file2)
await sidebar.switchToHomeWorkspace()
await sidebar.expectFileNotVisible(file2)
await sidebar.expectFileVisible(homeFileName)
})
})
test.describe('file operations', () => {
test('move file between workspaces via the menu', async ({ page, sidebar, editor }) => {
const workspace1 = getRandomName()
const workspace2 = getRandomName()
const file1 = getRandomName()
const file2 = getRandomName()
const homeFileName = getRandomName()
await sidebar.createNewDocument(file1)
await sidebar.createNewDocument(file2)
await sidebar.createNewDocument(homeFileName)
await sidebar.createWorkspace(workspace1)
await sidebar.createWorkspace(workspace2)
await sidebar.switchToHomeWorkspace()
await sidebar.moveFileToWorkspace(file1, workspace1)
await sidebar.moveFileToWorkspace(file2, workspace1)
await sidebar.switchToWorkspace(workspace1)
// make sure the file we move is not the active one
await sidebar.getFileByName(file2).click()
await sidebar.expectFileActive(file2)
await sidebar.moveFileToWorkspace(file1, workspace2)
await sidebar.expectFileNotVisible(file1)
await sidebar.expectFileVisible(file2)
await sidebar.switchToWorkspace(workspace2)
await sidebar.expectFileVisible(file1)
await page.reload()
await editor.isLoaded()
await editor.ensureSidebarOpen()
await sidebar.switchToWorkspace(workspace2)
await sidebar.expectFileVisible(file1)
await sidebar.switchToWorkspace(workspace1)
await sidebar.expectFileVisible(file2)
await sidebar.expectFileNotVisible(file1)
})
test('move file to Home', async ({ sidebar }) => {
const workspaceName = getRandomName()
const file1 = getRandomName()
const file2 = getRandomName()
const homeFileName = getRandomName()
await sidebar.createNewDocument(file1)
await sidebar.createNewDocument(file2)
await sidebar.createNewDocument(homeFileName)
await sidebar.createWorkspace(workspaceName)
await sidebar.switchToHomeWorkspace()
await sidebar.moveFileToWorkspace(file1, workspaceName)
await sidebar.moveFileToWorkspace(file2, workspaceName)
await sidebar.switchToWorkspace(workspaceName)
// make sure the file we move is not the active one
await sidebar.getFileByName(file2).click()
await sidebar.expectFileActive(file2)
await sidebar.moveFileToHome(file1)
await sidebar.expectFileNotVisible(file1)
await sidebar.expectFileVisible(file2)
await sidebar.switchToHomeWorkspace()
await sidebar.expectFileVisible(file1)
await sidebar.expectFileVisible(homeFileName)
})
test('move to menu is a checklist of every workspace', async ({ page, sidebar }) => {
const workspace1 = getRandomName()
const workspace2 = getRandomName()
const file1 = getRandomName()
await sidebar.createNewDocument(file1)
await sidebar.createWorkspace(workspace1)
await sidebar.createWorkspace(workspace2)
await sidebar.switchToHomeWorkspace()
const homeWorkspaceName = await sidebar.getHomeWorkspaceName()
// In Home: the file lives in the home workspace, which is checked; every other
// workspace is offered unchecked (an unchecked item's accessible name is
// just its label, so exact matching works for those).
await sidebar.openMoveToMenu(file1)
await expect(
page.getByRole('menuitemcheckbox', { name: homeWorkspaceName, checked: true })
).toBeVisible()
await expect(
page.getByRole('menuitemcheckbox', { name: workspace1, exact: true })
).toBeVisible()
await expect(
page.getByRole('menuitemcheckbox', { name: workspace2, exact: true })
).toBeVisible()
await sidebar.closeMoveToMenu()
await sidebar.moveFileToWorkspace(file1, workspace1)
// In workspace1: workspace1 is now the checked item; the home workspace and the
// other workspace are offered unchecked.
await sidebar.switchToWorkspace(workspace1)
await sidebar.openMoveToMenu(file1)
await expect(
page.getByRole('menuitemcheckbox', { name: workspace1, checked: true })
).toBeVisible()
await expect(
page.getByRole('menuitemcheckbox', { name: homeWorkspaceName, exact: true })
).toBeVisible()
await expect(
page.getByRole('menuitemcheckbox', { name: workspace2, exact: true })
).toBeVisible()
await sidebar.closeMoveToMenu()
})
test('pin and unpin files', async ({ page, sidebar, editor }) => {
const file1 = getRandomName()
const file2 = getRandomName()
const file3 = getRandomName()
await sidebar.createNewDocument(file1)
await sidebar.createNewDocument(file2)
await sidebar.createNewDocument(file3)
await sidebar.expectFileNotPinned(file1)
await sidebar.expectFileNotPinned(file2)
await sidebar.expectFileNotPinned(file3)
await sidebar.pinFile(file2)
await sidebar.expectFilePinned(file2)
let fileOrder = await sidebar.getVisibleFiles()
expect(fileOrder[0]).toBe(file2)
await sidebar.pinFile(file1)
await sidebar.expectFilePinned(file1)
fileOrder = await sidebar.getVisibleFiles()
const file1Index = fileOrder.indexOf(file1)
const file2Index = fileOrder.indexOf(file2)
const file3Index = fileOrder.indexOf(file3)
expect(file1Index).toBeLessThan(file3Index)
expect(file2Index).toBeLessThan(file3Index)
await sidebar.unpinFile(file2)
await sidebar.expectFileNotPinned(file2)
await sidebar.expectFilePinned(file1)
await page.reload()
await editor.isLoaded()
await editor.ensureSidebarOpen()
await sidebar.expectFilePinned(file1)
await sidebar.expectFileNotPinned(file2)
await sidebar.expectFileNotPinned(file3)
fileOrder = await sidebar.getVisibleFiles()
expect(fileOrder[0]).toBe(file1)
})
test('dragging a pinned file does not unpin it', async ({ sidebar }) => {
const workspaceName = getRandomName()
const file1 = getRandomName()
await sidebar.createWorkspace(workspaceName)
await sidebar.createNewDocument(file1)
// Pinning happens through the file menu.
await sidebar.pinFile(file1)
await sidebar.expectFilePinned(file1)
// Dragging a file is now just native link dragging, so dragging a
// pinned file out of the pinned section no longer unpins it.
await sidebar.dragFileToUnpinnedSection(file1)
await sidebar.expectFilePinned(file1)
})
test('deleting the active file in a workspace stays in the workspace', async ({
sidebar,
deleteFileDialog,
}) => {
const workspaceName = getRandomName()
const file1 = getRandomName()
const file2 = getRandomName()
const homeFileName = getRandomName()
await sidebar.createNewDocument(file1)
await sidebar.createNewDocument(file2)
await sidebar.createNewDocument(homeFileName)
await sidebar.createWorkspace(workspaceName)
await sidebar.switchToHomeWorkspace()
await sidebar.moveFileToWorkspace(file1, workspaceName)
await sidebar.moveFileToWorkspace(file2, workspaceName)
await sidebar.switchToWorkspace(workspaceName)
await sidebar.getFileByName(file2).click()
await sidebar.expectFileActive(file2)
await sidebar.deleteFileByName(file2)
await deleteFileDialog.expectIsVisible()
await deleteFileDialog.confirmDeletion()
await deleteFileDialog.expectIsNotVisible()
// We should land on the workspace's remaining file, not jump to the home workspace.
await sidebar.expectFileNotVisible(file2)
await sidebar.expectActiveWorkspace(workspaceName)
await sidebar.expectFileActive(file1)
})
})
test.describe('sharing', () => {
test.beforeEach(async ({ context }) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
})
test('invite already member user to workspace', async ({ sidebar, browser }) => {
const workspaceName = getRandomName()
const fileName = getRandomName()
const homeFileName = getRandomName()
await sidebar.createNewDocument(fileName)
await sidebar.createNewDocument(homeFileName)
await sidebar.createWorkspace(workspaceName)
await sidebar.switchToHomeWorkspace()
await sidebar.moveFileToWorkspace(fileName, workspaceName)
const inviteUrl = await sidebar.copyWorkspaceInviteLink(workspaceName)
const parallelIndex = test.info().parallelIndex
const { newSidebar, newEditor, newWorkspaceInviteDialog, newContext } = await openNewTab(
browser,
{
url: inviteUrl,
allowClipboard: true,
userProps: { user: 'suppy', index: parallelIndex },
}
)
await newEditor.isLoaded()
await newEditor.ensureSidebarOpen()
await newWorkspaceInviteDialog.acceptInvitation()
await newSidebar.expectWorkspaceVisible(workspaceName)
await newSidebar.expectFileVisible(fileName)
// Visiting the same invite link again lands the member back in the workspace
const newTab = await openNewTab(browser, {
url: inviteUrl,
allowClipboard: true,
userProps: { user: 'suppy', index: parallelIndex },
})
await newTab.newEditor.isLoaded()
await newTab.newEditor.ensureSidebarOpen()
await newTab.newSidebar.expectWorkspaceVisible(workspaceName)
await newTab.newSidebar.expectFileVisible(fileName)
await newTab.newContext.close()
await newContext.close()
})
test('signing in through the invite dialog completes the workspace join', async ({
sidebar,
browser,
}) => {
const workspaceName = getRandomName()
const fileName = getRandomName()
const homeFileName = getRandomName()
await sidebar.createNewDocument(fileName)
await sidebar.createNewDocument(homeFileName)
await sidebar.createWorkspace(workspaceName)
await sidebar.switchToWorkspace('Home')
await sidebar.moveFileToWorkspace(fileName, workspaceName)
const inviteUrl = await sidebar.copyWorkspaceInviteLink(workspaceName)
const parallelIndex = test.info().parallelIndex
const { newPage, newSidebar, newEditor, newWorkspaceInviteDialog, newContext } =
await openNewTab(browser, {
url: inviteUrl,
allowClipboard: true,
userProps: undefined,
})
// Signed out, the invite link shows the sign-in dialog naming the
// workspace. Sign in through it: the invite token survives the dialog, so
// the signed-in invite flow picks it up and offers the join afterwards.
await expect(newPage.locator('strong', { hasText: workspaceName })).toBeVisible()
const signInDialog = new SignInDialog(newPage)
await signInDialog.continueWithEmail(OTHER_USERS[parallelIndex])
await signInDialog.expectCodeStageVisible()
await signInDialog.fillCode('424242')
await newEditor.isLoaded()
await newEditor.ensureSidebarOpen()
await newWorkspaceInviteDialog.acceptInvitation()
await newSidebar.expectWorkspaceVisible(workspaceName)
await newSidebar.expectFileVisible(fileName)
await newContext.close()
})
})
})