test: add test specs for css() method and fix test errors

This commit is contained in:
bubkoo
2021-04-12 17:21:22 +08:00
parent 1fedd415bf
commit 636a73ccde
5 changed files with 406 additions and 51 deletions

View File

@ -7,7 +7,7 @@ export namespace Hook {
set?: <TElement extends Element>(
elem: TElement,
styleValue: string | number,
) => string | undefined
) => string | number | undefined
}
const hooks: Record<string, Definition> = {}
@ -19,4 +19,8 @@ export namespace Hook {
export function register(styleName: string, hook: Definition) {
hooks[styleName] = hook
}
export function unregister(styleName: string) {
delete hooks[styleName]
}
}

View File

@ -0,0 +1,295 @@
import { Dom } from '../dom'
import { Hook } from './hook'
describe('Dom', () => {
describe('css', () => {
function withCSSContext(css: string, callback: () => void) {
const head = document.head || document.getElementsByTagName('head')[0]
const style = document.createElement('style')
style.setAttribute('type', 'text/css')
head.appendChild(style)
const elem = style as any
if (elem.styleSheet) {
// This is required for IE8 and below.
elem.styleSheet.cssText = css
} else {
style.appendChild(document.createTextNode(css))
}
callback()
style.parentNode?.removeChild(style)
}
describe('css()', () => {
it('should set style with style name-value', () => {
const div = new Dom('div')
div.css('border', '1px')
expect(div.node.getAttribute('style')).toEqual('border: 1px;')
})
it('should set style with style cameCase name-value', () => {
const div = new Dom('div')
div.css('borderLeft', '1px')
expect(div.node.getAttribute('style')).toEqual('border-left: 1px;')
})
it('should auto add unit when set style with style name-value', () => {
const div = new Dom('div')
div.css('border', 1)
expect(div.node.getAttribute('style')).toEqual('border: 1px;')
})
it('should set style with object', () => {
const div = new Dom('div')
div.css({ border: 1, fontSize: 12 })
expect(div.node.getAttribute('style')).toEqual(
'border: 1px; font-size: 12px;',
)
})
it('should get style by styleName', () => {
const div = new Dom('div')
expect(div.css('border')).toEqual('')
div.css({ border: 1, fontSize: 12 })
expect(div.css('border')).toEqual('1px')
expect(div.css('fontSize')).toEqual('12px')
})
it('should get computed style by styleName', () => {
const body = new Dom(document.body)
const div = new Dom('div')
body.css({ fontSize: 18 })
div.appendTo(body)
expect(div.css('fontSize', true)).toEqual('18px')
body.attr('style', null)
div.remove()
})
it('should get computed style by styleNames', () => {
const body = new Dom(document.body)
const div = new Dom('div')
body.css({ fontSize: 18, fontWeight: 500 })
div.appendTo(body)
expect(div.css(['fontSize', 'fontWeight'], true)).toEqual({
fontSize: '18px',
fontWeight: 500,
})
body.attr('style', null)
div.remove()
})
it('should return all inline style', () => {
const div = new Dom('div')
div.css({ border: 1, left: 0 })
expect(div.css()).toEqual({
left: '0px',
border: '1px',
})
})
it('should return style by given style names', () => {
const div = new Dom('div')
div.css({ border: 1, left: 0 })
expect(div.css(['left', 'border'])).toEqual({
left: '0px',
border: '1px',
})
})
it('should return all the computed style', () => {
const div = new Dom('div')
const body = new Dom(document.body)
body.css({ fontSize: 18 })
div.appendTo(body)
expect(div.css(true).fontSize).toEqual('18px')
body.attr('style', null)
div.remove()
})
it('should fallback to get inline style if node is not in the document', () => {
const div = new Dom('div')
div.css({ fontSize: 18 })
expect(div.css('fontSize', true)).toEqual('18px')
})
it('should try to convert style value to number', () => {
const div = new Dom('div')
div.css({ fontWeight: 600 })
expect(div.css('fontWeight', true)).toEqual(600)
expect(div.css('fontWeight')).toEqual(600)
})
it('should set custom style', () => {
const div = new Dom('div')
div.css('--custom-key', '10px')
expect(div.node.getAttribute('style')).toEqual('--custom-key:10px;')
div.attr('style', null)
div.css('--customKey', '10px')
expect(div.node.getAttribute('style')).toEqual('--custom-key:10px;')
})
it('should get custom style', () => {
const div = new Dom('div')
div.css({
fontSize: 18,
'--custom-key': '10px',
})
expect(div.css('--customKey')).toEqual('10px')
expect(div.css('--custom-key')).toEqual('10px')
expect(div.css()).toEqual({
fontSize: '18px',
'--customKey': '10px',
} as any)
})
it('should not set style on text/comment node', () => {
const text = new Dom(document.createTextNode('test') as any)
text.css({ fontSize: 18 })
expect(text.css('fontSize')).toBeUndefined()
expect(text.css()).toEqual({})
expect(text.css(true)).toEqual({})
const comment = new Dom(document.createComment('test') as any)
comment.css({ fontSize: 18 })
expect(comment.css('fontSize')).toBeUndefined()
expect(comment.css()).toEqual({})
expect(comment.css(true)).toEqual({})
})
it('should handle "float" specialy', () => {
const div = new Dom('div').appendTo(document.body)
div.css({ float: 'left' })
expect(div.css('float', true)).toEqual('left')
expect(div.css('float')).toEqual('left')
div.remove()
})
it('should auto add browser prefix to style name', () => {
const div = new Dom('div').appendTo(document.body)
div.css('userDrag', 'none')
expect(div.node.getAttribute('style')).toEqual(
'-webkit-user-drag: none;',
)
expect(div.css('userDrag', true)).toEqual('none')
expect(div.css('userDrag')).toEqual('none')
div.remove()
})
it('should apply hook when get style', () => {
const div = new Dom('div')
div.css('fontSize', 18)
Hook.register('fontSize', {
get(node, computed) {
return computed ? 16 : 14
},
})
expect(div.css('fontSize', true)).toEqual(16)
expect(div.css('fontSize')).toEqual(14)
Hook.unregister('fontSize')
})
it('should apply hook when set style', () => {
const div = new Dom('div')
Hook.register('fontSize', {
set() {
return 20
},
})
div.css('fontSize', 18)
expect(div.css('fontSize')).toEqual('20px')
})
})
describe('show()', () => {
it('should not change the "display" style when it\'s visible', () => {
const div = new Dom('div')
div.show()
expect(div.node.getAttribute('style')).toBeNull()
expect(div.css('display')).toEqual('')
expect(div.visible()).toBeTrue()
})
it('should show the node', () => {
const div = new Dom('div')
div.css('display', 'none')
div.show()
expect(div.node.getAttribute('style')).toEqual('')
expect(div.css('display')).toEqual('')
expect(div.visible()).toBeTrue()
})
it('should recover the old value of "display" style', () => {
const div = new Dom('div')
div.css('display', 'inline-block')
div.hide()
expect(div.css('display')).toEqual('none')
expect(div.visible()).toBeFalse()
div.show()
expect(div.css('display')).toEqual('inline-block')
expect(div.visible()).toBeTrue()
})
it('should force visible when it was hidden in tree', () => {
withCSSContext('.hidden { display: none; }', () => {
const div = new Dom('div').appendTo(document.body)
div.addClass('hidden')
expect(div.visible()).toBeFalse()
div.show()
expect(div.visible()).toBeTrue()
expect(div.css('display')).toEqual('block')
div.remove()
})
})
})
describe('hide()', () => {
it('should hide the node', () => {
const div = new Dom('div')
div.hide()
expect(div.css('display')).toEqual('none')
expect(div.visible()).toBeFalse()
div.show()
expect(div.css('display')).toEqual('')
expect(div.visible()).toBeTrue()
})
})
describe('visible()', () => {
it('should return false when it was hidden in tree', () => {
withCSSContext('.hidden { display: none; }', () => {
const div = new Dom('div').appendTo(document.body)
div.addClass('hidden')
expect(div.visible()).toBeFalse()
div.remove()
})
})
})
describe('toggle()', () => {
it('should toggle visible state of node', () => {
const div = new Dom('div')
div.toggle()
expect(div.css('display')).toEqual('none')
expect(div.visible()).toBeFalse()
div.toggle()
expect(div.css('display')).toEqual('')
expect(div.visible()).toBeTrue()
})
it('should set visible state of node', () => {
const div = new Dom('div')
div.hide()
div.toggle(false)
expect(div.css('display')).toEqual('none')
expect(div.visible()).toBeFalse()
div.toggle(true)
expect(div.css('display')).toEqual('')
expect(div.visible()).toBeTrue()
})
})
})
})

