Ngx-quill: Testing component

Created on 24 Sep 2019  路  22Comments  路  Source: KillerCodeMonkey/ngx-quill

Hi,

I'm facing an issue when I try to test a component including the quill-editor
I got the same results as in https://github.com/KillerCodeMonkey/ngx-quill/issues/153


I tried 2 approach :

  • Importing QuillModule
    As the quill-editor component is a child of my component I did tried like in the source test file to import QuillModule in my component, pass it to the imports, provide the QuillModule.forRoot().providers but I always got a TypeError: Cannot read property 'classList' of null
    And I have no ngClass or anything related to class manipulation in my component.
  • Mock the QuillEditor component
    Then I tried like in the source test file to make a stub component and declare it in my component instead of Importing QuillModule I got issues with the FormModule Error: No value accessor for form control with unspecified name attribute
    I don't use a name attribute on the quillEditor, you don't neither but even if I add one the error is still there :/

Do you have any idea ?
Or could you add some tests in your exemple repo ?
It would help a lot. I can try to help if you want too.

Have a nice day, thanks !

All 22 comments

could you share some example tests?

The 'classList' of null stuff is part of a PR, where you can now pass classes as a string, e.g.

<quill-editor classes="testi test"></quill-editor>

So maybe you pass something invalid there?

Ok, I tried to add classes="aze" on my quill-editor component but didn't change.

Here is what I got :



text-layer-editor.component.html

<div
  class="rm-text-layer--editor--wrapper"
  [ngStyle]=getStyle()>

  <quill-editor
    class="rm-text-layer--editor"
    [placeholder]="defaultPlaceholder"
    format="html"
    [(ngModel)]="rmTextLayer.content"
    [modules]="modules"
    (onBlur)="blur()"
    (onEditorCreated)="onCreated($event)"
    (onEditorChanged)="onChange($event)"
    [preserveWhitespace]="true">
  </quill-editor>
</div>



text-layer-editor.component.spec.ts

import { async, ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule } from '@angular/forms'
import { Subject } from 'rxjs'
import { QuillModule } from 'ngx-quill'

import { TextLayerEditorComponent } from './text-layer-editor.component'
import { TextLayerEditorService } from './text-layer-editor.service'


describe('TextLayerEditorComponent', () => {
  let component: TextLayerEditorComponent
  let fixture: ComponentFixture<TextLayerEditorComponent>
  let fakeTextLayerEditorService: any

  beforeEach(async(() => {
    fakeTextLayerEditorService = {}

    TestBed.configureTestingModule({
      declarations: [ TextLayerEditorComponent ],
      imports: [FormsModule, QuillModule],
      providers: [
        { provide: TextLayerEditorService, useValue: fakeTextLayerEditorService },
        QuillModule.forRoot().providers,
      ]
    }).compileComponents()
    fixture = TestBed.createComponent(TextLayerEditorComponent) as ComponentFixture<TextLayerEditorComponent>
  }))

  beforeEach(() => {
    component = fixture.componentInstance
    component.setEditor = new Subject()
    fixture.detectChanges()
  })

  it('should create', () => {
    expect(component).toBeTruthy()
  })
})


Bonus : not really useful here :



text-layer-editor.component.ts

import { Component, OnInit, Input, OnDestroy } from '@angular/core'
import { Observable } from 'rxjs'

import { TextLayer } from '../config/text-layer.model'
import { getPosition } from '../../config/position-helper'
import { getSelectedNode } from '../config/text-layer-helper'
import { TextLayerEditorService } from './text-layer-editor.service'
import { quillModules } from './quill-editor-config'

@Component({
  selector: 'rm-editor-text-layer-editor',
  templateUrl: './text-layer-editor.component.html',
  styleUrls: ['./text-layer-editor.component.sass']
})
export class TextLayerEditorComponent implements OnInit, OnDestroy {

  @Input() setEditor: Observable<any>

  setEditorSubscription: any
  editor: any
  rmTextLayer: TextLayer = { x: 0, y: 0, w: 0, h: 0 }
  defaultPlaceholder = 'Enter text here'
  modules = quillModules

  constructor(private _editorService: TextLayerEditorService) {}

  get isDisplayed(): boolean {
    return this._editorService.isDisplayed
  }

