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:
110
apps/dotcom/client/e2e/tests/smoke/cookie-consent.spec.ts
Normal file
110
apps/dotcom/client/e2e/tests/smoke/cookie-consent.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
35
apps/dotcom/client/e2e/tests/smoke/database.spec.ts
Normal file
35
apps/dotcom/client/e2e/tests/smoke/database.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
162
apps/dotcom/client/e2e/tests/smoke/homepage.spec.ts
Normal file
162
apps/dotcom/client/e2e/tests/smoke/homepage.spec.ts
Normal 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)
|
||||
})
|
||||
47
apps/dotcom/client/e2e/tests/smoke/not-logged-in.spec.ts
Normal file
47
apps/dotcom/client/e2e/tests/smoke/not-logged-in.spec.ts
Normal 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')
|
||||
})
|
||||
383
apps/dotcom/client/e2e/tests/smoke/sharing.spec.ts
Normal file
383
apps/dotcom/client/e2e/tests/smoke/sharing.spec.ts
Normal 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()
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
64
apps/dotcom/client/e2e/tests/smoke/signin-dialog.spec.ts
Normal file
64
apps/dotcom/client/e2e/tests/smoke/signin-dialog.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
117
apps/dotcom/client/e2e/tests/smoke/test-import-download.spec.ts
Normal file
117
apps/dotcom/client/e2e/tests/smoke/test-import-download.spec.ts
Normal 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$/)
|
||||
})
|
||||
})
|
||||
513
apps/dotcom/client/e2e/tests/smoke/workspaces.spec.ts
Normal file
513
apps/dotcom/client/e2e/tests/smoke/workspaces.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user