import { strict as assert } from "assert" ; import { ISummaryTree } from "@fluidframework/protocol-definitions" ; import { IChannelServices } from "@fluidframework/datastore-definitions" ; import { Marker , ReferenceType , reservedMarkerIdKey , reservedMarkerSimpleTypeKey , reservedTileLabelsKey , } from "@fluidframework/merge-tree" ; import { MockFluidDataStoreRuntime , MockContainerRuntimeFactory , MockContainerRuntimeFactoryForReconnection , MockContainerRuntimeForReconnection , MockEmptyDeltaConnection , MockStorage , } from "@fluidframework/test-runtime-utils" ; import { SharedString } from "../sharedString" ; import { SharedStringFactory } from "../sequenceFactory" ; describe ( "SharedString" , ( ) => { let sharedString ; let dataStoreRuntime1 ; beforeEach ( ( ) => { dataStoreRuntime1 = new MockFluidDataStoreRuntime ( ) ; sharedString = new SharedString ( dataStoreRuntime1 , "shared-string-1" , SharedStringFactory . Attributes ) ; } ) ; describe ( "SharedString in local state" , ( ) => { beforeEach ( ( ) => { dataStoreRuntime1 . local = true ; } ) ; async function CreateStringAndCompare ( summaryTree ) { const services = { deltaConnection : new MockEmptyDeltaConnection ( ) , objectStorage : MockStorage . createFromSummary ( summaryTree ) } ; const dataStoreRuntime2 = new MockFluidDataStoreRuntime ( ) ; const sharedString2 = new SharedString ( dataStoreRuntime2 , "shared-string-2" , SharedStringFactory . Attributes ) ; await sharedString2 . load ( services ) ; await sharedString2 . loaded ; assert . equal ( sharedString . getText ( ) , sharedString2 . getText ( ) , "Could not correctly load from snapshot" ) ; } function verifyAndReturnSummaryTree ( ) { const summarizeResult = sharedString . getAttachSummary ( ) ; const summaryObjectKeys = Object . keys ( summarizeResult . summary . tree ) ; assert . strictEqual ( summaryObjectKeys . length , 1 , "summary should have one entries" ) ; assert . strictEqual ( summaryObjectKeys [ 0 ] , "content" , "content not present in summary" ) ; const subTree = summarizeResult . summary . tree . content as ISummaryTree ; const subTreeObjectKeys = Object . keys ( subTree . tree ) ; assert . strictEqual ( subTreeObjectKeys . length , 1 , "sub tree should have one entries" ) ; assert . strictEqual ( subTreeObjectKeys [ 0 ] , "header" , "header not present in sub tree" ) ; return summarizeResult . summary ; } it ( "can insert text" , async ( ) => { sharedString . insertText ( 0 , "hello" ) ; assert . equal ( sharedString . getText ( ) , "hello" , "Could not insert text at beginning" ) ; sharedString . insertText ( 5 , "world" ) ; assert . equal ( sharedString . getText ( ) , "helloworld" , "Could not insert text at end" ) ; sharedString . insertText ( 5 , " " ) ; assert . equal ( sharedString . getText ( ) , "hello world" , "Could not insert text in the middle" ) ; } ) ; it ( "can replace text" , async ( ) => { sharedString . insertText ( 0 , "hello world" ) ; sharedString . replaceText ( 6 , 11 , "there!" ) ; assert . equal ( sharedString . getText ( ) , "hello there!" , "Could not replace text" ) ; sharedString . replaceText ( 0 , 5 , "hi" ) ; assert . equal ( sharedString . getText ( ) , "hi there!" , "Could not replace text at beginning" ) ; } ) ; it ( "can remove text" , async ( ) => { sharedString . insertText ( 0 , "hello world" ) ; sharedString . removeText ( 5 , 11 ) ; assert . equal ( sharedString . getText ( ) , "hello" , "Could not remove text" ) ; sharedString . removeText ( 0 , 3 ) ; assert . equal ( sharedString . getText ( ) , "lo" , "Could not remove text from beginning" ) ; } ) ; it ( "can annotate the text" , async ( ) => { const text = "hello world" ; const styleProps = { style : "bold" } ; sharedString . insertText ( 0 , text , styleProps ) ; for ( let i = 0 ; i < text . length ; i ++ ) { assert . deepEqual ( { ... sharedString . getPropertiesAtPosition ( i ) } , { ... styleProps } , "Could not add props" ) ; } const colorProps = { color : "green" } ; sharedString . annotateRange ( 6 , text . length , colorProps ) ; for ( let i = 6 ; i < text . length ; i ++ ) { assert . deepEqual ( { ... sharedString . getPropertiesAtPosition ( i ) } , { ... styleProps , ... colorProps } , "Could not annotate props" ) ; } } ) ; it ( "can handle null annotations in text" , async ( ) => { const text = "hello world" ; const startingProps = { style : "bold" , color : null } ; sharedString . insertText ( 0 , text , startingProps ) ; for ( let i = 0 ; i < text . length ; i ++ ) { assert . strictEqual ( sharedString . getPropertiesAtPosition ( i ) . color , undefined , "Null values allowed in properties" ) ; } const updatedProps = { style : null } ; sharedString . annotateRange ( 6 , text . length , updatedProps ) ; for ( let i = 6 ; i < text . length ; i ++ ) { assert . strictEqual ( sharedString . getPropertiesAtPosition ( i ) . style , undefined , "Null values allowed in properties" ) ; } } ) ; it ( "can insert marker" , ( ) => { sharedString . insertText ( 0 , "hello world" ) ; sharedString . insertMarker ( 6 , ReferenceType . Simple , { [ reservedMarkerIdKey ] : "markerId" , [ reservedMarkerSimpleTypeKey ] : "markerKeyValue" } ) ; const simpleMarker = sharedString . getMarkerFromId ( "markerId" ) ; assert . equal ( simpleMarker . type , "Marker" , "Could not get simple marker" ) ; assert . equal ( simpleMarker . properties . markerId , "markerId" , "markerId is incorrect" ) ; assert . equal ( simpleMarker . properties . markerSimpleType , "markerKeyValue" , "markerSimpleType is incorrrect" ) ; sharedString . insertMarker ( 0 , ReferenceType . Tile , { [ reservedTileLabelsKey ] : [ "tileLabel" ] , [ reservedMarkerIdKey ] : "tileMarkerId" } ) ; const { parallelMarkers } = sharedString . getTextAndMarkers ( "tileLabel" ) ; const parallelMarker = parallelMarkers [ 0 ] ; assert . equal ( parallelMarker . type , "Marker" , "Could not get tile marker" ) ; assert . equal ( parallelMarker . properties . markerId , "tileMarkerId" , "tile markerId is incorrect" ) ; } ) ; it ( "can annotate marker" , ( ) => { sharedString . insertText ( 0 , "hello world" ) ; sharedString . insertMarker ( 6 , ReferenceType . Simple , { [ reservedMarkerIdKey ] : "markerId" } ) ; const props = { color : "blue" } ; const simpleMarker = sharedString . getMarkerFromId ( "markerId" ) as Marker ; sharedString . annotateMarker ( simpleMarker , props ) ; assert . equal ( simpleMarker . properties . color , "blue" , "Could not annotate marker" ) ; } ) ; it ( "replace zero range" , async ( ) => { sharedString . insertText ( 0 , "123" ) ; sharedString . replaceText ( 1 , 1 , "\u00e4\u00c4" ) ; assert . equal ( sharedString . getText ( ) , "1\u00e4\u00c423" , "Could not replace zero range" ) ; } ) ; it ( "replace negative range" , async ( ) => { sharedString . insertText ( 0 , "123" ) ; sharedString . replaceText ( 2 , 1 , "aaa" ) ; assert . equal ( sharedString . getText ( ) , "12aaa3" , "Could not replace negative range" ) ; } ) ; it ( "can load a SharedString from summary" , async ( ) => { const insertText = "text" ; const segmentCount = 1000 ; sharedString . initializeLocal ( ) ; for ( let i = 0 ; i < segmentCount ; i = i + 1 ) { sharedString . insertText ( 0 , `${ insertText }${ i }` ) ; } let summaryTree = verifyAndReturnSummaryTree ( ) ; await CreateStringAndCompare ( summaryTree ) ; for ( let i = 0 ; i < segmentCount ; i = i + 1 ) { sharedString . insertText ( 0 , `${ insertText }-${ i }` ) ; } summaryTree = verifyAndReturnSummaryTree ( ) ; await CreateStringAndCompare ( summaryTree ) ; } ) ; } ) ; describe ( "SharedString op processing in local state" , ( ) => { it ( "should correctly process operations sent in local state" , async ( ) => { dataStoreRuntime1 . local = true ; sharedString . initializeLocal ( ) ; sharedString . insertText ( 0 , "hello world" ) ; sharedString . replaceText ( 6 , 11 , "there" ) ; const containerRuntimeFactory = new MockContainerRuntimeFactory ( ) ; const dataStoreRuntime2 = new MockFluidDataStoreRuntime ( ) ; const containerRuntime2 = containerRuntimeFactory . createContainerRuntime ( dataStoreRuntime2 ) ; const services2 = { deltaConnection : containerRuntime2 . createDeltaConnection ( ) , objectStorage : MockStorage . createFromSummary ( sharedString . getAttachSummary ( ) . summary ) } ; const sharedString2 = new SharedString ( dataStoreRuntime2 , "shared-string-2" , SharedStringFactory . Attributes ) ; await sharedString2 . load ( services2 ) ; dataStoreRuntime1 . local = false ; const containerRuntime1 = containerRuntimeFactory . createContainerRuntime ( dataStoreRuntime1 ) ; const services1 = { deltaConnection : containerRuntime1 . createDeltaConnection ( ) , objectStorage : new MockStorage ( undefined ) } ; sharedString . connect ( services1 ) ; assert . equal ( sharedString . getText ( ) , "hello there" , "The first string does not have the text" ) ; assert . equal ( sharedString2 . getText ( ) , "hello there" , "The second string does not have the text" ) ; sharedString2 . insertText ( 0 , "well " ) ; containerRuntimeFactory . processAllMessages ( ) ; assert . equal ( sharedString . getText ( ) , "well hello there" , "The first string does not have the text" ) ; assert . equal ( sharedString2 . getText ( ) , "well hello there" , "The second string does not have the text" ) ; } ) ; } ) ; describe ( "SharedString in connected state with a remote SharedString" , ( ) => { let sharedString2 ; let containerRuntimeFactory ; beforeEach ( ( ) => { containerRuntimeFactory = new MockContainerRuntimeFactory ( ) ; dataStoreRuntime1 . local = false ; const containerRuntime1 = containerRuntimeFactory . createContainerRuntime ( dataStoreRuntime1 ) ; const services1 = { deltaConnection : containerRuntime1 . createDeltaConnection ( ) , objectStorage : new MockStorage ( ) } ; sharedString . initializeLocal ( ) ; sharedString . connect ( services1 ) ; const dataStoreRuntime2 = new MockFluidDataStoreRuntime ( ) ; const containerRuntime2 = containerRuntimeFactory . createContainerRuntime ( dataStoreRuntime2 ) ; const services2 = { deltaConnection : containerRuntime2 . createDeltaConnection ( ) , objectStorage : new MockStorage ( ) } ; sharedString2 = new SharedString ( dataStoreRuntime2 , "shared-string-2" , SharedStringFactory . Attributes ) ; sharedString2 . initializeLocal ( ) ; sharedString2 . connect ( services2 ) ; } ) ; it ( "can insert text" , async ( ) => { sharedString . insertText ( 0 , "hello" ) ; containerRuntimeFactory . processAllMessages ( ) ; assert . equal ( sharedString . getText ( ) , "hello" , "Could not insert text at beginning" ) ; assert . equal ( sharedString2 . getText ( ) , "hello" , "Could not insert text at beginning in remote string" ) ; sharedString2 . insertText ( 5 , " world" ) ; containerRuntimeFactory . processAllMessages ( ) ; assert . equal ( sharedString . getText ( ) , "hello world" , "Could not insert text at end" ) ; assert . equal ( sharedString2 . getText ( ) , "hello world" , "Could not insert text at end in remote string" ) ; } ) ; it ( "can replace text" , async ( ) => { sharedString . insertText ( 0 , "hello world" ) ; sharedString . replaceText ( 6 , 11 , "there!" ) ; containerRuntimeFactory . processAllMessages ( ) ; assert . equal ( sharedString . getText ( ) , "hello there!" , "Could not replace text" ) ; assert . equal ( sharedString2 . getText ( ) , "hello there!" , "Could not replace text in remote string" ) ; } ) ; it ( "can remove text" , async ( ) => { sharedString . insertText ( 0 , "hello world" ) ; sharedString . removeText ( 5 , 11 ) ; containerRuntimeFactory . processAllMessages ( ) ; assert . equal ( sharedString . getText ( ) , "hello" , "Could not remove text" ) ; assert . equal ( sharedString2 . getText ( ) , "hello" , "Could not remove text from remote string" ) ; } ) ; it ( "can annotate the text" , async ( ) => { const text = "hello world" ; const styleProps = { style : "bold" } ; sharedString . insertText ( 0 , text , styleProps ) ; containerRuntimeFactory . processAllMessages ( ) ; for ( let i = 0 ; i < text . length ; i ++ ) { assert . deepEqual ( { ... sharedString . getPropertiesAtPosition ( i ) } , { ... styleProps } , "Could not add props" ) ; assert . deepEqual ( { ... sharedString2 . getPropertiesAtPosition ( i ) } , { ... styleProps } , "Could not add props to remote string" ) ; } const colorProps = { color : "green" } ; sharedString . annotateRange ( 6 , text . length , colorProps ) ; containerRuntimeFactory . processAllMessages ( ) ; for ( let i = 6 ; i < text . length ; i ++ ) { assert . deepEqual ( { ... sharedString . getPropertiesAtPosition ( i ) } , { ... styleProps , ... colorProps } , "Could not annotate props" ) ; assert . deepEqual ( { ... sharedString2 . getPropertiesAtPosition ( i ) } , { ... styleProps , ... colorProps } , "Could not annotate props in remote string" ) ; } } ) ; it ( "can insert marker" , ( ) => { const label = "tileLabel" ; const id = "tileMarkerId" ; const simpleKey = "tileMarkerKey" ; const verifyMarker = ( marker ) => { assert . equal ( marker . type , "Marker" , "Could not get simple marker" ) ; assert . equal ( marker . properties . markerId , id , "markerId is incorrect" ) ; assert . equal ( marker . properties . markerSimpleType , simpleKey , "markerSimpleType is incorrrect" ) ; assert . equal ( marker . properties . referenceTileLabels [ 0 ] , label , "markerSimpleType is incorrrect" ) ; } ; sharedString . insertText ( 0 , "hello world" ) ; sharedString . insertMarker ( 6 , ReferenceType . Tile , { [ reservedTileLabelsKey ] : [ label ] , [ reservedMarkerIdKey ] : id , [ reservedMarkerSimpleTypeKey ] : simpleKey } ) ; containerRuntimeFactory . processAllMessages ( ) ; const simpleMarker1 = sharedString . getMarkerFromId ( id ) ; verifyMarker ( simpleMarker1 ) ; const simpleMarker2 = sharedString2 . getMarkerFromId ( id ) ; verifyMarker ( simpleMarker2 ) ; const textAndMarker1 = sharedString . getTextAndMarkers ( label ) ; verifyMarker ( textAndMarker1 . parallelMarkers [ 0 ] ) ; const textAndMarker2 = sharedString2 . getTextAndMarkers ( label ) ; verifyMarker ( textAndMarker2 . parallelMarkers [ 0 ] ) ; } ) ; it ( "can annotate marker" , ( ) => { sharedString . insertText ( 0 , "hello world" ) ; sharedString . insertMarker ( 6 , ReferenceType . Simple , { [ reservedMarkerIdKey ] : "markerId" } ) ; const props = { color : "blue" } ; const simpleMarker = sharedString . getMarkerFromId ( "markerId" ) as Marker ; sharedString . annotateMarker ( simpleMarker , props ) ; containerRuntimeFactory . processAllMessages ( ) ; const simpleMarker1 = sharedString . getMarkerFromId ( "markerId" ) as Marker ; assert . equal ( simpleMarker1 . properties . color , "blue" , "Could not annotate marker" ) ; const simpleMarker2 = sharedString . getMarkerFromId ( "markerId" ) as Marker ; assert . equal ( simpleMarker2 . properties . color , "blue" , "Could not annotate marker in remote string" ) ; } ) ; } ) ; describe ( "reconnect" , ( ) => { let containerRuntimeFactory ; let containerRuntime1 ; let containerRuntime2 ; let sharedString2 ; beforeEach ( async ( ) => { containerRuntimeFactory = new MockContainerRuntimeFactoryForReconnection ( ) ; containerRuntime1 = containerRuntimeFactory . createContainerRuntime ( dataStoreRuntime1 ) ; const services1 = { deltaConnection : containerRuntime1 . createDeltaConnection ( ) , objectStorage : new MockStorage ( ) } ; sharedString . initializeLocal ( ) ; sharedString . connect ( services1 ) ; const runtime2 = new MockFluidDataStoreRuntime ( ) ; containerRuntime2 = containerRuntimeFactory . createContainerRuntime ( runtime2 ) ; sharedString2 = new SharedString ( runtime2 , "shared-string-2" , SharedStringFactory . Attributes ) ; const services2 = { deltaConnection : containerRuntime2 . createDeltaConnection ( ) , objectStorage : new MockStorage ( ) } ; sharedString2 . initializeLocal ( ) ; sharedString2 . connect ( services2 ) ; } ) ; it ( "can resend unacked ops on reconnection" , async ( ) => { sharedString . insertText ( 0 , "helloworld" ) ; sharedString . replaceText ( 5 , 10 , " friend" ) ; for ( let i = 0 ; i < 10 ; i ++ ) { containerRuntime1 . connected = false ; containerRuntime1 . connected = true ; } containerRuntimeFactory . processAllMessages ( ) ; assert . equal ( sharedString2 . getText ( ) , "hello friend" ) ; } ) ; it ( "can store ops in disconnected state and resend them on reconnection" , async ( ) => { containerRuntime1 . connected = false ; sharedString . insertText ( 0 , "helloworld" ) ; sharedString . replaceText ( 5 , 10 , " friend" ) ; containerRuntime1 . connected = true ; containerRuntimeFactory . processAllMessages ( ) ; assert . equal ( sharedString2 . getText ( ) , "hello friend" ) ; } ) ; } ) ; } ) ;