  set isDisplayed(newVal: boolean) {
    this._editorService.isDisplayed = newVal
  }

  ngOnInit() {
    this.setEditorSubscription = this.setEditor.subscribe(
      (params) => this.handleSetEditor(params)
    )
  }

  onCreated(event: any) {
    this.editor = event
  }

  handleSetEditor(params: any) {
    this.isDisplayed = true
    this.rmTextLayer = params.layer
    setTimeout(() => {
      this.editor.focus()
      const range = document.createRange()
      const sel = window.getSelection()
      const { selectedNodeIndex, carretOffset } = params.carretPosition
      const selectedNode = getSelectedNode(this.editor.root, selectedNodeIndex)
      // range.setStart(selectedNode, carretOffset)
      range.selectNodeContents(selectedNode)
      // range.collapse(true)
      sel.removeAllRanges()
      sel.addRange(range)
    }, 20)
  }

  getStyle() {
    return {
      display: (this.isDisplayed) ? 'block' : 'none',
      ...getPosition(this.rmTextLayer)
    }
  }

  blur() {
    this.isDisplayed = false
  }

  onChange(ev: any) {
    this._editorService.onEditorChange(ev.editor)
  }

  ngOnDestroy() {
    this.setEditorSubscription.unsubscribe()
  }
}

Thats strange. i do not know if i have the time this week to inspect this deeper.

Maybe you can just try to add a simple test to the example repo?

Yeah I can't test my component's including quill-editor, quill-view or quill-toolbar yet :(
I'm still new to Karma but it's just a expect(component).toBeTruthy() so that's weird.

I'll see what I can do on the example repo.

strange, i added a simple test to the example repo and it is working quite fine for me:

https://github.com/KillerCodeMonkey/ngx-quill-example/blob/master/src/app/default/default.components.spec.ts

Sweet !
I spotted a difference here.
In your beforeEach you didn't add fixture.detectChanges()
If I remove it, it works. If I let it it won't

but even with.

describe('TextLayerEditorComponent', () => {
  let fixture: ComponentFixture<DefaultComponent>

  beforeEach(async(() => {

    TestBed.configureTestingModule({
      declarations: [ DefaultComponent ],
      imports: [FormsModule, QuillModule],
      providers: QuillModule.forRoot().providers
    }).compileComponents()
    fixture = TestBed.createComponent(DefaultComponent) as ComponentFixture<DefaultComponent>
    fixture.detectChanges()
  }))

  it('should create', () => {
    expect(fixture.componentInstance).toBeTruthy()
  })
})

it is working

True, that's interesting but I'm a bit clueless :/

ah i think your providers are wrong, just try the following:

TestBed.configureTestingModule({
      declarations: [ TextLayerEditorComponent ],
      imports: [FormsModule, QuillModule],
      providers: [
        { provide: TextLayerEditorService, useValue: fakeTextLayerEditorService },
        ...QuillModule.forRoot().providers,
      ]
    }).compileComponents()
    fixture = TestBed.createComponent(TextLayerEditorComponent) as ComponentFixture<TextLayerEditorComponent>
  }))

Yeah I tried it too, I even removed the fakeTextLayerEditorService like this

describe('TextLayerEditorComponent', () => {
  let component: TextLayerEditorComponent
  let fixture: ComponentFixture<TextLayerEditorComponent>

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ TextLayerEditorComponent ],
      imports: [FormsModule, QuillModule],
      providers: QuillModule.forRoot().providers
    }).compileComponents()
    fixture = TestBed.createComponent(TextLayerEditorComponent) as ComponentFixture<TextLayerEditorComponent>
  }))

  beforeEach(() => {
    component = fixture.componentInstance
    component.setEditor = new Subject()
    fixture.detectChanges()
  })

  it('should create', () => {
    expect(component).toBeTruthy()
  })
})

And still got the same issue 馃槺

okay and can you try to remove "class="rm-text-layer--editor" from the editor component?.

Yes, it's the same.

In your example, the one you use with format="html" is wrapped in a formGroup and has a formControlName
Is that required ?

i do not think so.

the example testcase is also not using formControls :)

could you simplify your component a little bit?