View File

@ -36,8 +36,17 @@ export class Style<TElement extends Element> extends Base<TElement> {
* is `true`, otherwise returns inline style properties.
*/
css(computed?: boolean): CSSProperties
css(styleName: string, computed?: boolean): string | number
css(styleNames: string[], computed?: boolean): Record<string, string | number>
css(styleName: string, styleValue: string | number): this
css(
style?: boolean | CSSPropertyName | CSSPropertyName[] | CSSProperties,
style?:
| boolean
| CSSPropertyName
| CSSPropertyName[]
| CSSProperties
| string
| string[],
value?: string | number | null | boolean,
) {
const node = (this.node as any) as HTMLElement
@ -46,10 +55,13 @@ export class Style<TElement extends Element> extends Base<TElement> {
if (style == null || typeof style === 'boolean') {
if (style) {
const result: CSSProperties = {}
const computedStyle = Util.getComputedStyle(node)
Array.from(computedStyle).forEach((key: MockedCSSName) => {
result[key] = Util.css(node, key, computedStyle)
})
if (Util.isValidNode(node)) {
const computedStyle = Util.getComputedStyle(node)
Array.from(computedStyle).forEach((key) => {
const name = Util.camelCase(key) as MockedCSSName
result[name] = Util.css(node, key, computedStyle)
})
}
return result
}
@ -60,10 +72,12 @@ export class Style<TElement extends Element> extends Base<TElement> {
if (Array.isArray(style)) {
const result: CSSProperties = {}
if (value) {
const computedStyle = Util.getComputedStyle(node)
style.forEach((name: MockedCSSName) => {
result[name] = Util.css(node, name, computedStyle)
})
if (Util.isValidNode(node)) {
const computedStyle = Util.getComputedStyle(node)
style.forEach((name: MockedCSSName) => {
result[name] = Util.css(node, name, computedStyle)
})
}
} else {
style.forEach((name: MockedCSSName) => {
result[name] = Util.style(node, name)
@ -81,14 +95,12 @@ export class Style<TElement extends Element> extends Base<TElement> {
}
// get style for property
if (typeof value == null || typeof value === 'boolean') {
if (value == null || typeof value === 'boolean') {
return value ? Util.css(node, style) : Util.style(node, style)
}
// set style for property
if (typeof value === 'string') {
Util.style(node, style, value)
}
Util.style(node, style, value)
return this
}

View File

@ -1,5 +1,5 @@
import { Global } from '../../global'
import { camelCase, isInDocument } from '../../util'
import { isInDocument } from '../../util'
import { Hook } from './hook'
import { CSSProperties } from './types'
@ -29,6 +29,20 @@ export namespace Util {
return result !== undefined ? `${result}` : result
}
export function isValidNode<TElement extends Element>(node: TElement) {
// Don't set styles on text and comment nodes
if (!node || node.nodeType === 3 || node.nodeType === 8) {
return false
}
const style = ((node as any) as HTMLElement).style
if (!style) {
return false
}
return true
}
export function isCustomStyleName(styleName: string) {
return styleName.indexOf('--') === 0
}
@ -37,8 +51,37 @@ export namespace Util {
// Used by the css & effects modules.
// Support: IE <=9 - 11+
// Microsoft forgot to hump their vendor prefix
export function cssCamelCase(str: string) {
return camelCase(str.replace(/^-ms-/, 'ms-'))
export function camelCase(str: string) {
const to = (s: string) =>
s
.replace(/^-ms-/, 'ms-')
.replace(/-([a-z])/g, (input) => input[1].toUpperCase())
if (isCustomStyleName(str)) {
return `--${to(str.substring(2))}`
}
return to(str)
}
export function kebabCase(str: string) {
return (
str
.replace(/([a-z])([A-Z])/g, '$1-$2')
// vendor
.replace(/^([A-Z])/g, '-$1')
.toLowerCase()
.replace(/^ms-/, '-ms-')
)
}
export function tryConvertToNumber(value: string | number) {
if (typeof value === 'number') {
return value
}
const numReg = /^[+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i
return numReg.test(value) ? +value : value
}
}
@ -150,7 +193,7 @@ export namespace Util {
name: string,
presets?: Record<string, string> | CSSStyleDeclaration,
) {
const styleName = cssCamelCase(name)
const styleName = camelCase(name)
const isCustom = isCustomStyleName(name)
// Make sure that we're working with the right name. We don't
@ -170,10 +213,10 @@ export namespace Util {
// Otherwise, if a way to get the computed value exists, use that
if (val === undefined) {
val = getComputedStyleValue(node, name, presets)
val = getComputedStyleValue(node, kebabCase(name), presets)
}
return val
return tryConvertToNumber(val)
}
}
@ -215,33 +258,26 @@ export namespace Util {
name?: string,
value?: string | number,
) {
// Don't set styles on text and comment nodes
if (!node || node.nodeType === 3 || node.nodeType === 8) {
if (!isValidNode(node)) {
return typeof name === 'undefined' ? {} : undefined
}
const style = ((node as any) as HTMLElement).style
if (!style) {
return typeof name === 'undefined' ? {} : undefined
}
const styleDeclaration = ((node as any) as HTMLElement).style
if (typeof name === 'undefined') {
const result: CSSProperties = {}
style.cssText
styleDeclaration.cssText
.split(/\s*;\s*/)
.filter((str) => str.length > 0)
.forEach((str) => {
const parts = str.split(/\s*:\s*/)
result[cssCamelCase(parts[0]) as MockedCSSName] = Util.style(
node,
parts[0],
)
result[camelCase(parts[0]) as MockedCSSName] = style(node, parts[0])
})
return result
}
// Make sure that we're working with the right name
const styleName = cssCamelCase(name)
const styleName = camelCase(name)
const isCustom = isCustomStyleName(name)
// Make sure that we're working with the right name. We don't
@ -256,7 +292,7 @@ export namespace Util {
// Setting a value
if (value !== undefined) {
let val = normalizeValue(name, value, isCustom)
let val = value
// If a hook was provided, use that value, otherwise just set the specified value
let setting = true
if (hook && hook.set) {
@ -268,27 +304,30 @@ export namespace Util {
}
if (setting) {
val = normalizeValue(name, val, isCustom)
if (name === 'float') {
name = 'cssFloat' // eslint-disable-line
}
if (isCustom) {
style.setProperty(name, val)
styleDeclaration.setProperty(kebabCase(name), val)
} else {
style[name as MockedCSSName] = val
styleDeclaration[name as MockedCSSName] = val
}
}
} else {
let ret: string | number | undefined
// If a hook was provided get the non-computed value from there
if (hook && hook.get) {
const ret = hook.get(node, false)
if (ret !== undefined) {
return ret
}
ret = hook.get(node, false)
}
// Otherwise just get the value from the style object
return style.getPropertyValue(name)
if (ret === undefined) {
ret = styleDeclaration.getPropertyValue(kebabCase(name))
}
return tryConvertToNumber(ret)
}
}
}
@ -316,7 +355,10 @@ export namespace Util {
const doc = node.ownerDocument || Global.document
const temp = doc.body.appendChild(doc.createElement(nodeName))
display = css(temp, 'display') as string
doc.removeChild(temp)
if (temp.parentNode) {
temp.parentNode.removeChild(temp)
}
if (display === 'none') {
display = 'block'
@ -337,23 +379,27 @@ export namespace Util {
}
const display = style.display
let val: string | undefined
if (show) {
// Since we force visibility upon cascade-hidden elements, an immediate (and slow)
// check is required in this first loop unless we have a nonempty display value (either
// inline or about-to-be-restored)
if (display === 'none') {
const value = cache.get(node)
if (!value) {
val = cache.get(node)
if (!val) {
style.display = ''
}
}
// for cascade-hidden
if (style.display === '' && isHiddenWithinTree(node)) {
style.display = getDefaultDisplay(node)
val = getDefaultDisplay(node)
}
} else if (display !== 'none') {
style.display = 'none'
val = 'none'
// Remember what we're overwriting
cache.set(node, display)
}
if (val != null) {
style.display = val
}
}
}

View File

@ -2,7 +2,5 @@ export const ucfirst = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)
export const lcfirst = (s: string) => s.charAt(0).toLowerCase() + s.slice(1)
export function camelCase(str: string) {
return str.replace(/-([a-z])/g, (input, letter: string) =>
letter.toUpperCase(),
)
return str.replace(/-([a-z])/g, (input) => input[1].toUpperCase())
}