Merge pull request #33450 from overleaf/worktree-mg-writefull-spelling-tab

Add writefull language suggestions section to Spelling and language tab

GitOrigin-RevId: 6195683ca175a4c3da25a7ab334a605c67db04b8
This commit is contained in:
Malik Glossop
2026-06-04 12:44:58 +02:00
committed by Copybot
parent fc31a88767
commit d25b032e16
11 changed files with 226 additions and 18 deletions
+2
View File
@@ -44,6 +44,7 @@ describe('editor', function () {
cy.log(`change project language to '${lng}'`) cy.log(`change project language to '${lng}'`)
cy.findByRole('button', { name: 'Settings' }).click() cy.findByRole('button', { name: 'Settings' }).click()
cy.findByRole('dialog').within(() => { cy.findByRole('dialog').within(() => {
cy.findByRole('tab', { name: 'Spelling and language' }).click()
cy.findByLabelText('Spellcheck language').select(lng) cy.findByLabelText('Spellcheck language').select(lng)
}) })
cy.get('body').type('{esc}') cy.get('body').type('{esc}')
@@ -76,6 +77,7 @@ describe('editor', function () {
cy.log('remove word from dictionary') cy.log('remove word from dictionary')
cy.findByRole('button', { name: 'Settings' }).click() cy.findByRole('button', { name: 'Settings' }).click()
cy.findByRole('dialog').within(() => { cy.findByRole('dialog').within(() => {
cy.findByRole('tab', { name: 'Spelling and language' }).click()
cy.findByLabelText('Dictionary').click() cy.findByLabelText('Dictionary').click()
}) })
cy.findByTestId('dictionary-modal').within(() => { cy.findByTestId('dictionary-modal').within(() => {
+1
View File
@@ -1090,6 +1090,7 @@ module.exports = {
integrationPanelComponents: [], integrationPanelComponents: [],
referenceSearchSetting: [], referenceSearchSetting: [],
settingsModalEditorTabSections: [], settingsModalEditorTabSections: [],
settingsModalSpellcheckSections: [],
errorLogsComponents: [], errorLogsComponents: [],
referenceIndices: [], referenceIndices: [],
railEntries: [], railEntries: [],
@@ -133,6 +133,7 @@
"ai_feedback_the_suggestion_didnt_fix_the_error": "", "ai_feedback_the_suggestion_didnt_fix_the_error": "",
"ai_feedback_the_suggestion_wasnt_the_best_fix_available": "", "ai_feedback_the_suggestion_wasnt_the_best_fix_available": "",
"ai_feedback_there_was_no_code_fix_suggested": "", "ai_feedback_there_was_no_code_fix_suggested": "",
"ai_language_suggestions": "",
"ai_shortcut_on_empty_lines": "", "ai_shortcut_on_empty_lines": "",
"ai_shortcut_on_text_selection": "", "ai_shortcut_on_text_selection": "",
"alignment": "", "alignment": "",
@@ -216,6 +217,7 @@
"binary_history_error": "", "binary_history_error": "",
"blank_project": "", "blank_project": "",
"blocked_filename": "", "blocked_filename": "",
"blocked_language_suggestions": "",
"blog": "", "blog": "",
"bold": "", "bold": "",
"booktabs": "", "booktabs": "",
@@ -413,6 +415,8 @@
"currently_subscribed_to_plan": "", "currently_subscribed_to_plan": "",
"custom": "", "custom": "",
"custom_borders": "", "custom_borders": "",
"custom_prompt": "",
"custom_prompt_example": "",
"customer_stories": "", "customer_stories": "",
"customize_your_group_subscription": "", "customize_your_group_subscription": "",
"customizing_figures": "", "customizing_figures": "",
@@ -613,6 +617,9 @@
"enabling": "", "enabling": "",
"end_of_document": "", "end_of_document": "",
"end_time_utc": "", "end_time_utc": "",
"english__preference_for_ai_suggestion": "",
"english_american": "",
"english_british": "",
"ensure_recover_account": "", "ensure_recover_account": "",
"enter_any_size_including_units_or_valid_latex_command": "", "enter_any_size_including_units_or_valid_latex_command": "",
"enter_emails_separated_by_commas": "", "enter_emails_separated_by_commas": "",
@@ -1042,6 +1049,13 @@
"labs_ongoing_experiments": "", "labs_ongoing_experiments": "",
"labs_settings": "", "labs_settings": "",
"language": "", "language": "",
"language_model_gpt_any_language": "",
"language_model_gpt_any_language_description": "",
"language_model_gpt_custom_prompt": "",
"language_model_gpt_custom_prompt_description": "",
"language_model_writefull_english": "",
"language_model_writefull_english_description": "",
"language_suggestions": "",
"last_active": "", "last_active": "",
"last_active_description": "", "last_active_description": "",
"last_edit": "", "last_edit": "",
@@ -1205,6 +1219,7 @@
"missing_field_for_entry": "", "missing_field_for_entry": "",
"missing_fields_for_entry": "", "missing_fields_for_entry": "",
"missing_payment_details": "", "missing_payment_details": "",
"model": "",
"money_back_guarantee": "", "money_back_guarantee": "",
"month": "", "month": "",
"month_plural": "", "month_plural": "",
@@ -1857,6 +1872,7 @@
"speech_input_not_available": "", "speech_input_not_available": "",
"spellcheck": "", "spellcheck": "",
"spellcheck_language": "", "spellcheck_language": "",
"spelling_and_language": "",
"split_view": "", "split_view": "",
"sso": "", "sso": "",
"sso_active": "", "sso_active": "",
@@ -64,6 +64,7 @@ export default /** @type {const} */ ([
'shuffle', 'shuffle',
'smart_toy', 'smart_toy',
'space_dashboard', 'space_dashboard',
'spellcheck',
'star', 'star',
'strikethrough_s', 'strikethrough_s',
'table_chart', 'table_chart',
@@ -48,6 +48,11 @@ const editorTabExtraSectionHooks: SettingsSectionHook[] = importOverleafModules(
.map((m: any) => m?.import?.default) .map((m: any) => m?.import?.default)
.filter((h: unknown): h is SettingsSectionHook => typeof h === 'function') .filter((h: unknown): h is SettingsSectionHook => typeof h === 'function')
const spellcheckExtraSectionHooks: SettingsSectionHook[] =
importOverleafModules('settingsModalSpellcheckSections')
.map((m: any) => m?.import?.default)
.filter((h: unknown): h is SettingsSectionHook => typeof h === 'function')
const useSlotSections = (hooks: SettingsSectionHook[]): SettingsSection[] => const useSlotSections = (hooks: SettingsSectionHook[]): SettingsSection[] =>
hooks.map(hook => hook()).filter((s): s is SettingsSection => s != null) hooks.map(hook => hook()).filter((s): s is SettingsSection => s != null)
@@ -78,6 +83,7 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
const hasEditorTabs = useFeatureFlag('editor-tabs') const hasEditorTabs = useFeatureFlag('editor-tabs')
const editorTabExtraSections = useSlotSections(editorTabExtraSectionHooks) const editorTabExtraSections = useSlotSections(editorTabExtraSectionHooks)
const spellcheckExtraSections = useSlotSections(spellcheckExtraSectionHooks)
const allSettingsTabs: SettingsEntry[] = useMemo( const allSettingsTabs: SettingsEntry[] = useMemo(
() => [ () => [
@@ -125,20 +131,6 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
}, },
], ],
}, },
{
key: 'spellcheck',
title: t('spellcheck'),
settings: [
{
key: 'spellCheckLanguage',
component: <SpellCheckSetting />,
},
{
key: 'dictionary-settings',
component: <DictionarySetting />,
},
],
},
{ {
key: 'tools', key: 'tools',
title: t('tools'), title: t('tools'),
@@ -156,6 +148,28 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
...editorTabExtraSections, ...editorTabExtraSections,
], ],
}, },
{
key: 'spelling_and_language',
title: t('spelling_and_language'),
icon: 'spellcheck',
sections: [
{
key: 'spellcheck',
title: t('spellcheck'),
settings: [
{
key: 'spellCheckLanguage',
component: <SpellCheckSetting />,
},
{
key: 'dictionary-settings',
component: <DictionarySetting />,
},
],
},
...spellcheckExtraSections,
],
},
{ {
key: 'compiler', key: 'compiler',
title: t('compiler'), title: t('compiler'),
@@ -269,6 +283,7 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
hasEmailNotifications, hasEmailNotifications,
isOverleaf, isOverleaf,
editorTabExtraSections, editorTabExtraSections,
spellcheckExtraSections,
] ]
) )
@@ -5,6 +5,7 @@ import {
useCallback, useCallback,
ReactNode, ReactNode,
useState, useState,
useId,
} from 'react' } from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import { useSelect } from 'downshift' import { useSelect } from 'downshift'
@@ -14,12 +15,47 @@ import FormControl from '@/shared/components/form/form-control'
import MaterialIcon from '@/shared/components/material-icon' import MaterialIcon from '@/shared/components/material-icon'
import { CaretUp, CaretDown, Check } from '@phosphor-icons/react' import { CaretUp, CaretDown, Check } from '@phosphor-icons/react'
import { DropdownItem } from '@/shared/components/dropdown/dropdown-menu' import { DropdownItem } from '@/shared/components/dropdown/dropdown-menu'
import OLOverlay from '@/shared/components/ol/ol-overlay'
import OLSpinner from './ol/ol-spinner' import OLSpinner from './ol/ol-spinner'
import DSFormLabel from '@/shared/components/ds/ds-form-label' import DSFormLabel from '@/shared/components/ds/ds-form-label'
import DSFormGroup from '@/shared/components/ds/ds-form-group' import DSFormGroup from '@/shared/components/ds/ds-form-group'
import DSFormControl from '@/shared/components/ds/ds-form-control' import DSFormControl from '@/shared/components/ds/ds-form-control'
import { DropdownItemProps } from '@/shared/components/types/dropdown-menu-props' import { DropdownItemProps } from '@/shared/components/types/dropdown-menu-props'
function SelectMenuPopover({
show,
target,
onHide,
children,
}: {
show: boolean
target: HTMLElement | null
onHide: () => void
children: ReactNode
}) {
const id = useId()
return (
<OLOverlay
show={show}
target={target}
placement="bottom-start"
rootClose
onHide={onHide}
>
{({ ref, style }) => (
<div
id={`select-popover-${id}`}
ref={ref}
style={{ ...style, width: target?.offsetWidth }}
className="select-portal-popover"
>
{children}
</div>
)}
</OLOverlay>
)
}
export type SelectProps<T> = { export type SelectProps<T> = {
// The items rendered as dropdown options. // The items rendered as dropdown options.
items: T[] items: T[]
@@ -63,6 +99,11 @@ export type SelectProps<T> = {
// CIAM-specific layout // CIAM-specific layout
isCiam?: boolean isCiam?: boolean
size?: React.ComponentProps<typeof FormControl>['size'] size?: React.ComponentProps<typeof FormControl>['size']
// Renders the menu in a portal so it escapes overflow-clipping ancestors.
portal?: boolean
// Optional id for the toggle button element. When provided, enables association
// with an external <label htmlFor="...">
id?: string
} }
export const Select = <T,>({ export const Select = <T,>({
@@ -85,7 +126,10 @@ export const Select = <T,>({
dataTestId, dataTestId,
isCiam, isCiam,
size, size,
portal = false,
id,
}: SelectProps<T>) => { }: SelectProps<T>) => {
const toggleButtonId = id ? { id } : {}
const [selectedItem, setSelectedItem] = useState<T | undefined | null>( const [selectedItem, setSelectedItem] = useState<T | undefined | null>(
defaultItem defaultItem
) )
@@ -164,13 +208,14 @@ export const Select = <T,>({
return isCiam ? <Check /> : 'check' return isCiam ? <Check /> : 'check'
} }
const dropdown = ( const menu = (
<ul <ul
{...getMenuProps({ disabled })} {...getMenuProps({ disabled }, { suppressRefError: portal })}
className={classNames('dropdown-menu', { className={classNames('dropdown-menu', {
'w-100': !isCiam, 'w-100': !isCiam,
'ciam-dropdown-menu': isCiam, 'ciam-dropdown-menu': isCiam,
show: isOpen, show: isOpen,
'select-portal-menu': portal,
})} })}
> >
{isOpen && {isOpen &&
@@ -208,6 +253,18 @@ export const Select = <T,>({
</ul> </ul>
) )
const dropdown = portal ? (
<SelectMenuPopover
show={isOpen}
target={rootRef.current}
onHide={closeMenu}
>
{menu}
</SelectMenuPopover>
) : (
menu
)
if (isCiam) { if (isCiam) {
return ( return (
<div className="select-wrapper" ref={rootRef}> <div className="select-wrapper" ref={rootRef}>
@@ -224,6 +281,7 @@ export const Select = <T,>({
disabled, disabled,
onKeyDown, onKeyDown,
className: 'select-trigger', className: 'select-trigger',
...toggleButtonId,
})} })}
value={value} value={value}
readOnly readOnly
@@ -252,6 +310,7 @@ export const Select = <T,>({
disabled, disabled,
onKeyDown, onKeyDown,
className: 'select-trigger', className: 'select-trigger',
...toggleButtonId,
})} })}
value={value} value={value}
readOnly readOnly
@@ -17,3 +17,12 @@
.select-highlighted { .select-highlighted {
background-color: var(--select-background-highlighted); background-color: var(--select-background-highlighted);
} }
.select-portal-popover {
z-index: calc(#{$zindex-modal} + 1);
}
.select-portal-menu {
position: static;
width: 100%;
}
+17
View File
@@ -167,6 +167,7 @@
"ai_feedback_the_suggestion_didnt_fix_the_error": "The suggestion didnt fix the error", "ai_feedback_the_suggestion_didnt_fix_the_error": "The suggestion didnt fix the error",
"ai_feedback_the_suggestion_wasnt_the_best_fix_available": "The suggestion wasnt the best fix available", "ai_feedback_the_suggestion_wasnt_the_best_fix_available": "The suggestion wasnt the best fix available",
"ai_feedback_there_was_no_code_fix_suggested": "There was no code fix suggested", "ai_feedback_there_was_no_code_fix_suggested": "There was no code fix suggested",
"ai_language_suggestions": "AI language suggestions",
"ai_shortcut_on_empty_lines": "AI shortcut on empty lines", "ai_shortcut_on_empty_lines": "AI shortcut on empty lines",
"ai_shortcut_on_text_selection": "AI shortcut on text selection", "ai_shortcut_on_text_selection": "AI shortcut on text selection",
"ai_usage": "AI usage", "ai_usage": "AI usage",
@@ -291,6 +292,7 @@
"binary_history_error": "Preview not available for this file type", "binary_history_error": "Preview not available for this file type",
"blank_project": "Blank project", "blank_project": "Blank project",
"blocked_filename": "This file name is blocked.", "blocked_filename": "This file name is blocked.",
"blocked_language_suggestions": "Blocked language suggestions",
"blog": "Blog", "blog": "Blog",
"bold": "Bold", "bold": "Bold",
"booktabs": "Booktabs", "booktabs": "Booktabs",
@@ -555,6 +557,8 @@
"currently_subscribed_to_plan": "You are currently subscribed to the <0>__planName__</0> plan.", "currently_subscribed_to_plan": "You are currently subscribed to the <0>__planName__</0> plan.",
"custom": "Custom", "custom": "Custom",
"custom_borders": "Custom borders", "custom_borders": "Custom borders",
"custom_prompt": "Custom prompt",
"custom_prompt_example": "For example, \"Correct the text to reduce the number of words while maintaining clarity.\"",
"customer_resource_portal": "Customer resource portal", "customer_resource_portal": "Customer resource portal",
"customer_stories": "Customer stories", "customer_stories": "Customer stories",
"customize": "Customize", "customize": "Customize",
@@ -798,6 +802,9 @@
"enabling": "Enabling", "enabling": "Enabling",
"end_of_document": "End of document", "end_of_document": "End of document",
"end_time_utc": "End time (UTC)", "end_time_utc": "End time (UTC)",
"english__preference_for_ai_suggestion": "English preference for AI suggestions",
"english_american": "English (American)",
"english_british": "English (British)",
"ensure_recover_account": "This will ensure that it can be used to recover your __appName__ account in case you lose access to your primary email address.", "ensure_recover_account": "This will ensure that it can be used to recover your __appName__ account in case you lose access to your primary email address.",
"enter_any_size_including_units_or_valid_latex_command": "Enter any size (including units) or valid LaTeX command", "enter_any_size_including_units_or_valid_latex_command": "Enter any size (including units) or valid LaTeX command",
"enter_emails_separated_by_commas": "Enter emails separated by commas", "enter_emails_separated_by_commas": "Enter emails separated by commas",
@@ -1078,6 +1085,7 @@
"go_to_writefull": "Go to Writefull", "go_to_writefull": "Go to Writefull",
"good_news_you_already_purchased_this_add_on": "Good news! You already have this add-on, so no need to pay again.", "good_news_you_already_purchased_this_add_on": "Good news! You already have this add-on, so no need to pay again.",
"good_news_you_are_already_receiving_this_add_on_via_writefull": "Good news! You already have this add-on via your Writefull subscription. No need to pay again.", "good_news_you_are_already_receiving_this_add_on_via_writefull": "Good news! You already have this add-on via your Writefull subscription. No need to pay again.",
"gpt": "GPT",
"great_for_getting_started": "Great for getting started", "great_for_getting_started": "Great for getting started",
"great_for_small_teams_and_departments": "Great for small teams and departments", "great_for_small_teams_and_departments": "Great for small teams and departments",
"group": "Group", "group": "Group",
@@ -1357,6 +1365,13 @@
"labs_program_benefits": "By signing up for Overleaf Labs you can get your hands on in-development features and try them out as much as you like. All we ask in return is your honest feedback to help us develop and improve. Its important to note that features available in this program are still being tested and actively developed. This means they could change, be removed, or become part of a premium plan.", "labs_program_benefits": "By signing up for Overleaf Labs you can get your hands on in-development features and try them out as much as you like. All we ask in return is your honest feedback to help us develop and improve. Its important to note that features available in this program are still being tested and actively developed. This means they could change, be removed, or become part of a premium plan.",
"labs_settings": "Labs settings", "labs_settings": "Labs settings",
"language": "Language", "language": "Language",
"language_model": "Language model",
"language_model_gpt_any_language": "GPT model, any language",
"language_model_gpt_any_language_description": "Using this model means your text is sent to OpenAIs servers and may be kept there for up to 30 days. It is not used to train OpenAIs models.",
"language_model_gpt_custom_prompt": "GPT model with custom prompt",
"language_model_gpt_custom_prompt_description": "Customize your edits with a prompt, no matter your language, text type, or editing needs.",
"language_model_writefull_english": "Writefull",
"language_model_writefull_english_description": "Our custom model for research writing. It doesnt store your data or train on your texts.",
"language_suggestions": "Language suggestions", "language_suggestions": "Language suggestions",
"larger_discounts_available": "Larger discounts available", "larger_discounts_available": "Larger discounts available",
"larger_discounts_for_groups_of_20_plus": "Larger discounts for groups of 20+", "larger_discounts_for_groups_of_20_plus": "Larger discounts for groups of 20+",
@@ -1580,6 +1595,7 @@
"missing_fields_for_entry": "Missing fields for", "missing_fields_for_entry": "Missing fields for",
"missing_payment_details": "Missing payment details", "missing_payment_details": "Missing payment details",
"missing_required_sso_fields": "Missing required SSO fields in order to update configuration. Please check the SSO configuration and try again. Missing: __missingFields__.", "missing_required_sso_fields": "Missing required SSO fields in order to update configuration. Please check the SSO configuration and try again. Missing: __missingFields__.",
"model": "Model",
"money_back_guarantee": "14-day money back guarantee, hassle free", "money_back_guarantee": "14-day money back guarantee, hassle free",
"month": "month", "month": "month",
"month_plural": "months", "month_plural": "months",
@@ -2418,6 +2434,7 @@
"speech_input_not_available": "Speech input is not yet available in this browser", "speech_input_not_available": "Speech input is not yet available in this browser",
"spellcheck": "Spellcheck", "spellcheck": "Spellcheck",
"spellcheck_language": "Spellcheck language", "spellcheck_language": "Spellcheck language",
"spelling_and_language": "Spelling and language",
"split_view": "Split view", "split_view": "Split view",
"sso": "SSO", "sso": "SSO",
"sso_account_already_linked": "Account already linked to another __appName__ user", "sso_account_already_linked": "Account already linked to another __appName__ user",
@@ -40,6 +40,7 @@ function render(props: RenderProps) {
items={testData} items={testData}
itemToString={x => String(x?.value)} itemToString={x => String(x?.value)}
label={props.label} label={props.label}
id={props.id}
name="select_control" name="select_control"
defaultText={props.defaultText} defaultText={props.defaultText}
defaultItem={props.defaultItem} defaultItem={props.defaultItem}
@@ -52,6 +53,7 @@ function render(props: RenderProps) {
optionalLabel={props.optionalLabel} optionalLabel={props.optionalLabel}
loading={props.loading} loading={props.loading}
selectedIcon={props.selectedIcon} selectedIcon={props.selectedIcon}
portal={props.portal}
/> />
<button type="submit">submit</button> <button type="submit">submit</button>
</form> </form>
@@ -61,6 +63,38 @@ function render(props: RenderProps) {
describe('<Select />', function () { describe('<Select />', function () {
describe('initial rendering', function () { describe('initial rendering', function () {
it('associates an external label when id is provided', function () {
cy.mount(
<div style={{ width: '300px' }}>
<label htmlFor="external-select">External label</label>
<Select
id="external-select"
items={testData}
itemToString={x => String(x?.value)}
itemToKey={x => String(x.key)}
defaultText="Choose an item"
/>
</div>
)
cy.findByRole('combobox', { name: 'External label' }).should('exist')
})
it('does not associate an external label when id is not provided', function () {
cy.mount(
<div style={{ width: '300px' }}>
<label htmlFor="external-select">External label</label>
<Select
items={testData}
itemToString={x => String(x?.value)}
itemToKey={x => String(x.key)}
defaultText="Choose an item"
/>
</div>
)
cy.findByRole('combobox').should('have.value', 'Choose an item')
cy.findByRole('combobox', { name: 'External label' }).should('not.exist')
})
it('renders default text', function () { it('renders default text', function () {
render({ defaultText: 'Choose an item' }) render({ defaultText: 'Choose an item' })
cy.findByTestId('ol-spinner').should('not.exist') cy.findByTestId('ol-spinner').should('not.exist')
@@ -135,6 +169,26 @@ describe('<Select />', function () {
cy.findByRole('option', { name: 'Demo item 2 Subtitle 2' }) cy.findByRole('option', { name: 'Demo item 2 Subtitle 2' })
cy.findByRole('option', { name: 'Demo item 3 Subtitle 3' }) cy.findByRole('option', { name: 'Demo item 3 Subtitle 3' })
}) })
it('renders dropdown inside a popover when portal is enabled', function () {
render({ defaultText: 'Choose an item', portal: true })
cy.findByRole('combobox').click()
cy.get('.select-portal-popover').should('exist')
cy.get('.select-portal-popover .select-portal-menu')
.should('exist')
.and('be.visible')
cy.findByRole('option', { name: 'Demo item 1' }).should('exist')
})
it('does not render a popover when portal is disabled', function () {
render({ defaultText: 'Choose an item', portal: false })
cy.findByRole('combobox').click()
cy.get('.select-portal-popover').should('not.exist')
cy.get('.select-portal-menu').should('not.exist')
cy.findByRole('option', { name: 'Demo item 1' }).should('exist')
})
}) })
describe('item selection', function () { describe('item selection', function () {
@@ -24,11 +24,10 @@ const TAB_SETTINGS = {
'Keybindings', 'Keybindings',
'PDF Viewer', 'PDF Viewer',
'Reference search', 'Reference search',
'Spellcheck language',
'Dictionary',
'Breadcrumbs', 'Breadcrumbs',
'Equation preview', 'Equation preview',
], ],
'Spelling and language': ['Spellcheck language', 'Dictionary'],
Compiler: [ Compiler: [
'Main document', 'Main document',
'Compiler', 'Compiler',
@@ -154,6 +153,22 @@ describe('<SettingsModal />', function () {
selectTab('Editor') selectTab('Editor')
await waitFor(() => expect(screen.getByText('AI assistance')).to.exist) await waitFor(() => expect(screen.getByText('AI assistance')).to.exist)
}) })
it('shows the Language Suggestions section in the Spelling and language tab', async function () {
render(
<EditorProviders
rootFolder={[rootFolder as any]}
layoutContext={{ leftMenuShown: true }}
>
<SettingsModal />
</EditorProviders>
)
selectTab('Spelling and language')
await waitFor(
() => expect(screen.getByText('Language suggestions')).to.exist
)
})
}) })
describe('when a user does not have Writefull enabled', function () { describe('when a user does not have Writefull enabled', function () {
@@ -182,6 +197,25 @@ describe('<SettingsModal />', function () {
expect(screen.queryByText('AI assistance')).to.be.null expect(screen.queryByText('AI assistance')).to.be.null
}) })
it('does not show the Language Suggestions section when ol-writefullEnabled is false', async function () {
window.metaAttributesCache.set('ol-writefullEnabled', false)
window.metaAttributesCache.set('ol-showAiFeatures', true)
render(
<EditorProviders
rootFolder={[rootFolder as any]}
layoutContext={{ leftMenuShown: true }}
>
<SettingsModal />
</EditorProviders>
)
selectTab('Spelling and language')
await waitFor(
() => expect(screen.getByLabelText('Spellcheck language')).to.exist
)
expect(screen.queryByText('Language suggestions')).to.be.null
})
it('does not show the AI assistance section when ol-showAiFeatures is false', async function () { it('does not show the AI assistance section when ol-showAiFeatures is false', async function () {
window.metaAttributesCache.set('ol-writefullEnabled', true) window.metaAttributesCache.set('ol-writefullEnabled', true)
window.metaAttributesCache.set('ol-showAiFeatures', false) window.metaAttributesCache.set('ol-showAiFeatures', false)