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 (