diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/ShuffledResponsePool/index.vue b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/ShuffledResponsePool/index.vue index 2153493a89..3621797039 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/ShuffledResponsePool/index.vue +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/components/ShuffledResponsePool/index.vue @@ -43,8 +43,8 @@ import isEqual from 'lodash/isEqual'; import shuffle from 'lodash/shuffle'; import { themeTokens, themePalette } from 'kolibri-design-system/lib/styles/theme'; - import { richTextComparisonKey } from '../../utils/richText'; import TipTapEditor from 'shared/views/TipTapEditor/TipTapEditor/TipTapEditor'; + import { richTextComparisonKey } from 'shared/views/TipTapEditor/TipTapEditor/utils/comparisonKey'; import EditorImageProcessor from 'shared/views/TipTapEditor/TipTapEditor/services/imageService'; export default { diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/Editor.vue b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/Editor.vue index db539c994c..174e989a49 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/Editor.vue +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/Editor.vue @@ -248,8 +248,9 @@ import ClickableRegion from '../../components/ClickableRegion/index.vue'; import ShuffledResponsePool from '../../components/ShuffledResponsePool/index.vue'; import EditableChipList from '../../components/EditableChipList/index.vue'; - import { hasRichTextContent, richTextComparisonKey } from '../../utils/richText'; + import { hasRichTextContent } from '../../utils/richText'; import TipTapEditor from 'shared/views/TipTapEditor/TipTapEditor/TipTapEditor'; + import { richTextComparisonKey } from 'shared/views/TipTapEditor/TipTapEditor/utils/comparisonKey'; import EditorImageProcessor from 'shared/views/TipTapEditor/TipTapEditor/services/imageService'; /** Which card currently holds the open TipTap editor. */ diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/__tests__/parse.spec.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/__tests__/parse.spec.js index c32e3ab262..78fa53bba0 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/__tests__/parse.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/__tests__/parse.spec.js @@ -13,6 +13,9 @@ const contentsOf = pairs => pairs.map(pair => pair.map(choice => choice.content) const SCHEMA = { baseType: BaseType.PAIR, cardinality: Cardinality.MULTIPLE }; +const SOLVE_X2 = '
Solve for x
'; +const SOLVE_Y3 = 'Solve for x
'; + const parseXmlString = xml => parseXML(xml).documentElement; describe('parse()', () => { @@ -430,6 +433,21 @@ describe('parse → buildXML → parse round-trip', () => { ]); }); + it('preserves two choices with the same text but different formulas', () => { + const state = { + responseIdentifier: 'RESPONSE', + prompt: '', + pairs: [ + [ + { id: 'choice_aaa11111', content: SOLVE_X2 }, + { id: 'choice_bbb22222', content: SOLVE_Y3 }, + ], + ], + distractors: [], + }; + expect(contentsOf(roundTrip(state).pairs)).toEqual([[SOLVE_X2, SOLVE_Y3]]); + }); + it('preserves the default state as one pair of two blank choices', () => { const reparsed = roundTrip(parse('', [])); expect(contentsOf(reparsed.pairs)).toEqual([['', '']]); diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/__tests__/validation.spec.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/__tests__/validation.spec.js index 433b0639a5..f14264cdd9 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/__tests__/validation.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/__tests__/validation.spec.js @@ -1,5 +1,6 @@ import { validateAssociateInteraction } from '../validation'; import { ValidationError } from '../../../constants'; +import { richTextComparisonKey } from 'shared/views/TipTapEditor/TipTapEditor/utils/comparisonKey'; function makeState(overrides = {}) { return { @@ -136,7 +137,7 @@ describe('validateAssociateInteraction()', () => { const errors = validateAssociateInteraction(state); expect(errors).toContainEqual({ code: ValidationError.DUPLICATE_DISTRACTOR_CONTENT, - text: 'Lysander', + text: richTextComparisonKey('Lysander'), }); expect( errors.filter(e => e.code === ValidationError.DUPLICATE_DISTRACTOR_CONTENT), @@ -147,7 +148,7 @@ describe('validateAssociateInteraction()', () => { const state = makeState({ distractors: [{ id: 'choice_eee55555', content: 'Antonio' }] }); expect(validateAssociateInteraction(state)).toContainEqual({ code: ValidationError.DUPLICATE_DISTRACTOR_CONTENT, - text: 'Antonio', + text: richTextComparisonKey('Antonio'), }); }); diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/parse.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/parse.js index 908db6eb14..21d782ea8b 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/parse.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/parse.js @@ -4,8 +4,9 @@ import { QTIDeclaration } from '../../serialization/qti/QTIDeclaration'; import { buildXmlNode, getContentHTML, getPromptHTML, parseXML } from '../../serialization/xml'; import CorrectResponse from '../../serialization/qti/declarations/correctResponse'; import { generateRandomSlug } from '../../utils/generateRandomSlug'; -import { hasRichTextContent, richTextComparisonKey } from '../../utils/richText'; +import { hasRichTextContent } from '../../utils/richText'; import { RESPONSE_IDENTIFIER } from '../../constants'; +import { richTextComparisonKey } from 'shared/views/TipTapEditor/TipTapEditor/utils/comparisonKey'; const serializer = new XMLSerializer(); diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/validation.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/validation.js index 7a728d9cf4..d6628e9f94 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/validation.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/associate/validation.js @@ -1,6 +1,7 @@ import flatten from 'lodash/flatten'; import { ValidationError } from '../../constants'; -import { hasRichTextContent, richTextComparisonKey } from '../../utils/richText'; +import { hasRichTextContent } from '../../utils/richText'; +import { richTextComparisonKey } from 'shared/views/TipTapEditor/TipTapEditor/utils/comparisonKey'; /** * Validate AssociateState → ValidationError[]. diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/choice/__tests__/validation.spec.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/choice/__tests__/validation.spec.js index 9f6e8224e2..23d9d2a868 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/choice/__tests__/validation.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/choice/__tests__/validation.spec.js @@ -132,6 +132,20 @@ describe('validate()', () => { ValidationError.DUPLICATE_CHOICE_CONTENT, ); }); + + it.each([ + ['a formula', '', ''], + ['an image', '
', '
'],
+ ['a script', '2', '2'],
+ ])('does not flag choices with the same text but %s that differs', (_, first, second) => {
+ const state = makeState({
+ choices: [
+ makeAnswer({ id: 'a', content: `Solve ${first} for x
`, correct: true }), + makeAnswer({ id: 'b', content: `Solve ${second} for x
`, correct: false }), + ], + }); + expect(validate(state, QuestionType.SINGLE_SELECT)).toEqual([]); + }); }); describe('EMPTY_CHOICE_CONTENT', () => { diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/choice/validation.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/choice/validation.js index 91a09a21c0..e95154b237 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/choice/validation.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/choice/validation.js @@ -1,5 +1,6 @@ import { QuestionType, ValidationError } from '../../constants'; -import { hasRichTextContent, richTextComparisonKey } from '../../utils/richText'; +import { hasRichTextContent } from '../../utils/richText'; +import { richTextComparisonKey } from 'shared/views/TipTapEditor/TipTapEditor/utils/comparisonKey'; /** * Validate ChoiceState → ValidationError[]. diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/descriptors.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/descriptors.js index 395196005d..b7aa3360d9 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/descriptors.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/descriptors.js @@ -9,8 +9,9 @@ import { inlineChoiceInteractionDescriptor } from './inlineChoice/Descriptor'; /** * Every interaction's descriptor: matching, parsing, building and validating XML. * - * This module imports `Descriptor.js` files only so that headless validation can be done withou - * the bundle size cost of the editors. + * This module imports `Descriptor.js` files only so that headless validation does not load the + * interaction editors. It does load TipTap's extensions: rich text comparison keys parse with + * their schema. * * Registering a new interaction means adding its descriptor here and its editor in index.js */ diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/Editor.vue b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/Editor.vue index 30017aa6e8..d663179866 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/Editor.vue +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/Editor.vue @@ -309,8 +309,9 @@ import ClickableRegion from '../../components/ClickableRegion/index.vue'; import EditableChipList from '../../components/EditableChipList/index.vue'; import ShuffledResponsePool from '../../components/ShuffledResponsePool/index.vue'; - import { hasRichTextContent, richTextComparisonKey } from '../../utils/richText'; + import { hasRichTextContent } from '../../utils/richText'; import TipTapEditor from 'shared/views/TipTapEditor/TipTapEditor/TipTapEditor'; + import { richTextComparisonKey } from 'shared/views/TipTapEditor/TipTapEditor/utils/comparisonKey'; import EditorImageProcessor from 'shared/views/TipTapEditor/TipTapEditor/services/imageService'; /** Which card or chip list currently holds the open TipTap editor. */ diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/__tests__/parse.spec.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/__tests__/parse.spec.js index 9f0e6f1469..7bf8dd43a0 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/__tests__/parse.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/__tests__/parse.spec.js @@ -23,6 +23,9 @@ const declaration = (...values) =>Solve for x
'; +const SOLVE_Y3 = 'Solve for x
'; + const ROWS = matchSet(choice('row_dog', 'Dog') + choice('row_eagle', 'Eagle')); const RESPONSES = matchSet( choice('choice_mammal', 'Mammal') + choice('choice_bird', 'Bird') + choice('choice_fur', 'Fur'), @@ -484,6 +487,27 @@ describe('parse → buildXML → parse round-trip', () => { expect(roundTrip(state)).toEqual(state); }); + it.each([ + ['formulas', SOLVE_X2, SOLVE_Y3], + [ + 'SVGs', + '', + '', + ], + ['scripts', 'x2
', 'x2
'], + ])('preserves answers that differ only in their %s', (_, first, second) => { + const state = { + responseIdentifier: 'RESPONSE', + prompt: '', + rows: [ + { id: 'row_dog', content: 'Dog', matches: [{ id: 'choice_a', content: first }] }, + { id: 'row_eagle', content: 'Eagle', matches: [{ id: 'choice_b', content: second }] }, + ], + distractors: [], + }; + expect(matchContents(roundTrip(state).rows)).toEqual([[first], [second]]); + }); + it('preserves the default state', () => { const original = parse('', []); expect(roundTrip(original)).toEqual(original); diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/__tests__/validation.spec.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/__tests__/validation.spec.js index 79e6d1546d..e6f74c2a84 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/__tests__/validation.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/__tests__/validation.spec.js @@ -1,6 +1,6 @@ import { validateMatchInteraction as validate } from '../validation'; import { ValidationError } from '../../../constants'; -import { richTextComparisonKey } from '../../../utils/richText'; +import { richTextComparisonKey } from 'shared/views/TipTapEditor/TipTapEditor/utils/comparisonKey'; const row = (index, content, matches) => ({ id: `row_${index}`, content, matches }); const answer = (id, content) => ({ id, content }); diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/parse.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/parse.js index ae39c65d61..c498bc8e4a 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/parse.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/parse.js @@ -2,9 +2,10 @@ import { QTIDeclaration } from '../../serialization/qti/QTIDeclaration'; import { buildXmlNode, getContentHTML, getPromptHTML, parseXML } from '../../serialization/xml'; import CorrectResponse from '../../serialization/qti/declarations/correctResponse'; import { generateRandomSlug } from '../../utils/generateRandomSlug'; -import { hasRichTextContent, richTextComparisonKey } from '../../utils/richText'; +import { hasRichTextContent } from '../../utils/richText'; import { RESPONSE_IDENTIFIER } from '../../constants'; import { _extractCorrectPairIds, newChoice } from '../associate/parse'; +import { richTextComparisonKey } from 'shared/views/TipTapEditor/TipTapEditor/utils/comparisonKey'; const serializer = new XMLSerializer(); diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/validation.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/validation.js index a2e93bf78a..71195193e1 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/validation.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/match/validation.js @@ -1,5 +1,6 @@ import { ValidationError } from '../../constants'; -import { hasRichTextContent, richTextComparisonKey } from '../../utils/richText'; +import { hasRichTextContent } from '../../utils/richText'; +import { richTextComparisonKey } from 'shared/views/TipTapEditor/TipTapEditor/utils/comparisonKey'; /** * Validate MatchState → ValidationError[]. diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/ordering/__tests__/validation.spec.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/ordering/__tests__/validation.spec.js index f8856eaf52..ee9aa92444 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/ordering/__tests__/validation.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/ordering/__tests__/validation.spec.js @@ -157,5 +157,19 @@ describe('validateOrderingInteraction()', () => { ValidationError.DUPLICATE_CHOICE_CONTENT, ); }); + + it.each([ + ['a formula', '', ''], + ['an image', '
', '
'],
+ ['a script', '2', '2'],
+ ])('does not flag items with the same text but %s that differs', (_, first, second) => {
+ const state = makeState({
+ items: [
+ makeItem({ id: 'a', content: `Solve ${first} for x
` }), + makeItem({ id: 'b', content: `Solve ${second} for x
` }), + ], + }); + expect(validateOrderingInteraction(state)).toEqual([]); + }); }); }); diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/ordering/validation.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/ordering/validation.js index bc8e68d0c1..4f60436825 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/ordering/validation.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/ordering/validation.js @@ -1,5 +1,6 @@ import { ValidationError } from '../../constants'; -import { hasRichTextContent, richTextComparisonKey } from '../../utils/richText'; +import { hasRichTextContent } from '../../utils/richText'; +import { richTextComparisonKey } from 'shared/views/TipTapEditor/TipTapEditor/utils/comparisonKey'; /** * Validate OrderingState → ValidationError[]. diff --git a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/utils/__tests__/richText.spec.js b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/utils/__tests__/richText.spec.js index d398d94b0c..982a2d359f 100644 --- a/contentcuration/contentcuration/frontend/shared/views/QTIEditor/utils/__tests__/richText.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/QTIEditor/utils/__tests__/richText.spec.js @@ -1,4 +1,4 @@ -import { hasRichTextContent, richTextComparisonKey } from '../richText'; +import { hasRichTextContent } from '../richText'; describe('hasRichTextContent', () => { it.each([ @@ -32,29 +32,3 @@ describe('hasRichTextContent', () => { expect(hasRichTextContent(content)).toBe(expected); }); }); - -describe('richTextComparisonKey', () => { - it('reads through markup, so the same words marked up differently are one key', () => { - expect(richTextComparisonKey('Paris
')).toBe( - richTextComparisonKey('Paris
'), - ); - }); - - it('keeps two different formulas apart when there is no text to compare', () => { - expect(richTextComparisonKey('')).not.toBe( - richTextComparisonKey('
'), - ); - }); - - it('keeps two different images apart when there is no text to compare', () => { - expect(richTextComparisonKey('



\n
\n
Paris
')).toBe( + richTextComparisonKey('Paris
'), + ); + }); + + it.each([ + ['bold', 'x2
'], + ['italic', 'x2
'], + ['underline', 'x2
'], + ['strikethrough', 'x2
'], + ])('reads %s as the same text', (_, marked) => { + expect(richTextComparisonKey(marked)).toBe(richTextComparisonKey('x2
')); + }); + + it('keeps superscript, subscript and plain text apart', () => { + const keys = ['x2
', 'x2
', 'x2
'].map( + richTextComparisonKey, + ); + expect(new Set(keys).size).toBe(3); + }); + + it.each([ + ['formula', '', ''], + ['image', '
', '
'],
+ ['SVG', '', ''],
+ ['MathML', '', ''],
+ ])('keeps two different %s apart', (_, first, second) => {
+ expect(richTextComparisonKey(`${first}
`)).not.toBe( + richTextComparisonKey(`${second}
`), + ); + expect(richTextComparisonKey(`a ${first}
`)).not.toBe( + richTextComparisonKey(`a ${second}
`), + ); + }); + + it('reads the same SVG laid out differently as one', () => { + expect(richTextComparisonKey('')).toBe( + richTextComparisonKey('\n \n
'), + ); + }); + + it.each([ + ['formula', ''], + ['MathML', ''], + ['SVG', ''], + ])('keeps the same text and %s apart when it moves', (_, media) => { + expect(richTextComparisonKey(`${media} cm
`)).not.toBe( + richTextComparisonKey(`cm ${media}
`), + ); + expect(richTextComparisonKey(`a${media}b
`)).not.toBe( + richTextComparisonKey(`ab${media}
`), + ); + }); + + it('keeps a formula apart from its LaTeX typed as text', () => { + expect(richTextComparisonKey('a
')).not.toBe( + richTextComparisonKey('a x^2
'), + ); + }); + + it('keeps a formula apart from its key typed as text', () => { + const key = richTextComparisonKey('a
'); + expect(richTextComparisonKey(`${key}
`)).not.toBe(key); + }); + + describe.each([ + ['beside text', 'see '], + ['alone', ''], + ])('an image %s', (_, text) => { + const key = image => richTextComparisonKey(`${text}${image}
`); + + it.each([ + ['alt text', '
', '
'],
+ ['alignment', '
', '
'],
+ ['style', '
', '
'],
+ ])('is kept apart when its %s differs', (_, first, second) => {
+ expect(key(first)).not.toBe(key(second));
+ });
+
+ it('reads attribute order and the default alignment as the same image', () => {
+ expect(key('
')).toBe(
+ key('
'),
+ );
+ });
+
+ it('reads the same image at another size as the same image', () => {
+ expect(key('
')).toBe(key('
'));
+ });
+
+ it.each([
+ ['title', '
'],
+ ['class', '
'],
+ ['size style', '
'],
+ ['saved upload', '
'],
+ ])('with a %s it does not show reads as the same image', (_, other) => {
+ expect(key(other)).toBe(key('
'));
+ });
+ });
+
+ it.each([
+ ['formula', '', ''],
+ ['image', '
', '
'],
+ ['MathML', '', ''],
+ ])('keeps the same code apart when its %s differs', (_, first, second) => {
+ expect(richTextComparisonKey(`x ${first}`)).not.toBe(
+ richTextComparisonKey(`x ${second}`),
+ );
+ });
+
+ it('keeps the indentation of a code block', () => {
+ expect(richTextComparisonKey('if x:\n return 1')).not.toBe(
+ richTextComparisonKey('if x:\n return 1'),
+ );
+ });
+
+ it.each([
+ ['paragraphs', 'a
b
'], + ['a line break', 'a
b
ab
')); + }); + + it('reads text in a code block as the same text in a paragraph', () => { + expect(richTextComparisonKey('ab')).toBe(
+ richTextComparisonKey('ab
'), + ); + }); + + it.each([ + ['a trailing', 'Paris
'], + ['a leading', 'Paris
'], + ['an empty paragraph of', 'Paris
'], + ['a line-break-ended', '
Paris
Paris
')); + }); + + it.each([ + ['Paris

Paris

a
', + 'a
', + ], + ])('drops a non-breaking space at the end of a line in %j', (padded, plain) => { + expect(richTextComparisonKey(padded)).toBe(richTextComparisonKey(plain)); + }); + + it('keeps a non-breaking space inside text apart from a space', () => { + expect(richTextComparisonKey('a b
')).not.toBe(richTextComparisonKey('a b
')); + }); + + it('reads an entity in plain text as the character it stands for', () => { + expect(richTextComparisonKey('Salt & pepper')).toBe( + richTextComparisonKey('Salt & pepper
'), + ); + }); + + describe('after TipTap rewraps it', () => { + let editors = []; + + const rewrap = (html, edit = () => {}) => { + const { initializeEditor, editor } = useEditor(); + initializeEditor(html); + editors.push(editor.value); + edit(editor.value); + return editor.value.getHTML(); + }; + + afterEach(() => { + editors.forEach(editor => editor.destroy()); + editors = []; + }); + + it.each([ + 'see
here
\n
\n caption\n
see 
Solve\n \n for x
', + 'a
\n', + '
a
\n\n \n
', + 'see 
',
+ '

if x:\n return 1',
+ 'a
b
a
\n b
see 
a b
')).not.toBe( + richTextComparisonKey('ab
'), + ); + }); + + it.each([ + ['in text', 'a b
', 'a b
'], + [ + 'beside a formula', + 'a
', + 'a
', + ], + ])('collapses runs of whitespace %s', (_, run, single) => { + expect(richTextComparisonKey(run)).toBe(richTextComparisonKey(single)); + }); +});