import { ApproxStructure, Assertions, Logger, Step, UiFinder, Chain, Log } from '@ephox/agar'; import { Assert, UnitTest } from '@ephox/bedrock-client'; import { Arr, Optional } from '@ephox/katamari'; import { Compare, SugarBody } from '@ephox/sugar'; import * as Behaviour from 'ephox/alloy/api/behaviour/Behaviour'; import { Replacing } from 'ephox/alloy/api/behaviour/Replacing'; import type { AlloyComponent } from 'ephox/alloy/api/component/ComponentApi'; import * as DomFactory from 'ephox/alloy/api/component/DomFactory'; import * as GuiFactory from 'ephox/alloy/api/component/GuiFactory'; import * as Memento from 'ephox/alloy/api/component/Memento'; import type { AlloySpec } from 'ephox/alloy/api/component/SpecTypes'; import { Container } from 'ephox/alloy/api/ui/Container'; import * as GuiSetup from 'ephox/alloy/test/GuiSetup'; UnitTest.asynctest('ReplacingTest', (success, failure) => { const make = (reuseDom: boolean) => Container.sketch({ containerBehaviours: Behaviour.derive([ Replacing.config({ reuseDom }) ]), components: [ Container.sketch({ dom: { tag: 'span' }}) ] }); const memWithoutReuse = Memento.record(make(false)); const memWithReuse = Memento.record(make(true)); GuiSetup.setup((_store, _doc, _body) => GuiFactory.build( Container.sketch({ containerBehaviours: Behaviour.derive([ Replacing.config({ }) ]), components: [ memWithoutReuse.asSpec(), memWithReuse.asSpec() ] }) ), (_doc, _body, _gui, component, _store) => { const withoutReuseComp = memWithoutReuse.get(component); const withReuseComp = memWithReuse.get(component); const dog = GuiFactory.premade( GuiFactory.build({ uid: 'dog', dom: { tag: 'div' } }) ); const makeTag = (tag: string, classes: string[] = []): AlloySpec => ({ dom: { tag, classes }, components: [ ] }); const sCheckReplaceAtWith = (label: string, comp: AlloyComponent, expectedClasses: string[], inputClasses: string[], replaceeIndex: number, replaceSpec: Optional) => Logger.t( `${label}: Check replaceAt(${replaceeIndex}, with spec for data: [${inputClasses.join(', ')}]`, Step.sync(() => { Replacing.set(comp, Arr.map(inputClasses, (ic) => makeTag('div', [ ic ])) ); Replacing.replaceAt(comp, replaceeIndex, replaceSpec); Assertions.assertStructure( 'Asserting structure', ApproxStructure.build((s, _str, arr) => s.element('div', { children: Arr.map(expectedClasses, (ec) => s.element('div', { classes: [ arr.has(ec) ] })) })), comp.element ); }) ); const sCheckReplaceAt = (label: string, comp: AlloyComponent, expectedClasses: string[], inputClasses: string[], replaceeIndex: number, replaceClass: Optional) => sCheckReplaceAtWith(label, comp, expectedClasses, inputClasses, replaceeIndex, replaceClass.map((clazz) => makeTag('div', [ clazz ]))); return Arr.map([ { comp: withoutReuseComp, label: 'Without reuse' }, { comp: withReuseComp, label: 'With reuse' }, ], (spec) => { return Log.stepsAsStep('TBA', spec.label, [ Assertions.sAssertStructure( 'Initially, has a single span', ApproxStructure.build((s, _str, _arr) => s.element('div', { children: [ s.element('span', {}) ] })), spec.comp.element ), Step.sync(() => { Replacing.set(spec.comp, []); }), Assertions.sAssertStructure( 'After set([]), is empty', ApproxStructure.build((s, _str, _arr) => s.element('div', { children: [] })), spec.comp.element ), Step.sync(() => { Assert.eq('Should have no contents', [], Replacing.contents(spec.comp)); }), Step.sync(() => { Replacing.set(spec.comp, [ Container.sketch({ uid: 'first' }), Container.sketch({ uid: 'second' }) ]); }), Assertions.sAssertStructure( 'After first time of replace([ first, second ])', ApproxStructure.build((s, _str, _arr) => s.element('div', { children: [ s.element('div', {}), s.element('div', {}) ] })), spec.comp.element ), Step.sync(() => { Assert.eq('Should have 2 children', 2, Replacing.contents(spec.comp).length); }), Logger.t( 'Repeating adding the same uids to check clearing is working', Step.sync(() => { Replacing.set(spec.comp, [ Container.sketch({ uid: 'first' }), Container.sketch({ uid: 'second' }) ]); }) ), Assertions.sAssertStructure( 'After second time of set([ first, second ])', ApproxStructure.build((s, _str, _arr) => s.element('div', { children: [ s.element('div', {}), s.element('div', {}) ] })), spec.comp.element ), Step.sync(() => { Assert.eq('Should have 2 children still', 2, Replacing.contents(spec.comp).length); }), Logger.t( 'Replacing.append to put a new thing at the end.', Step.sync(() => { Replacing.append(spec.comp, Container.sketch({ dom: { tag: 'span' }})); }) ), Assertions.sAssertStructure( 'After append(span)', ApproxStructure.build((s, _str, _arr) => s.element('div', { children: [ s.element('div', {}), s.element('div', {}), s.element('span', {}) ] })), spec.comp.element ), Step.sync(() => { Assert.eq('Should have 3 children now', 3, Replacing.contents(spec.comp).length); }), Logger.t( 'Replacing.prepend to put a new thing at the start', Step.sync(() => { Replacing.prepend(spec.comp, Container.sketch({ dom: { tag: 'label' } })); }) ), Assertions.sAssertStructure( 'After prepend(label)', ApproxStructure.build((s, _str, _arr) => s.element('div', { children: [ s.element('label', {}), s.element('div', {}), s.element('div', {}), s.element('span', {}) ] })), spec.comp.element ), Step.sync(() => { Assert.eq('Should have 4 children now', 4, Replacing.contents(spec.comp).length); }), Logger.t( 'Replacing.remove to remove the second div', Step.sync(() => { const second = spec.comp.getSystem().getByUid('second').getOrDie(); Replacing.remove(spec.comp, second); }) ), Assertions.sAssertStructure( 'After remove(second)', ApproxStructure.build((s, _str, _arr) => s.element('div', { children: [ s.element('label', {}), s.element('div', {}), s.element('span', {}) ] })), spec.comp.element ), Step.sync(() => { Assert.eq('Should have 3 children again', 3, Replacing.contents(spec.comp).length); }), Logger.t( 'Removing should have removed from world, so I should be able to re-add it', Step.sync(() => { Replacing.append(spec.comp, Container.sketch({ uid: 'second' })); }) ), Assertions.sAssertStructure( 'After append(second) after remove(second)', ApproxStructure.build((s, _str, _arr) => s.element('div', { children: [ s.element('label', {}), s.element('div', {}), s.element('span', {}), s.element('div', {}) ] })), spec.comp.element ), Step.sync(() => { Assert.eq('Should have 4 children again', 4, Replacing.contents(spec.comp).length); }), Logger.t( 'Different positions on premade', Step.sync(() => { const specA = { dom: { tag: 'div', classes: [ 'a' ] }}; const specB = { dom: { tag: 'span', classes: [ 'b' ] }}; const premadeB = GuiFactory.premade(GuiFactory.build({ dom: { tag: 'span', classes: [ 'premade-b' ] } })); Logger.sync( 'Checking moving premade', () => { Replacing.set(spec.comp, [ specA, premadeB ]); Assertions.assertStructure( 'Initial stage', ApproxStructure.build((s, str, arr) => { return s.element('div', { children: [ s.element('div', { classes: [ arr.has('a') ] }), s.element('span', { classes: [ arr.has('premade-b') ] }) ] }); }), spec.comp.element ); Replacing.set(spec.comp, [ premadeB, specA ]); Assertions.assertStructure( 'Switching premadeB and specA', ApproxStructure.build((s, str, arr) => { return s.element('div', { children: [ s.element('span', { classes: [ arr.has('premade-b') ] }), s.element('div', { classes: [ arr.has('a') ] }) ] }); }), spec.comp.element ); Replacing.set(spec.comp, [ { dom: { tag: 'blockquote', }, components: [ premadeB ] } ]); Assertions.assertStructure( 'Checking structure blockqoute(premadeB)', ApproxStructure.build((s, str, arr) => { return s.element('div', { children: [ s.element('blockquote', { children: [ s.element('span', { classes: [ arr.has('premade-b') ] }) ] }) ] }); }), spec.comp.element ); Replacing.set(spec.comp, [ { dom: { tag: 'span', }, components: [ specB, premadeB, specA ] } ]); Assertions.assertStructure( 'Checking structure span(specB, premadeB, specA)', ApproxStructure.build((s, str, arr) => { return s.element('div', { children: [ s.element('span', { children: [ s.element('span', { classes: [ arr.has('b') ] }), s.element('span', { classes: [ arr.has('premade-b') ] }), s.element('div', { classes: [ arr.has('a') ] }) ] }) ] }); }), spec.comp.element ); Replacing.set(spec.comp, [ { dom: { tag: 'span', }, components: [ premadeB, specA ] } ]); Assertions.assertStructure( 'Checking structure span(premadeB, specA)', ApproxStructure.build((s, str, arr) => { return s.element('div', { children: [ s.element('span', { children: [ s.element('span', { classes: [ arr.has('premade-b') ] }), s.element('div', { classes: [ arr.has('a') ] }) ] }) ] }); }), spec.comp.element ); } ); }) ), sCheckReplaceAt( '.replaceAt 0 of 0 - should do nothing', spec.comp, [], [], 0, Optional.some('replaceAt-0') ), sCheckReplaceAt( '.replaceAt 0 of 1', spec.comp, [ 'replaceAt-0' ], [ 'original' ], 0, Optional.some('replaceAt-0') ), sCheckReplaceAt( '.replaceAt 0 of 3', spec.comp, [ 'replaceAt-0', 'original2', 'original3' ], [ 'original1', 'original2', 'original3' ], 0, Optional.some('replaceAt-0') ), sCheckReplaceAt( '.replaceAt 2 of 3', spec.comp, [ 'original1', 'original2', 'replaceAt-2' ], [ 'original1', 'original2', 'original3' ], 2, Optional.some('replaceAt-2') ), sCheckReplaceAt( '.replaceAt 1 of 3 with nothing', spec.comp, [ 'original1', 'original3' ], [ 'original1', 'original2', 'original3' ], 1, Optional.none() ), sCheckReplaceAtWith( '.replaceAt 2 of 3 with premade spec', spec.comp, [ 'original1', 'original2', 'replaceAt-2' ], [ 'original1', 'original2', 'original3' ], 2, Optional.some(GuiFactory.premade(GuiFactory.build(makeTag('div', [ 'replaceAt-2' ])))) ) ]); }).concat([ Step.sync(() => { Replacing.set(withReuseComp, [ { dom: { tag: 'label' }} ]); }), Chain.asStep(withReuseComp.element, [ UiFinder.cFindIn('label'), Chain.op((label) => { label.dom.magicAttribute = 24; Replacing.set(withReuseComp, [ { dom: { tag: 'label' }} ]); }), Chain.inject(withReuseComp.element), UiFinder.cFindIn('label'), Chain.op((label) => { Assert.eq('magic attribute', 24, label.dom.magicAttribute); }) ]), Chain.asStep(withReuseComp.element, [ Chain.op(() => { Replacing.set(withReuseComp, [ { dom: { tag: 'div' }, components: [ { dom: { tag: 'label', classes: [ 'nested-label' ] }}, dog ] } ]); }), UiFinder.cFindIn('.nested-label'), Chain.op((label) => { label.dom.magicAttribute = 25; Replacing.set(withReuseComp, [ { dom: { tag: 'div' }, components: [ { dom: { tag: 'label', classes: [ 'other-label' ] }}, dog ] } ]); }), Chain.inject(withReuseComp.element), UiFinder.cFindIn('.other-label'), Chain.op((label) => { Assert.eq('Testing that the DOM element has stuck around (and has kept a variable)', 25, label.dom.magicAttribute); }) ]), Step.sync(() => { Replacing.set(withReuseComp, [ { dom: DomFactory.fromHtml('
'), components: [ { dom: DomFactory.fromHtml('something else'), components: [ { dom: DomFactory.fromHtml('italics') } ] } ] } ]); const beforeLastChild = UiFinder.findIn(SugarBody.body(), 'em[data-dog]').getOrDie(); // Replace while only changing the class of the middle component Replacing.set(withReuseComp, [ { dom: DomFactory.fromHtml('
'), components: [ { dom: DomFactory.fromHtml('something else'), components: [ { dom: DomFactory.fromHtml('italics') } ] } ] } ]); const afterLastChild = UiFinder.findIn(SugarBody.body(), 'em[data-dog]').getOrDie(); Assert.eq('The DOM elements should not have been re-rendered', beforeLastChild.dom, afterLastChild.dom); }), // Now, test a premade Step.sync(() => { const premadeA = GuiFactory.build({ dom: { tag: 'div', classes: [ 'mover' ] } }); Replacing.set(withReuseComp, [ { dom: { tag: 'div', classes: [ 'first' ] } }, GuiFactory.premade(premadeA) ]); Assertions.assertStructure( 'Before moving premade', ApproxStructure.build((s, str, arr) => { return s.element('div', { children: [ s.element('div', { classes: [ arr.has('first') ] }), s.element('div', { classes: [ arr.has('mover') ] }) ] }); }), withReuseComp.element ); Replacing.set(withReuseComp, [ GuiFactory.premade(premadeA) ]); Assertions.assertStructure( 'After moving premade to be the first child', ApproxStructure.build((s, str, arr) => { return s.element('div', { children: [ s.element('div', { classes: [ arr.has('mover') ] }) ] }); }), withReuseComp.element ); }), Step.sync(() => { Replacing.set(withReuseComp, [ { dom: { tag: 'div', innerHtml: '

Paragraph

' }, components: [ { dom: { tag: 'span', classes: [ 'foo' ] } } ] } ]); const beforeSpan = UiFinder.findIn(SugarBody.body(), 'span.foo').getOrDie(); Replacing.set(withReuseComp, [ { dom: { tag: 'div', innerHtml: '

Paragraph

' }, components: [ { dom: { tag: 'span', classes: [ 'bar' ] } } ] } ]); const afterSpan = UiFinder.findIn(SugarBody.body(), 'span.bar').getOrDie(); Assertions.assertStructure( 'Replacing with mixed children', ApproxStructure.build((s, str, arr) => s.element('div', { children: [ s.element('div', { children: [ s.element('p', {}), s.element('span', { classes: [ arr.has('bar') ] }), ] }) ] }) ), withReuseComp.element ); Assert.eq('Span elements were re-rendered as mixed children cannot be patched', false, Compare.eq(afterSpan, beforeSpan)); }) ]); }, success, failure); });