keep in mind that detectChange, triggers the changeDetection, so ngOnInit gets called, so could you comment out your handleSetEditor and ngOnInit function?

Yes I tried but didn't change neither
New hint, it work when I comment my modules = quillModules



It's coming from this config for info

import Quill from 'quill'

import { FontSizeConfig } from '../config/available-font-sizes'
import { AvailableFonts } from '../config/available-fonts'
import { LineHeightConfig } from '../config/available-line-height'
import { LetterSpacingConfig } from '../config/available-letter-spacings'
import { AvailableColors } from '../config/available-colors'
import { BottomSpaceConfig } from './../config/available-bottom-space'
import {
  setBottomSpace,
  updateFontSize,
  updateLineHeight,
  updateLetterSpacing,
  updateBottomSpace,
  setColor,
  setTextAlign
} from '../config/format-helper'

const size = Quill.import('attributors/style/size')
const font = Quill.import('formats/font')
const Parchment = Quill.import('parchment')

const getConf = (whitelistValue: string[], scopeName: string): any => {
  return {scope: Parchment.Scope[scopeName], whitelist: whitelistValue}
}

const newStyle = (
  customName: string, cssProperty: string,
  whitelistValue: string[], scopeName: string = 'INLINE') => {
  return new Parchment.Attributor.Style(customName, cssProperty, getConf(whitelistValue, scopeName))
}

const lineHeightStyle = newStyle('lineHeight', 'line-height', LineHeightConfig.availableString)
const letterSpacingStyle = newStyle('letterSpacing', 'letter-spacing', LetterSpacingConfig.availablePx)
const colorStyle = newStyle('customColor', 'color', AvailableColors)
const bottomSpaceStyle = newStyle('bottomSpace', 'margin-bottom', BottomSpaceConfig.availablePx, 'BLOCK')
const textAlignStyle = newStyle('textAlign', 'text-align', ['left', 'right', 'center', 'justify'], 'BLOCK')

size.whitelist = FontSizeConfig.availablePx
font.whitelist = AvailableFonts

Quill.register(lineHeightStyle, true)
Quill.register(letterSpacingStyle, true)
Quill.register(colorStyle, true)
Quill.register(bottomSpaceStyle, true)
Quill.register(size, true)
Quill.register(font, true)
Quill.register(textAlignStyle, true)

export const quillModules = {
  toolbar: {
    container: '#rm-text-layer--toolbar',
    handlers: {
      increaseFontSize() { updateFontSize(this.quill, 1) },
      decreaseFontSize() { updateFontSize(this.quill, -1) },
      increaseLineHeight() { updateLineHeight(this.quill, 0.5) },
      decreaseLineHeight() { updateLineHeight(this.quill, -0.5) },
      increaseLetterSpacing() { updateLetterSpacing(this.quill, 1) },
      decreaseLetterSpacing() { updateLetterSpacing(this.quill, -1) },
      setColor(value) { setColor(this.quill, value) },
      setBottomSpace(value) { setBottomSpace(this.quill, value) },
      increaseBottomSpace() { updateBottomSpace(this.quill, 1) },
      decreaseBottomSpace() { updateBottomSpace(this.quill, -1) },
      chooseTextAlign(value) { setTextAlign(this.quill, value) },
    }
  },
}



For info here is my format helper

import { getCurrentSelectionParentElement } from './text-layer-helper'

export const pxToNumber = (px: string): number => parseInt(px.replace('px', ''), 10)


/*
* Getters
*********/

export const getCurrentFormat = (quill: any) => {
  return quill.getFormat(quill.selection.savedRange)
}

export const getPropertyFormat = (quill: any, format: any, formatProp: string, defaultVal) => {
  return (format || getCurrentFormat(quill))[formatProp] || defaultVal
}

export const getCurrentFontSize = (quill: any, format?: any): number => {
  const currentSize = getPropertyFormat(quill, format, 'size', '16px')
  return pxToNumber(currentSize)
}

export const getCurrentLineHeight = (quill: any, format?: any): number => {
  const currentValue = getPropertyFormat(quill, format, 'lineHeight', '1.5')
  return parseFloat(currentValue)
}

export const getCurrentLetterSpacing = (quill: any, format?: any): number => {
  const currentValue = getPropertyFormat(quill, format, 'letterSpacing', '0px')
  return pxToNumber(currentValue)
}

