diff --git a/packages/sanity/playwright-ct.config.ts b/packages/sanity/playwright-ct.config.ts index 603293215800..391ca9a6586e 100644 --- a/packages/sanity/playwright-ct.config.ts +++ b/packages/sanity/playwright-ct.config.ts @@ -35,10 +35,10 @@ export default defineConfig({ ], /* Maximum time one test can run for. */ - timeout: 120 * 1000, + timeout: 10 * 1000, expect: { // Maximum time expect() should wait for the condition to be met. - timeout: 20 * 1000, + timeout: 5 * 1000, }, /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ diff --git a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Annotations.spec.tsx b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Annotations.spec.tsx index f47ed718ce7b..6438a4ab29dc 100644 --- a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Annotations.spec.tsx +++ b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Annotations.spec.tsx @@ -17,11 +17,7 @@ test.describe('Portable Text Input', () => { // Backtrack and click link icon in menu bar await page.keyboard.press('ArrowLeft') await page.keyboard.press('Shift+ArrowLeft+ArrowLeft+ArrowLeft+ArrowLeft') - await page - .getByRole('button') - .filter({has: page.locator('[data-sanity-icon="link"]')}) - .click() - + await page.getByRole('button', {name: 'Link'}).click() // Assertion: Wait for link to be re-rendered / PTE internal state to be done await expect($pte.locator('span[data-link]')).toBeVisible() diff --git a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Decorators.spec.tsx b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Decorators.spec.tsx index 3c33c2f07f36..7003806ef3c1 100644 --- a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Decorators.spec.tsx +++ b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Decorators.spec.tsx @@ -1,5 +1,4 @@ /* eslint-disable max-nested-callbacks */ -import Os from 'os' import {expect, test} from '@playwright/experimental-ct-react' import React from 'react' import {testHelpers} from '../../../utils/testHelpers' @@ -10,76 +9,105 @@ const DEFAULT_DECORATORS = [ name: 'strong', title: 'Strong', hotkey: 'b', + icon: 'bold', }, { name: 'em', title: 'Italic', hotkey: 'i', + icon: 'italic', }, { name: 'underline', title: 'Underline', hotkey: 'u', + icon: 'underline', }, { name: 'code', title: 'Code', hotkey: "'", + icon: 'code', }, { name: 'strike', title: 'Strike', hotkey: undefined, // Currently not defined + icon: 'strikethrough', }, ] test.describe('Portable Text Input', () => { test.describe('Decorators', () => { - test.describe('Keyboard shortcuts', () => { - test.beforeEach(({browserName}) => { - test.skip( - browserName === 'webkit' && Os.platform() === 'linux', - 'Skipping Webkit for Linux which currently is flaky with this test.', - ) + test('Render default decorators with keyboard shortcuts', async ({mount, page}) => { + const { + getModifierKey, + getFocusedPortableTextEditor, + getFocusedPortableTextInput, + insertPortableText, + toggleHotkey, + } = testHelpers({ + page, }) - test('Render default styles with keyboard shortcuts', async ({mount, page}) => { - const {getModifierKey, getFocusedPortableTextEditor, insertPortableText, toggleHotkey} = - testHelpers({ - page, - }) - await mount() - const $pte = await getFocusedPortableTextEditor('field-body') - const modifierKey = getModifierKey() + await mount() + const $portableTextInput = await getFocusedPortableTextInput('field-defaultDecorators') + const $pte = await getFocusedPortableTextEditor('field-defaultDecorators') + const modifierKey = getModifierKey() - // eslint-disable-next-line max-nested-callbacks - for (const decorator of DEFAULT_DECORATORS) { - if (decorator.hotkey) { - await toggleHotkey(decorator.hotkey, modifierKey) - await insertPortableText(`${decorator.name} text 123`, $pte) - await toggleHotkey(decorator.hotkey, modifierKey) - await expect( - $pte.locator(`[data-mark="${decorator.name}"]`, { - hasText: `${decorator.name} text 123`, - }), - ).toBeVisible() - } + for (const decorator of DEFAULT_DECORATORS) { + if (decorator.hotkey) { + // Turn on the decorator + await toggleHotkey(decorator.hotkey, modifierKey) + // Assertion: button was toggled + await expect( + $portableTextInput.locator( + `button[data-testid="action-button-${decorator.name}"][data-selected]:not([disabled])`, + ), + ).toBeVisible() + // Insert some text + await insertPortableText(`${decorator.name} text 123`, $pte) + // Turn off the decorator + await toggleHotkey(decorator.hotkey, modifierKey) + // Assertion: button was toggled + await expect( + $portableTextInput.locator( + `button[data-testid="action-button-${decorator.name}"]:not([data-selected]):not([disabled])`, + ), + ).toBeVisible() + // Assertion: text has the correct decorator value + await expect( + $pte.locator(`[data-mark="${decorator.name}"]`, { + hasText: `${decorator.name} text 123`, + }), + ).toBeVisible() } - }) + } }) - test.describe('Toolbar', () => { + test.describe('Toolbar buttons', () => { test('Should display all default decorator buttons', async ({mount, page}) => { const {getFocusedPortableTextInput} = testHelpers({page}) await mount() - const $portableTextInput = await getFocusedPortableTextInput('field-body') + const $portableTextInput = await getFocusedPortableTextInput('field-defaultDecorators') - // Assertion: All buttons in the menu bar should be visible + // Assertion: All buttons in the menu bar should be visible and have icon for (const decorator of DEFAULT_DECORATORS) { - await expect( - $portableTextInput.getByRole('button', {name: decorator.title}), - ).toBeVisible() + const $button = $portableTextInput.getByRole('button', {name: decorator.title}) + await expect($button).toBeVisible() + await expect($button.locator(`svg[data-sanity-icon='${decorator.icon}']`)).toBeVisible() } }) + + test('Should display custom decorator button and icon', async ({mount, page}) => { + const {getFocusedPortableTextInput} = testHelpers({page}) + await mount() + const $portableTextInput = await getFocusedPortableTextInput('field-customDecorator') + // Assertion: Button for highlight should exist + const $highlightButton = $portableTextInput.getByRole('button', {name: 'Highlight'}) + await expect($highlightButton).toBeVisible() + // Assertion: Icon for highlight should exist + await expect($highlightButton.locator(`svg[data-sanity-icon='bulb-outline']`)).toBeVisible() + }) }) }) }) diff --git a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/DecoratorsStory.tsx b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/DecoratorsStory.tsx index 14c63b278852..bacc21a4caaa 100644 --- a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/DecoratorsStory.tsx +++ b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/DecoratorsStory.tsx @@ -1,5 +1,6 @@ import {defineArrayMember, defineField, defineType} from '@sanity/types' import React from 'react' +import {BulbOutlineIcon} from '@sanity/icons' import {TestWrapper} from '../../utils/TestWrapper' const SCHEMA_TYPES = [ @@ -10,13 +11,25 @@ const SCHEMA_TYPES = [ fields: [ defineField({ type: 'array', - name: 'body', + name: 'defaultDecorators', of: [ defineArrayMember({ type: 'block', }), ], }), + defineField({ + type: 'array', + name: 'customDecorator', + of: [ + defineArrayMember({ + type: 'block', + marks: { + decorators: [{title: 'Highlight', value: 'highlight', icon: BulbOutlineIcon}], + }, + }), + ], + }), ], }), ] diff --git a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Input.spec.tsx b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Input.spec.tsx index 2e76907e4e49..8bd7b1f2a9a7 100644 --- a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Input.spec.tsx +++ b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Input.spec.tsx @@ -33,11 +33,12 @@ test.describe('Portable Text Input', () => { const $pte = await getFocusedPortableTextEditor('field-body') const $placeholder = $pte.getByTestId('pt-input-placeholder') // Assertion: placeholder is there + await expect($placeholder).toBeVisible() await expect($placeholder).toHaveText('Empty') // Write some text await insertPortableText('Hello there', $pte) // Assertion: placeholder was removed - expect(await $placeholder.count()).toEqual(0) + await expect($placeholder).not.toBeVisible() }) }) }) diff --git a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/ObjectBlock.spec.tsx b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/ObjectBlock.spec.tsx index b9f9dbdf1815..37f28d5d850c 100644 --- a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/ObjectBlock.spec.tsx +++ b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/ObjectBlock.spec.tsx @@ -11,13 +11,7 @@ test.describe('Portable Text Input', () => { const $portableTextInput = await getFocusedPortableTextInput('field-body') - await page - .getByRole('button') - .filter({hasText: /^Object$/}) - // @todo It seems like Firefox has different focus behaviour when using keypress here - // causing the focus assertion to fail. The insert button will stay focused even after the dialog opens. - // .press('Enter', {delay: DEFAULT_TYPE_DELAY}) - .click() + await page.getByRole('button', {name: 'Insert Object (block)'}).click() // Assertion: Object preview should be visible await expect($portableTextInput.locator('.pt-block.pt-object-block')).toBeVisible() @@ -28,13 +22,7 @@ test.describe('Portable Text Input', () => { await mount() const $pte = await getFocusedPortableTextEditor('field-body') - await page - .getByRole('button') - .filter({hasText: /^Inline Object$/}) - // @todo It seems like Firefox has different focus behaviour when using keypress here - // causing the focus assertion to fail. The insert button will stay focused even after the dialog opens. - // .press('Enter', {delay: DEFAULT_TYPE_DELAY}) - .click() + await page.getByRole('button', {name: 'Insert Inline Object (inline)'}).click() // Assertion: Object preview should be visible await expect($pte.getByTestId('inline-preview')).toBeVisible() @@ -49,10 +37,7 @@ test.describe('Portable Text Input', () => { const $pte = await getFocusedPortableTextEditor('field-body') - await page - .getByRole('button') - .filter({hasText: /^Object$/}) - .click() + await page.getByRole('button', {name: 'Insert Object (block)'}).click() // Assertion: Object preview should be visible await expect($pte.locator('.pt-block.pt-object-block')).toBeVisible() @@ -81,10 +66,7 @@ test.describe('Portable Text Input', () => { const $portableTextField = await getFocusedPortableTextInput('field-body') - await page - .getByRole('button') - .filter({hasText: /^Object$/}) - .click() + await page.getByRole('button', {name: 'Insert Object (block)'}).click() // Assertion: Object preview should be visible await expect($portableTextField.locator('.pt-block.pt-object-block')).toBeVisible() @@ -138,10 +120,7 @@ test.describe('Portable Text Input', () => { const $pte = await getFocusedPortableTextEditor('field-body') - await page - .getByRole('button') - .filter({hasText: /^Object$/}) - .click() + await page.getByRole('button', {name: 'Insert Object (block)'}).click() // Assertion: Object preview should be visible await expect($pte.locator('.pt-block.pt-object-block')).toBeVisible() diff --git a/packages/sanity/playwright-ct/tests/utils/testHelpers.tsx b/packages/sanity/playwright-ct/tests/utils/testHelpers.tsx index c68d4ec3db7d..0c909a4aeb3a 100644 --- a/packages/sanity/playwright-ct/tests/utils/testHelpers.tsx +++ b/packages/sanity/playwright-ct/tests/utils/testHelpers.tsx @@ -12,6 +12,16 @@ export const TYPE_DELAY_HIGH = 150 export type MountResult = Awaited> export function testHelpers({page}: {page: PlaywrightTestArgs['page']}) { + const activatePTInputOverlay = async ($pteField: Locator) => { + const $overlay = $pteField.getByTestId('activate-overlay') + if (await $overlay.isVisible()) { + await $overlay.focus() + await page.keyboard.press('Space') + } + await $pteField + .locator(`[data-testid='pt-editor__toolbar-card']`) + .waitFor({state: 'visible', timeout: 1000}) + } return { /** * Returns the DOM element of a focused Portable Text Input ready to typed into @@ -20,12 +30,14 @@ export function testHelpers({page}: {page: PlaywrightTestArgs['page']}) { * @returns The Portable Text Input element */ getFocusedPortableTextInput: async (testId: string) => { + // Wait for field to get ready (without this tests fails randomly on Webkit) + await page.locator(`[data-testid='${testId}']`).waitFor() const $pteField: Locator = page.getByTestId(testId) - // Activate the input - await $pteField.getByTestId('activate-overlay').focus() - await page.keyboard.press('Space') + // Activate the input if needed + await activatePTInputOverlay($pteField) // Ensure focus on the contentEditable element of the Portable Text Editor const $pteTextbox = $pteField.getByRole('textbox') + await $pteTextbox.isEditable() await $pteTextbox.focus() return $pteField }, @@ -38,12 +50,14 @@ export function testHelpers({page}: {page: PlaywrightTestArgs['page']}) { * @returns The PT-editor's contentEditable element */ getFocusedPortableTextEditor: async (testId: string) => { + // Wait for field to get ready (without this tests fails randomly on Webkit) + await page.locator(`[data-testid='${testId}']`).waitFor() const $pteField: Locator = page.getByTestId(testId) - // Activate the input - await $pteField.getByTestId('activate-overlay').focus() - await page.keyboard.press('Space') + // Activate the input if needed + await activatePTInputOverlay($pteField) // Ensure focus on the contentEditable element of the Portable Text Editor const $pteTextbox = $pteField.getByRole('textbox') + await $pteTextbox.isEditable() await $pteTextbox.focus() return $pteTextbox }, @@ -82,6 +96,7 @@ export function testHelpers({page}: {page: PlaywrightTestArgs['page']}) { }), ) }, text) + await locator.getByText(text).waitFor() }, /** * Will create a keyboard event of a given hotkey combination that can be activated with a modifier key @@ -89,9 +104,7 @@ export function testHelpers({page}: {page: PlaywrightTestArgs['page']}) { * @param modifierKey - the modifier key (if any) that can activate the hotkey */ toggleHotkey: async (hotkey: string, modifierKey?: string) => { - if (modifierKey) await page.keyboard.down(modifierKey) - await page.keyboard.press(hotkey) - if (modifierKey) await page.keyboard.up(modifierKey) + await page.keyboard.press(modifierKey ? `${modifierKey}+${hotkey}` : hotkey) }, } } diff --git a/packages/sanity/src/core/form/inputs/PortableText/toolbar/ActionMenu.tsx b/packages/sanity/src/core/form/inputs/PortableText/toolbar/ActionMenu.tsx index 88831a4a16e4..767f1ed170b6 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/toolbar/ActionMenu.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/toolbar/ActionMenu.tsx @@ -62,6 +62,7 @@ export const ActionMenu = memo(function ActionMenu(props: ActionMenuProps) { const active = activeKeys.includes(action.key) return (