diff --git a/apps/docs/content/docs/developers/embedding/authoring/v2.mdx b/apps/docs/content/docs/developers/embedding/authoring/v2.mdx index deaabd542..8836548f1 100644 --- a/apps/docs/content/docs/developers/embedding/authoring/v2.mdx +++ b/apps/docs/content/docs/developers/embedding/authoring/v2.mdx @@ -115,9 +115,10 @@ const EnvelopeEditor = ({ presignToken, envelopeId }) => { ### Create Component Only -| Prop | Type | Required | Description | -| ------ | ------------------------------ | -------- | --------------------------------------------------- | -| `type` | `"DOCUMENT"` \| `"TEMPLATE"` | Yes | Whether to create a document or template envelope | +| Prop | Type | Required | Description | +| ---------- | ------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `type` | `"DOCUMENT"` \| `"TEMPLATE"` | Yes | Whether to create a document or template envelope | +| `folderId` | `string` | No | The ID of the folder to create the envelope in. If not provided, the envelope is created in the root folder. The folder must match the envelope type and team. | ### Update Component Only diff --git a/apps/remix/app/routes/embed+/playground.tsx b/apps/remix/app/routes/embed+/playground.tsx index e135709cf..d3cd08be7 100644 --- a/apps/remix/app/routes/embed+/playground.tsx +++ b/apps/remix/app/routes/embed+/playground.tsx @@ -36,6 +36,7 @@ export default function EmbedPlaygroundPage() { const [envelopeType, setEnvelopeType] = useState<'DOCUMENT' | 'TEMPLATE'>( () => (searchParams.get('envelopeType') as 'DOCUMENT' | 'TEMPLATE') || 'DOCUMENT', ); + const [folderId, setFolderId] = useState(() => searchParams.get('folderId') || ''); // Auto-launch if query params are present on mount const [iframeSrc, setIframeSrc] = useState(null); @@ -201,6 +202,7 @@ export default function EmbedPlaygroundPage() { mode: string; envelopeId: string; envelopeType: string; + folderId: string; }) => { const newParams = new URLSearchParams(); @@ -224,6 +226,10 @@ export default function EmbedPlaygroundPage() { newParams.set('envelopeType', params.envelopeType); } + if (params.folderId) { + newParams.set('folderId', params.folderId); + } + const qs = newParams.toString(); void navigate(qs ? `?${qs}` : '.', { replace: true }); @@ -263,6 +269,7 @@ export default function EmbedPlaygroundPage() { const hashData = { externalId: externalId || undefined, type: mode === 'create' ? envelopeType : undefined, + folderId: mode === 'create' && folderId ? folderId : undefined, darkModeDisabled: darkModeDisabled || undefined, css: rawCss || undefined, cssVars: Object.keys(filteredCssVars).length > 0 ? filteredCssVars : undefined, @@ -292,7 +299,7 @@ export default function EmbedPlaygroundPage() { setIframeSrc(buildIframeSrc(basePath, presignToken, hash)); setIframeKey((prev) => prev + 1); - updateQueryParams({ token: inputToken, externalId, mode, envelopeId, envelopeType }); + updateQueryParams({ token: inputToken, externalId, mode, envelopeId, envelopeType, folderId }); }; const handleSubmit = useCallback( @@ -306,6 +313,7 @@ export default function EmbedPlaygroundPage() { mode, envelopeId, envelopeType, + folderId, generalFeatures, settingsFeatures, actionsFeatures, @@ -324,6 +332,7 @@ export default function EmbedPlaygroundPage() { setMode('create'); setEnvelopeId(''); setEnvelopeType('DOCUMENT'); + setFolderId(''); setIframeSrc(null); setMessages([]); setTokenError(null); @@ -422,19 +431,34 @@ export default function EmbedPlaygroundPage() { {mode === 'create' && ( -
- - -
+ <> +
+ + +
+ +
+ + setFolderId(e.target.value)} + style={{ width: '100%', padding: '4px', fontSize: '12px' }} + placeholder="folder cuid" + /> +
+ )} {mode === 'edit' && ( diff --git a/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx b/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx index ab3665545..57925c5e7 100644 --- a/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx +++ b/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx @@ -358,7 +358,7 @@ const EnvelopeCreatePage = ({ embedAuthoringOptions }: EnvelopeCreatePageProps) publicDescription: '', userId: tokenUserId, teamId: tokenTeamId, - folderId: null, + folderId: embedAuthoringOptions?.folderId ?? null, documentMeta: { id: '', ...defaultDocumentMeta, diff --git a/packages/app-tests/e2e/envelope-editor-v2/envelope-editor-embedded-css.spec.ts b/packages/app-tests/e2e/envelope-editor-v2/envelope-editor-embedded-css.spec.ts deleted file mode 100644 index 5f9246f72..000000000 --- a/packages/app-tests/e2e/envelope-editor-v2/envelope-editor-embedded-css.spec.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { expect, test } from '@playwright/test'; - -import { prisma } from '@documenso/prisma'; - -import { - type TEnvelopeEditorSurface, - openEmbeddedEnvelopeEditor, -} from '../fixtures/envelope-editor'; - -const TEST_CSS_VARS = { - background: '#ff0000', - primary: '#00ff00', - radius: '1rem', -}; - -/** - * A unique CSS selector used for asserting raw CSS injection. - */ -const TEST_RAW_CSS = '.e2e-css-test-marker { color: red; }'; - -/** - * Expected HSL values after conversion by `toNativeCssVars`: - * - colord('#ff0000').toHsl() → { h: 0, s: 100, l: 50 } - * - colord('#00ff00').toHsl() → { h: 120, s: 100, l: 50 } - */ -const EXPECTED_CSS_VARS = { - '--background': '0 100 50', - '--primary': '120 100 50', - '--radius': '1rem', -}; - -const enableEmbedAuthoringWhiteLabel = async (userId: number) => { - const organisation = await prisma.organisation.findFirstOrThrow({ - where: { ownerUserId: userId }, - include: { organisationClaim: true }, - }); - - await prisma.organisationClaim.update({ - where: { id: organisation.organisationClaim.id }, - data: { - flags: { - allowLegacyEnvelopes: true, - embedAuthoringWhiteLabel: true, - }, - }, - }); -}; - -/** - * The default background color from the theme before any CSS injection. - * - * The theme default `--background: 0 0% 100%` resolves to hsl(0, 0%, 100%) which is white. - */ -const DEFAULT_BODY_BG_COLOR = 'rgb(255, 255, 255)'; - -/** - * When `--background` is set to `0 100 50` (hsl(0, 100%, 50%)) the body background - * resolves to pure red via the Tailwind `bg-background` → `hsl(var(--background))` chain. - */ -const INJECTED_BODY_BG_COLOR = 'rgb(255, 0, 0)'; - -const assertCssNotInjected = async (surface: TEnvelopeEditorSurface) => { - const { root: page } = surface; - - const cssState = await page.evaluate(() => { - const rootStyle = document.documentElement.style; - const bodyBgColor = window.getComputedStyle(document.body).backgroundColor; - - return { - background: rootStyle.getPropertyValue('--background'), - primary: rootStyle.getPropertyValue('--primary'), - radius: rootStyle.getPropertyValue('--radius'), - bodyBgColor, - hasInjectedStyle: Array.from(document.head.querySelectorAll('style')).some((el) => - el.innerHTML.includes('.e2e-css-test-marker'), - ), - }; - }); - - // CSS custom properties should not be set on the inline style. - expect(cssState.background).toBe(''); - expect(cssState.primary).toBe(''); - expect(cssState.radius).toBe(''); - - // No raw CSS style tag should be injected. - expect(cssState.hasInjectedStyle).toBe(false); - - // The body should still use the default theme background color. - expect(cssState.bodyBgColor).toBe(DEFAULT_BODY_BG_COLOR); -}; - -const assertCssInjected = async (surface: TEnvelopeEditorSurface) => { - const { root: page } = surface; - - const cssState = await page.evaluate(() => { - const rootStyle = document.documentElement.style; - const bodyBgColor = window.getComputedStyle(document.body).backgroundColor; - - return { - background: rootStyle.getPropertyValue('--background'), - primary: rootStyle.getPropertyValue('--primary'), - radius: rootStyle.getPropertyValue('--radius'), - bodyBgColor, - hasInjectedStyle: Array.from(document.head.querySelectorAll('style')).some((el) => - el.innerHTML.includes('.e2e-css-test-marker'), - ), - }; - }); - - // CSS custom properties should be set to the expected HSL values. - expect(cssState.background).toBe(EXPECTED_CSS_VARS['--background']); - expect(cssState.primary).toBe(EXPECTED_CSS_VARS['--primary']); - expect(cssState.radius).toBe(EXPECTED_CSS_VARS['--radius']); - - // Raw CSS style tag should be injected. - expect(cssState.hasInjectedStyle).toBe(true); - - // The body background should reflect the injected --background value (red). - expect(cssState.bodyBgColor).toBe(INJECTED_BODY_BG_COLOR); -}; - -const assertDarkModeDisabled = async (surface: TEnvelopeEditorSurface) => { - const { root: page } = surface; - - const hasDarkModeDisabled = await page.evaluate(() => - document.documentElement.classList.contains('dark-mode-disabled'), - ); - - expect(hasDarkModeDisabled).toBe(true); -}; - -test.describe('embedded create', () => { - test('cssVars and css respect embedAuthoringWhiteLabel flag', async ({ page }) => { - const surface = await openEmbeddedEnvelopeEditor(page, { - envelopeType: 'DOCUMENT', - mode: 'create', - tokenNamePrefix: 'e2e-embed-css', - css: TEST_RAW_CSS, - cssVars: TEST_CSS_VARS, - darkModeDisabled: true, - }); - - // darkModeDisabled is applied regardless of the flag. - await assertDarkModeDisabled(surface); - - // Flag is disabled by default so CSS should NOT be injected. - await assertCssNotInjected(surface); - - // Enable the embedAuthoringWhiteLabel flag on the organisation claim. - await enableEmbedAuthoringWhiteLabel(surface.userId); - - // Reload the page to re-run the layout loader with the updated claim. - await page.reload(); - await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible(); - - // CSS should now be injected. - await assertCssInjected(surface); - - // darkModeDisabled should still be applied after reload. - await assertDarkModeDisabled(surface); - }); -}); - -test.describe('embedded edit', () => { - test('cssVars and css respect embedAuthoringWhiteLabel flag', async ({ page }) => { - const surface = await openEmbeddedEnvelopeEditor(page, { - envelopeType: 'TEMPLATE', - mode: 'edit', - tokenNamePrefix: 'e2e-embed-css', - css: TEST_RAW_CSS, - cssVars: TEST_CSS_VARS, - darkModeDisabled: true, - }); - - // darkModeDisabled is applied regardless of the flag. - await assertDarkModeDisabled(surface); - - // Flag is disabled by default so CSS should NOT be injected. - await assertCssNotInjected(surface); - - // Enable the embedAuthoringWhiteLabel flag on the organisation claim. - await enableEmbedAuthoringWhiteLabel(surface.userId); - - // Reload the page to re-run the layout loader with the updated claim. - await page.reload(); - await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible(); - - // CSS should now be injected. - await assertCssInjected(surface); - - // darkModeDisabled should still be applied after reload. - await assertDarkModeDisabled(surface); - }); -}); diff --git a/packages/app-tests/e2e/envelope-editor-v2/envelope-editor-embedded.spec.ts b/packages/app-tests/e2e/envelope-editor-v2/envelope-editor-embedded.spec.ts new file mode 100644 index 000000000..7126b1fe8 --- /dev/null +++ b/packages/app-tests/e2e/envelope-editor-v2/envelope-editor-embedded.spec.ts @@ -0,0 +1,436 @@ +import type { Page } from '@playwright/test'; +import { expect, test } from '@playwright/test'; + +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token'; +import { nanoid } from '@documenso/lib/universal/id'; +import { prisma } from '@documenso/prisma'; +import { seedUser } from '@documenso/prisma/seed/users'; + +import { + type TEnvelopeEditorSurface, + addEnvelopeItemPdf, + createEmbeddedEnvelopeCreateHash, + getEnvelopeEditorSettingsTrigger, + openEmbeddedEnvelopeEditor, + persistEmbeddedEnvelope, + setRecipientEmail, +} from '../fixtures/envelope-editor'; +import { expectToastTextToBeVisible } from '../fixtures/generic'; + +const TEST_CSS_VARS = { + background: '#ff0000', + primary: '#00ff00', + radius: '1rem', +}; + +/** + * A unique CSS selector used for asserting raw CSS injection. + */ +const TEST_RAW_CSS = '.e2e-css-test-marker { color: red; }'; + +/** + * Expected HSL values after conversion by `toNativeCssVars`: + * - colord('#ff0000').toHsl() → { h: 0, s: 100, l: 50 } + * - colord('#00ff00').toHsl() → { h: 120, s: 100, l: 50 } + */ +const EXPECTED_CSS_VARS = { + '--background': '0 100 50', + '--primary': '120 100 50', + '--radius': '1rem', +}; + +const enableEmbedAuthoringWhiteLabel = async (userId: number) => { + const organisation = await prisma.organisation.findFirstOrThrow({ + where: { ownerUserId: userId }, + include: { organisationClaim: true }, + }); + + await prisma.organisationClaim.update({ + where: { id: organisation.organisationClaim.id }, + data: { + flags: { + allowLegacyEnvelopes: true, + embedAuthoringWhiteLabel: true, + }, + }, + }); +}; + +/** + * The default background color from the theme before any CSS injection. + * + * The theme default `--background: 0 0% 100%` resolves to hsl(0, 0%, 100%) which is white. + */ +const DEFAULT_BODY_BG_COLOR = 'rgb(255, 255, 255)'; + +/** + * When `--background` is set to `0 100 50` (hsl(0, 100%, 50%)) the body background + * resolves to pure red via the Tailwind `bg-background` → `hsl(var(--background))` chain. + */ +const INJECTED_BODY_BG_COLOR = 'rgb(255, 0, 0)'; + +const assertCssNotInjected = async (surface: TEnvelopeEditorSurface) => { + const { root: page } = surface; + + const cssState = await page.evaluate(() => { + const rootStyle = document.documentElement.style; + const bodyBgColor = window.getComputedStyle(document.body).backgroundColor; + + return { + background: rootStyle.getPropertyValue('--background'), + primary: rootStyle.getPropertyValue('--primary'), + radius: rootStyle.getPropertyValue('--radius'), + bodyBgColor, + hasInjectedStyle: Array.from(document.head.querySelectorAll('style')).some((el) => + el.innerHTML.includes('.e2e-css-test-marker'), + ), + }; + }); + + // CSS custom properties should not be set on the inline style. + expect(cssState.background).toBe(''); + expect(cssState.primary).toBe(''); + expect(cssState.radius).toBe(''); + + // No raw CSS style tag should be injected. + expect(cssState.hasInjectedStyle).toBe(false); + + // The body should still use the default theme background color. + expect(cssState.bodyBgColor).toBe(DEFAULT_BODY_BG_COLOR); +}; + +const assertCssInjected = async (surface: TEnvelopeEditorSurface) => { + const { root: page } = surface; + + const cssState = await page.evaluate(() => { + const rootStyle = document.documentElement.style; + const bodyBgColor = window.getComputedStyle(document.body).backgroundColor; + + return { + background: rootStyle.getPropertyValue('--background'), + primary: rootStyle.getPropertyValue('--primary'), + radius: rootStyle.getPropertyValue('--radius'), + bodyBgColor, + hasInjectedStyle: Array.from(document.head.querySelectorAll('style')).some((el) => + el.innerHTML.includes('.e2e-css-test-marker'), + ), + }; + }); + + // CSS custom properties should be set to the expected HSL values. + expect(cssState.background).toBe(EXPECTED_CSS_VARS['--background']); + expect(cssState.primary).toBe(EXPECTED_CSS_VARS['--primary']); + expect(cssState.radius).toBe(EXPECTED_CSS_VARS['--radius']); + + // Raw CSS style tag should be injected. + expect(cssState.hasInjectedStyle).toBe(true); + + // The body background should reflect the injected --background value (red). + expect(cssState.bodyBgColor).toBe(INJECTED_BODY_BG_COLOR); +}; + +const assertDarkModeDisabled = async (surface: TEnvelopeEditorSurface) => { + const { root: page } = surface; + + const hasDarkModeDisabled = await page.evaluate(() => + document.documentElement.classList.contains('dark-mode-disabled'), + ); + + expect(hasDarkModeDisabled).toBe(true); +}; + +/** + * Open an embedded create editor with a pre-seeded user. This is needed for folderId + * tests where the folder must exist before the hash is built. + */ +const openEmbeddedCreateWithUser = async ( + page: Page, + user: { id: number; email: string; name: string | null }, + team: { id: number }, + options: { folderId?: string; tokenNamePrefix?: string }, +): Promise => { + const { token } = await createApiToken({ + userId: user.id, + teamId: team.id, + tokenName: `${options.tokenNamePrefix ?? 'e2e-embed-folder'}-document`, + expiresIn: null, + }); + + // Exchange API token for presign token. + const response = await page + .context() + .request.post(`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2/embedding/create-presign-token`, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + data: {}, + }); + + const data: unknown = await response.json(); + + if (typeof data !== 'object' || data === null || !('token' in data)) { + throw new Error(`Unexpected presign response: ${JSON.stringify(data)}`); + } + + const presignToken = data.token; + + if (typeof presignToken !== 'string' || presignToken.length === 0) { + throw new Error(`Unexpected presign response: ${JSON.stringify(data)}`); + } + + const hash = createEmbeddedEnvelopeCreateHash({ + envelopeType: 'DOCUMENT', + folderId: options.folderId, + }); + + await page.goto( + `/embed/v2/authoring/envelope/create?token=${encodeURIComponent(presignToken)}#${hash}`, + ); + + await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible(); + + return { + root: page, + isEmbedded: true, + envelopeType: 'DOCUMENT', + userId: user.id, + userEmail: user.email, + userName: user.name ?? '', + teamId: team.id, + }; +}; + +/** + * Helper to set an external ID on the envelope via the settings dialog so we can + * look it up in the database after creation. + */ +const setExternalIdViaSettings = async (surface: TEnvelopeEditorSurface, externalId: string) => { + await getEnvelopeEditorSettingsTrigger(surface.root).click(); + await expect(surface.root.getByRole('heading', { name: 'Document Settings' })).toBeVisible(); + + await surface.root.locator('input[name="externalId"]').fill(externalId); + await surface.root.getByRole('button', { name: 'Update' }).click(); +}; + +/** + * Minimal setup for an embedded create flow: upload a PDF and add a recipient + * so the "Create Document" button works. + */ +const setupMinimalEnvelope = async (surface: TEnvelopeEditorSurface, externalId: string) => { + await addEnvelopeItemPdf(surface.root); + await setRecipientEmail(surface.root, 0, `${nanoid()}@test.documenso.com`); + await setExternalIdViaSettings(surface, externalId); +}; + +/** + * Click "Create Document" and expect a failure toast instead of the success heading. + */ +const expectCreateToFail = async (surface: TEnvelopeEditorSurface) => { + const actionButtonName = + surface.envelopeType === 'DOCUMENT' ? 'Create Document' : 'Create Template'; + + await surface.root.getByRole('button', { name: actionButtonName }).click(); + await expectToastTextToBeVisible(surface.root, 'Failed to create document'); +}; + +test.describe('embedded create - folderId', () => { + test('creates envelope in the specified folder when folderId is provided', async ({ page }) => { + const { user, team } = await seedUser(); + + const folder = await prisma.folder.create({ + data: { + name: 'E2E Document Folder', + teamId: team.id, + userId: user.id, + type: 'DOCUMENT', + }, + }); + + const surface = await openEmbeddedCreateWithUser(page, user, team, { + folderId: folder.id, + tokenNamePrefix: 'e2e-embed-folder', + }); + + const externalId = `e2e-folder-create-${nanoid()}`; + + await setupMinimalEnvelope(surface, externalId); + await persistEmbeddedEnvelope(surface); + + const envelope = await prisma.envelope.findFirstOrThrow({ + where: { + externalId, + userId: surface.userId, + teamId: surface.teamId, + }, + }); + + expect(envelope.folderId).toBe(folder.id); + }); + + test('creates envelope in root folder when no folderId is provided', async ({ page }) => { + const surface = await openEmbeddedEnvelopeEditor(page, { + envelopeType: 'DOCUMENT', + mode: 'create', + tokenNamePrefix: 'e2e-embed-folder-none', + }); + + const externalId = `e2e-folder-root-${nanoid()}`; + + await setupMinimalEnvelope(surface, externalId); + await persistEmbeddedEnvelope(surface); + + const envelope = await prisma.envelope.findFirstOrThrow({ + where: { + externalId, + userId: surface.userId, + teamId: surface.teamId, + }, + }); + + expect(envelope.folderId).toBeNull(); + }); + + test('rejects creation when folderId has wrong folder type', async ({ page }) => { + const { user, team } = await seedUser(); + + // Create a TEMPLATE folder but attempt to create a DOCUMENT envelope in it. + const templateFolder = await prisma.folder.create({ + data: { + name: 'E2E Template Folder', + teamId: team.id, + userId: user.id, + type: 'TEMPLATE', + }, + }); + + const surface = await openEmbeddedCreateWithUser(page, user, team, { + folderId: templateFolder.id, + tokenNamePrefix: 'e2e-embed-folder-wrong-type', + }); + + const externalId = `e2e-folder-wrong-type-${nanoid()}`; + + await setupMinimalEnvelope(surface, externalId); + await expectCreateToFail(surface); + + // Verify no envelope was created with this externalId. + const count = await prisma.envelope.count({ where: { externalId } }); + expect(count).toBe(0); + }); + + test('rejects creation when folderId belongs to another team', async ({ page }) => { + // Create the user who will use the embedded editor. + const { user, team } = await seedUser(); + + // Create a second user/team that owns the folder. + const { user: otherUser, team: otherTeam } = await seedUser(); + + const otherTeamFolder = await prisma.folder.create({ + data: { + name: 'E2E Other Team Folder', + teamId: otherTeam.id, + userId: otherUser.id, + type: 'DOCUMENT', + }, + }); + + // Open the embedded editor for the first user but pass the folder from the other team. + const surface = await openEmbeddedCreateWithUser(page, user, team, { + folderId: otherTeamFolder.id, + tokenNamePrefix: 'e2e-embed-folder-no-perm', + }); + + const externalId = `e2e-folder-no-perm-${nanoid()}`; + + await setupMinimalEnvelope(surface, externalId); + await expectCreateToFail(surface); + + // Verify no envelope was created with this externalId. + const count = await prisma.envelope.count({ where: { externalId } }); + expect(count).toBe(0); + }); + + test('rejects creation when folderId does not exist', async ({ page }) => { + const surface = await openEmbeddedEnvelopeEditor(page, { + envelopeType: 'DOCUMENT', + mode: 'create', + tokenNamePrefix: 'e2e-embed-folder-nonexistent', + folderId: 'nonexistent-folder-id', + }); + + const externalId = `e2e-folder-nonexistent-${nanoid()}`; + + await setupMinimalEnvelope(surface, externalId); + await expectCreateToFail(surface); + + // Verify no envelope was created with this externalId. + const count = await prisma.envelope.count({ where: { externalId } }); + expect(count).toBe(0); + }); +}); + +test.describe('embedded create', () => { + test('cssVars and css respect embedAuthoringWhiteLabel flag', async ({ page }) => { + const surface = await openEmbeddedEnvelopeEditor(page, { + envelopeType: 'DOCUMENT', + mode: 'create', + tokenNamePrefix: 'e2e-embed-css', + css: TEST_RAW_CSS, + cssVars: TEST_CSS_VARS, + darkModeDisabled: true, + }); + + // darkModeDisabled is applied regardless of the flag. + await assertDarkModeDisabled(surface); + + // Flag is disabled by default so CSS should NOT be injected. + await assertCssNotInjected(surface); + + // Enable the embedAuthoringWhiteLabel flag on the organisation claim. + await enableEmbedAuthoringWhiteLabel(surface.userId); + + // Reload the page to re-run the layout loader with the updated claim. + await page.reload(); + await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible(); + + // CSS should now be injected. + await assertCssInjected(surface); + + // darkModeDisabled should still be applied after reload. + await assertDarkModeDisabled(surface); + }); +}); + +test.describe('embedded edit', () => { + test('cssVars and css respect embedAuthoringWhiteLabel flag', async ({ page }) => { + const surface = await openEmbeddedEnvelopeEditor(page, { + envelopeType: 'TEMPLATE', + mode: 'edit', + tokenNamePrefix: 'e2e-embed-css', + css: TEST_RAW_CSS, + cssVars: TEST_CSS_VARS, + darkModeDisabled: true, + }); + + // darkModeDisabled is applied regardless of the flag. + await assertDarkModeDisabled(surface); + + // Flag is disabled by default so CSS should NOT be injected. + await assertCssNotInjected(surface); + + // Enable the embedAuthoringWhiteLabel flag on the organisation claim. + await enableEmbedAuthoringWhiteLabel(surface.userId); + + // Reload the page to re-run the layout loader with the updated claim. + await page.reload(); + await expect(page.getByRole('heading', { name: 'Documents' })).toBeVisible(); + + // CSS should now be injected. + await assertCssInjected(surface); + + // darkModeDisabled should still be applied after reload. + await assertDarkModeDisabled(surface); + }); +}); diff --git a/packages/app-tests/e2e/fixtures/envelope-editor.ts b/packages/app-tests/e2e/fixtures/envelope-editor.ts index 1fc50ae5c..64ecd75b5 100644 --- a/packages/app-tests/e2e/fixtures/envelope-editor.ts +++ b/packages/app-tests/e2e/fixtures/envelope-editor.ts @@ -48,14 +48,16 @@ const encodeEmbeddedOptions = (options: Record) => { export const createEmbeddedEnvelopeCreateHash = ({ envelopeType, externalId, + folderId, features = DEFAULT_EMBEDDED_EDITOR_CONFIG, css, cssVars, darkModeDisabled, -}: { envelopeType: TEnvelopeEditorType } & TEmbeddedHashCommonOptions) => { +}: { envelopeType: TEnvelopeEditorType; folderId?: string } & TEmbeddedHashCommonOptions) => { return encodeEmbeddedOptions({ externalId, type: envelopeType, + folderId, features, css, cssVars, @@ -139,6 +141,7 @@ type OpenEmbeddedEnvelopeEditorOptions = { mode?: 'create' | 'edit'; tokenNamePrefix?: string; externalId?: string; + folderId?: string; features?: typeof DEFAULT_EMBEDDED_EDITOR_CONFIG; css?: string; cssVars?: Record; @@ -152,6 +155,7 @@ export const openEmbeddedEnvelopeEditor = async ( mode = 'create', tokenNamePrefix = 'e2e-embed', externalId, + folderId, features, css, cssVars, @@ -205,6 +209,7 @@ export const openEmbeddedEnvelopeEditor = async ( const hash = createEmbeddedEnvelopeCreateHash({ envelopeType, externalId, + folderId, features, css, cssVars, diff --git a/packages/lib/types/envelope-editor.ts b/packages/lib/types/envelope-editor.ts index 98a8f0d6b..d36d40acb 100644 --- a/packages/lib/types/envelope-editor.ts +++ b/packages/lib/types/envelope-editor.ts @@ -210,6 +210,7 @@ export const DEFAULT_EMBEDDED_EDITOR_CONFIG = { export const ZEmbedCreateEnvelopeAuthoringSchema = ZBaseEmbedDataSchema.extend({ externalId: z.string().optional(), type: z.nativeEnum(EnvelopeType), + folderId: z.string().optional(), features: z.object({}).passthrough().optional().default(DEFAULT_EMBEDDED_EDITOR_CONFIG), });