export const getCurrentBottomSpace = (quill: any, format?: any): number => {
  const currentValue = getPropertyFormat(quill, format, 'bottomSpace', '0px')
  return pxToNumber(currentValue)
}

export const getTextAlign = (quill: any, format?: any): string => {
  return getPropertyFormat(quill, format, 'textAlign', 'left')
}

export const isCurrentlyBold = (quill: any, format?: any): boolean => {
  return getPropertyFormat(quill, format, 'bold', false)
}

export const isCurrentlyItalic = (quill: any, format?: any): boolean => {
  return getPropertyFormat(quill, format, 'italic', false)
}



/*
* Setters
*********/

const setCustom = (customProp: string, quill: any, value: string) => { quill.format(customProp, value, 'user') }

export const setColor = (quill: any, value: string) => { setCustom('customColor', quill, value) }
export const setFontSize = (quill: any, value: string) => { setCustom('font-size', quill, value) }
export const setLineHeight = (quill: any, value: string) => { setCustom('lineHeight', quill, value) }
export const setLetterSpacing = (quill: any, value: string) => { setCustom('letterSpacing', quill, value) }
export const setBottomSpace = (quill: any, value: string) => { setCustom('bottomSpace', quill, value) }
export const setTextAlign = (quill: any, value: string) => { setCustom('text-align', quill, value) }


/*
* Updates
*********/

export const updateFontSize = (quill: any, variation: number) => {
  setFontSize(quill, `${ getCurrentFontSize(quill) + variation }px`)
}

export const updateLineHeight = (quill: any, variation: number) => {
  setLineHeight(quill, `${ ((getCurrentLineHeight(quill) * 10) + variation) / 10 }`)
}

export const updateLetterSpacing = (quill: any, variation: number) => {
  setLetterSpacing(quill, `${ getCurrentLetterSpacing(quill) + variation }px`)
}

export const updateBottomSpace = (quill: any, variation: number) => {
  const node = getCurrentSelectionParentElement('P')
  const sel = window.getSelection()
  const range = document.createRange()
  range.selectNode(node)
  const userRange = document.createRange()
  userRange.setStart(sel.anchorNode, sel.anchorOffset)
  userRange.setEnd(sel.focusNode, sel.focusOffset)
  sel.removeAllRanges()
  sel.addRange(range)
  setBottomSpace(quill, `${getCurrentBottomSpace(quill) + variation}px`)
  sel.removeAllRanges()
  sel.addRange(userRange)
}

woah but thats a thing of your project.

I will totaly not check your whole quill configuration 馃槈

And the error message with the classList of null stuff makes no sense then?!

what i see is: #rm-text-layer--toolbar is the selector for your toolbar container, but there is no toolbar with that id?

Yes sure, I won't ask for that :)

Yeah the error make no sense :/

The toolbar is defined in an other component.
I use multiple quill-view on my page and one quill-editor bind to a quill-editor-toolbar.
When I click on a quill view I set the editor at the same place and edit the content by binding the content of the view / editor. It works like a charm but not the tests

Thanks for your help, it led me on the toolbar and it was the issue.
In the beforeEach I tried to redefine my component modules and I saw that some document query where called.
So I fixed it like that

beforeEach(() => {
  component = fixture.componentInstance
  component.setEditor = new Subject()
  component.modules = {
    toolbar: [['bold', 'italic']]
  } as any
  fixture.detectChanges()
})

I had to cast it as any because my modules structure is different by default.

Thanks @KillerCodeMonkey always a pleasure to find some solutions with you ! :)

(this way it uses the default quill toolbar instead of trying to fetch a custom one from the DOM, which is defined in an other component)

Ah nice you solved it.

Yeah that still feel a bit like it's a trick but at least it's not a commented test ...
Have a nice day, thanks for your help !

Was this page helpful?
0 / 5 - 0 ratings

Related issues

KerickHowlett picture KerickHowlett  路  38Comments

craig-dae picture craig-dae  路  57Comments

ThomasOliver545 picture ThomasOliver545  路  24Comments

zrilo picture zrilo  路  21Comments

unnamed666 picture unnamed666  路  16Comments