Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4089072
ve.test.utils.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
30 KB
Referenced Files
None
Subscribers
None
ve.test.utils.js
View Options
/*!
* VisualEditor test utilities.
*
* @copyright See AUTHORS.txt
*/
(
function
()
{
/**
* @class
* @singleton
* @ignore
*/
ve
.
test
=
{
utils
:
{}
};
let
nextUniqueId
=
1
;
// Create a dummy platform and target so ve.init.platform/target are available
function
DummyPlatform
()
{
DummyPlatform
.
super
.
apply
(
this
,
arguments
);
// Set this to true to test storage methods failing and returning false
this
.
storageDisabled
=
false
;
}
OO
.
inheritClass
(
DummyPlatform
,
ve
.
init
.
Platform
);
DummyPlatform
.
prototype
.
formatNumber
=
function
(
number
)
{
return
number
.
toLocaleString
();
};
DummyPlatform
.
prototype
.
getUserLanguages
=
function
()
{
return
[
'en'
];
};
DummyPlatform
.
prototype
.
getMessage
=
function
()
{
return
Array
.
prototype
.
join
.
call
(
arguments
,
','
);
};
DummyPlatform
.
prototype
.
getHtmlMessage
=
function
(
...
args
)
{
const
$wrapper
=
$
(
'<div>'
);
args
.
forEach
(
(
arg
,
i
)
=>
{
$wrapper
.
append
(
arg
);
if
(
i
<
args
.
length
-
1
)
{
$wrapper
.
append
(
','
);
}
}
);
// Merge text nodes
$wrapper
[
0
].
normalize
();
return
$wrapper
.
contents
().
toArray
();
};
DummyPlatform
.
prototype
.
getLanguageCodes
=
function
()
{
return
[
'ar'
,
'en'
,
'es'
,
'fr'
,
'ru'
,
'zh'
];
};
DummyPlatform
.
prototype
.
getLanguageName
=
function
(
lang
)
{
return
'langname-'
+
lang
;
};
DummyPlatform
.
prototype
.
getLanguageDirection
=
function
()
{
return
'ltr'
;
};
DummyPlatform
.
prototype
.
getExternalLinkUrlProtocolsRegExp
=
function
()
{
return
/^https?:\/\//i
;
};
DummyPlatform
.
prototype
.
getUnanchoredExternalLinkUrlProtocolsRegExp
=
function
()
{
return
/https?:\/\//i
;
};
DummyPlatform
.
prototype
.
generateUniqueId
=
function
()
{
return
'test'
+
(
nextUniqueId
++
);
};
DummyPlatform
.
prototype
.
getUserConfig
=
function
()
{
return
undefined
;
};
DummyPlatform
.
prototype
.
setUserConfig
=
function
()
{};
DummyPlatform
.
prototype
.
createSafeStorage
=
function
(
store
)
{
const
platform
=
this
;
const
EXPIRY_PREFIX
=
'_EXPIRY_'
;
const
MockSafeStorage
=
function
(
s
)
{
this
.
store
=
s
;
};
OO
.
initClass
(
MockSafeStorage
);
MockSafeStorage
.
prototype
.
get
=
function
(
key
)
{
if
(
platform
.
storageDisabled
)
{
return
false
;
}
return
this
.
store
.
getItem
(
key
);
};
MockSafeStorage
.
prototype
.
set
=
function
(
key
,
value
,
expiry
)
{
if
(
platform
.
storageDisabled
||
value
===
'__FAIL__'
)
{
return
false
;
}
this
.
store
.
setItem
(
key
,
value
);
this
.
setExpires
(
key
,
expiry
);
return
true
;
};
MockSafeStorage
.
prototype
.
remove
=
function
(
key
)
{
if
(
platform
.
storageDisabled
)
{
return
false
;
}
this
.
store
.
removeItem
(
key
);
this
.
setExpires
(
key
);
return
true
;
};
MockSafeStorage
.
prototype
.
getObject
=
function
(
key
)
{
const
json
=
this
.
get
(
key
);
if
(
json
===
false
)
{
return
false
;
}
try
{
return
JSON
.
parse
(
json
);
}
catch
(
e
)
{}
return
null
;
};
MockSafeStorage
.
prototype
.
setObject
=
function
(
key
,
value
)
{
try
{
const
json
=
JSON
.
stringify
(
value
);
return
this
.
set
(
key
,
json
);
}
catch
(
e
)
{}
return
false
;
};
MockSafeStorage
.
prototype
.
setExpires
=
function
(
key
,
expiry
)
{
if
(
platform
.
storageDisabled
)
{
return
false
;
}
if
(
expiry
)
{
this
.
store
.
setItem
(
EXPIRY_PREFIX
+
key
,
Math
.
floor
(
Date
.
now
()
/
1000
)
+
expiry
);
}
else
{
this
.
store
.
removeItem
(
EXPIRY_PREFIX
+
key
);
}
};
return
new
MockSafeStorage
(
store
);
};
DummyPlatform
.
prototype
.
createLocalStorage
=
DummyPlatform
.
prototype
.
createSessionStorage
=
function
(
store
=
{}
)
{
const
MockSystemStorage
=
function
()
{};
OO
.
initClass
(
MockSystemStorage
);
MockSystemStorage
.
prototype
.
getItem
=
function
(
key
)
{
return
Object
.
prototype
.
hasOwnProperty
.
call
(
store
,
key
)
?
store
[
key
]
:
null
;
};
MockSystemStorage
.
prototype
.
setItem
=
function
(
key
,
value
)
{
store
[
key
]
=
String
(
value
);
};
MockSystemStorage
.
prototype
.
removeItem
=
function
(
key
)
{
delete
store
[
key
];
};
MockSystemStorage
.
prototype
.
clear
=
function
()
{
for
(
const
key
in
store
)
{
delete
store
[
key
];
}
};
MockSystemStorage
.
prototype
.
key
=
function
(
index
)
{
const
keys
=
Object
.
keys
(
store
);
return
index
<
keys
.
length
?
keys
[
index
]
:
null
;
};
if
(
!
Object
.
prototype
.
hasOwnProperty
.
call
(
store
,
'length'
)
)
{
Object
.
defineProperty
(
store
,
'length'
,
{
get
:
()
=>
Object
.
keys
(
store
).
length
}
);
}
const
storage
=
this
.
createSafeStorage
(
new
MockSystemStorage
(
store
)
);
return
ve
.
init
.
createConflictableStorage
(
storage
);
};
ve
.
test
.
utils
.
DummyPlatform
=
DummyPlatform
;
function
DummyTarget
()
{
DummyTarget
.
super
.
apply
(
this
,
arguments
);
}
OO
.
inheritClass
(
DummyTarget
,
ve
.
init
.
Target
);
DummyTarget
.
prototype
.
addSurface
=
function
()
{
// Parent method
const
surface
=
DummyTarget
.
super
.
prototype
.
addSurface
.
apply
(
this
,
arguments
);
this
.
$element
.
append
(
surface
.
$element
);
if
(
!
this
.
getSurface
()
)
{
this
.
setSurface
(
surface
);
}
surface
.
initialize
();
return
surface
;
};
DummyTarget
.
prototype
.
setupToolbar
=
function
()
{};
ve
.
test
.
utils
.
DummyTarget
=
DummyTarget
;
/* eslint-disable no-new */
new
ve
.
test
.
utils
.
DummyPlatform
();
new
ve
.
test
.
utils
.
DummyTarget
();
// Target gets appended to qunit-fixture in ve.qunit.local.js
/* eslint-enable no-new */
function
ImageTransferHandler
()
{
// Parent constructor
ImageTransferHandler
.
super
.
apply
(
this
,
arguments
);
}
OO
.
inheritClass
(
ImageTransferHandler
,
ve
.
ui
.
DataTransferHandler
);
ImageTransferHandler
.
static
.
name
=
'imageTest'
;
ImageTransferHandler
.
static
.
kinds
=
[
'file'
];
ImageTransferHandler
.
static
.
types
=
[
'image/jpeg'
,
'image/gif'
];
ImageTransferHandler
.
prototype
.
process
=
function
()
{
const
file
=
this
.
item
.
getAsFile
();
const
text
=
file
.
name
||
(
file
.
type
+
':'
+
file
.
size
);
this
.
insertableDataDeferred
.
resolve
(
text
.
split
(
''
)
);
};
ve
.
test
.
utils
.
ImageTransferHandler
=
ImageTransferHandler
;
/**
* Creates a simulated jQuery Event
*
* @param {string|Object} src Event type, or original event object
* @param {Object} props jQuery event properties
* @return {jQuery.Event}
*/
ve
.
test
.
utils
.
createTestEvent
=
function
TestEvent
(
src
,
props
)
{
if
(
props
&&
!
(
'which'
in
props
)
)
{
props
.
which
=
props
.
keyCode
;
}
const
event
=
$
.
Event
(
src
,
props
);
event
.
isSimulated
=
true
;
return
event
;
};
// Disable scroll animatinos
ve
.
scrollIntoView
=
function
()
{
return
ve
.
createDeferred
().
resolve
().
promise
();
};
const
voidGroup
=
'('
+
ve
.
elementTypes
.
void
.
join
(
'|'
)
+
')'
;
const
voidRegexp
=
new
RegExp
(
'(<'
+
voidGroup
+
'[^>]*?(/?))>'
,
'g'
);
const
originalCreateDocumentFromHtml
=
ve
.
createDocumentFromHtml
;
/**
* Wraps ve.createDocumentFromHtml to validate HTML structure using an XML parser.
*
* Some automatic fixes are applied to HTML to make it parseable,
* but some errors will still be thrown, for example:
* - Unescaped < or > in attributes
*
* If your test HTML is triggering a false positive warning, you can
* disable this check with the ignoreXmlWarnings param.
*
* @param {string} html
* @param {boolean} ignoreXmlWarnings Skip validation
* @return {HTMLDocument}
*/
ve
.
test
.
utils
.
createDocumentFromHtml
=
function
(
html
,
ignoreXmlWarnings
)
{
if
(
html
&&
!
ignoreXmlWarnings
)
{
const
xml
=
'<xml>'
+
html
// Close open void tags
.
replace
(
voidRegexp
,
(
...
args
)
=>
args
[
3
]
?
// self-closing - do nothing
args
[
0
]
:
// add close tag
args
[
0
]
+
'</'
+
args
[
2
]
+
'>'
)
// Remove entities, named ones not recognised
.
replace
(
/&[^;]+;/g
,
''
)
// Remove doctype
.
replace
(
/<!doctype html>/i
,
''
)
+
'</xml>'
;
let
xmlDoc
;
// Firefox additionally throws an error
try
{
xmlDoc
=
(
new
DOMParser
()
).
parseFromString
(
xml
,
'application/xml'
);
}
catch
(
e
)
{
}
if
(
xmlDoc
)
{
const
parserError
=
xmlDoc
.
querySelector
(
'parsererror'
);
if
(
parserError
)
{
// eslint-disable-next-line no-console
console
.
warn
(
parserError
.
innerText
,
'\n'
,
html
,
'\n'
,
xml
);
}
}
}
return
originalCreateDocumentFromHtml
(
html
);
};
/**
* Override ve.createDocumentFromHtml to validate HTML structure using an XML parser
* when the original function is called, then restore it when it returns.
*
* @param {Function} originalFunc
* @return {Function}
*/
function
withCreateDocumentFromHtmlValidation
(
originalFunc
)
{
return
function
(
...
params
)
{
ve
.
createDocumentFromHtml
=
ve
.
test
.
utils
.
createDocumentFromHtml
;
try
{
return
originalFunc
(
...
params
);
}
finally
{
ve
.
createDocumentFromHtml
=
originalCreateDocumentFromHtml
;
}
};
}
function
getSerializableData
(
model
)
{
return
model
.
getFullData
(
undefined
,
'roundTrip'
);
}
/**
* Make a HTML `<base>` tag
*
* @param {string} base Base URL
* @return {string}
*/
ve
.
test
.
utils
.
makeBaseTag
=
function
(
base
)
{
if
(
/[<>&'"]/
.
test
(
base
)
)
{
throw
new
Error
(
'Weird base'
);
}
return
'<base href="'
+
base
+
'">'
;
};
/**
* Add a `<base>` tag to `<head>` using HTML string splicing
*
* @param {string} docHtml Document HTML
* @param {string} base Base URL
* @return {string} Document HTML
*/
ve
.
test
.
utils
.
addBaseTag
=
function
(
docHtml
,
base
)
{
if
(
!
ve
.
matchTag
(
docHtml
,
'body'
)
)
{
docHtml
=
'<body>'
+
docHtml
+
'</body>'
;
}
return
ve
.
addHeadTag
(
docHtml
,
ve
.
test
.
utils
.
makeBaseTag
(
base
)
);
};
/**
* @param {QUnit.Assert} assert
* @param {Object} caseItem
* @param {string} caseItem.type
* @param {ve.Range} caseItem.range
* @param {Function} caseItem.expected
* @param {string} caseItem.base
* @param {string} caseItem.msg
*/
ve
.
test
.
utils
.
runIsolateTest
=
function
(
assert
,
caseItem
)
{
if
(
arguments
.
length
>
2
)
{
caseItem
=
{
type
:
arguments
[
1
],
range
:
arguments
[
2
],
expected
:
arguments
[
3
],
base
:
arguments
[
4
],
msg
:
arguments
[
5
]
};
}
const
doc
=
ve
.
dm
.
example
.
createExampleDocument
(
'isolationData'
,
undefined
,
caseItem
.
base
),
surface
=
new
ve
.
dm
.
Surface
(
doc
),
fragment
=
surface
.
getLinearFragment
(
caseItem
.
range
);
const
data
=
ve
.
copy
(
getSerializableData
(
doc
)
);
fragment
.
isolateAndUnwrap
(
caseItem
.
type
);
caseItem
.
expected
(
data
);
assert
.
deepEqual
(
getSerializableData
(
doc
),
data
,
caseItem
.
msg
);
};
/**
* @param {QUnit.Assert} assert
* @param {Object} caseItem
* @param {string} caseItem.msg
* @param {string} caseItem.actionName
* @param {string} caseItem.method
* @param {string} caseItem.html
* @param {Array} [caseItem.args] Arguments to pass to the action
* @param {ve.Range|Object|string} caseItem.rangeOrSelection
* @param {boolean} [caseItem.createView] Create a view surface, instead of just a model surface (slower)
* @param {ve.Range|Object|string} [caseItem.expectedRangeOrSelection]
* @param {ve.Range|Object|string} [caseItem.expectedOriginalRangeOrSelection]
* @param {boolean} [caseItem.undo] Test that the action undoes cleanly
* @param {Function} [caseItem.expectedData] Function to mutate linear data into expected data
* @param {Function} [caseItem.expectedOriginalData] Function to mutate linear data into expected data after undo
*/
ve
.
test
.
utils
.
runActionTest
=
function
(
assert
,
caseItem
)
{
if
(
arguments
.
length
>
2
)
{
const
args
=
Array
.
prototype
.
slice
.
call
(
arguments
);
const
options
=
args
[
8
]
||
{};
assert
=
args
[
1
];
caseItem
=
{
actionName
:
args
[
0
],
html
:
args
[
2
],
createView
:
args
[
3
],
method
:
args
[
4
],
args
:
args
[
5
],
rangeOrSelection
:
args
[
6
],
msg
:
args
[
7
],
...
options
};
}
const
surface
=
caseItem
.
createView
?
ve
.
test
.
utils
.
createViewOnlySurfaceFromHtml
(
caseItem
.
html
||
ve
.
dm
.
example
.
html
)
:
ve
.
test
.
utils
.
createModelOnlySurfaceFromHtml
(
caseItem
.
html
||
ve
.
dm
.
example
.
html
),
action
=
ve
.
ui
.
actionFactory
.
create
(
caseItem
.
actionName
,
surface
),
data
=
ve
.
copy
(
getSerializableData
(
surface
.
getModel
().
getDocument
()
)
),
documentModel
=
surface
.
getModel
().
getDocument
(),
selection
=
ve
.
test
.
utils
.
selectionFromRangeOrSelection
(
documentModel
,
caseItem
.
rangeOrSelection
),
expectedSelection
=
caseItem
.
expectedRangeOrSelection
&&
ve
.
test
.
utils
.
selectionFromRangeOrSelection
(
documentModel
,
caseItem
.
expectedRangeOrSelection
);
let
originalData
;
if
(
caseItem
.
undo
)
{
originalData
=
ve
.
copy
(
data
);
}
ve
.
dm
.
example
.
postprocessAnnotations
(
data
,
surface
.
getModel
().
getDocument
().
getStore
()
);
if
(
caseItem
.
expectedData
)
{
caseItem
.
expectedData
(
data
,
action
);
}
surface
.
getModel
().
setSelection
(
selection
);
if
(
caseItem
.
insertionAnnotations
)
{
caseItem
.
insertionAnnotations
.
forEach
(
(
annotation
)
=>
{
surface
.
getModel
().
addInsertionAnnotations
(
ve
.
dm
.
annotationFactory
.
create
(
annotation
.
type
,
annotation
.
data
)
);
}
);
}
action
[
caseItem
.
method
](
...(
caseItem
.
args
||
[]
)
);
const
afterApply
=
()
=>
{
const
actualData
=
getSerializableData
(
surface
.
getModel
().
getDocument
()
);
ve
.
dm
.
example
.
postprocessAnnotations
(
actualData
,
surface
.
getModel
().
getDocument
().
getStore
()
);
assert
.
equalLinearData
(
actualData
,
data
,
caseItem
.
msg
+
': data models match'
);
if
(
expectedSelection
)
{
assert
.
equalHash
(
surface
.
getModel
().
getSelection
(),
expectedSelection
,
caseItem
.
msg
+
': selections match'
);
}
if
(
caseItem
.
expectedInsertionAnnotations
)
{
const
actualAnnotations
=
surface
.
getModel
().
getInsertionAnnotations
().
get
();
assert
.
deepEqual
(
actualAnnotations
.
map
(
(
annotation
)
=>
annotation
.
getComparableObject
()
),
caseItem
.
expectedInsertionAnnotations
,
'Insertion annotations match expected state'
);
}
if
(
caseItem
.
undo
)
{
if
(
caseItem
.
expectedOriginalData
)
{
caseItem
.
expectedOriginalData
(
originalData
,
action
);
}
surface
.
getModel
().
undo
();
assert
.
equalLinearData
(
getSerializableData
(
surface
.
getModel
().
getDocument
()
),
originalData
,
caseItem
.
msg
+
' (undo): data models match'
);
if
(
expectedSelection
)
{
const
expectedOriginalRangeOrSelection
=
caseItem
.
expectedOriginalRangeOrSelection
&&
ve
.
test
.
utils
.
selectionFromRangeOrSelection
(
documentModel
,
caseItem
.
expectedOriginalRangeOrSelection
);
assert
.
equalHash
(
surface
.
getModel
().
getSelection
(),
expectedOriginalRangeOrSelection
||
selection
,
caseItem
.
msg
+
' (undo): selections match'
);
}
}
};
if
(
caseItem
.
createView
)
{
// When rendering a view, wait for MutationObserver events to fire
// before checking document state.
const
done
=
assert
.
async
();
setTimeout
(
()
=>
{
afterApply
();
done
();
}
);
}
else
{
// In model-only mode test synchronously
afterApply
();
}
};
ve
.
test
.
utils
.
runGetModelFromDomTest
=
function
(
assert
,
caseItem
,
msg
)
{
if
(
caseItem
.
head
!==
undefined
||
caseItem
.
body
!==
undefined
)
{
const
html
=
'<head>'
+
(
caseItem
.
head
||
''
)
+
'</head><body>'
+
caseItem
.
body
+
'</body>'
;
const
htmlDoc
=
ve
.
createDocumentFromHtml
(
ve
.
test
.
utils
.
addBaseTag
(
html
,
caseItem
.
base
),
caseItem
.
ignoreXmlWarnings
);
const
model
=
ve
.
dm
.
converter
.
getModelFromDom
(
htmlDoc
,
{
fromClipboard
:
!!
caseItem
.
fromClipboard
}
);
let
actualDataReal
=
model
.
getFullData
();
let
actualDataMeta
=
getSerializableData
(
model
);
// Round-trip here, check round-trip later
if
(
caseItem
.
modify
)
{
actualDataReal
=
ve
.
copy
(
actualDataReal
);
actualDataMeta
=
ve
.
copy
(
actualDataMeta
);
caseItem
.
modify
(
model
);
}
const
actualRtDoc
=
ve
.
dm
.
converter
.
getDomFromModel
(
model
);
// Normalize and verify data
ve
.
dm
.
example
.
postprocessAnnotations
(
actualDataReal
,
model
.
getStore
(),
caseItem
.
preserveAnnotationDomElements
);
ve
.
dm
.
example
.
postprocessAnnotations
(
actualDataMeta
,
model
.
getStore
(),
caseItem
.
preserveAnnotationDomElements
);
assert
.
equalLinearData
(
actualDataReal
,
(
caseItem
.
realData
||
caseItem
.
data
),
msg
+
': real data'
);
assert
.
equalLinearData
(
actualDataMeta
,
caseItem
.
data
,
msg
+
': data with metaItems restored'
);
assert
.
deepEqual
(
model
.
getInnerWhitespace
(),
caseItem
.
innerWhitespace
||
new
Array
(
2
),
msg
+
': inner whitespace'
);
// Check storeItems have been added to store
if
(
caseItem
.
storeItems
)
{
for
(
const
hash
in
caseItem
.
storeItems
)
{
assert
.
deepEqualWithDomElements
(
model
.
getStore
().
value
(
hash
)
||
{},
caseItem
.
storeItems
[
hash
],
msg
+
': store item '
+
hash
+
' found'
);
}
}
// Check round-trip
const
expectedRtDoc
=
caseItem
.
normalizedBody
?
ve
.
createDocumentFromHtml
(
ve
.
test
.
utils
.
addBaseTag
(
caseItem
.
normalizedBody
,
caseItem
.
base
),
caseItem
.
ignoreXmlWarnings
)
:
htmlDoc
;
assert
.
equalDomElement
(
actualRtDoc
.
body
,
expectedRtDoc
.
body
,
msg
+
': round-trip'
);
}
};
ve
.
test
.
utils
.
runGetModelFromDomTest
=
withCreateDocumentFromHtmlValidation
(
ve
.
test
.
utils
.
runGetModelFromDomTest
);
ve
.
test
.
utils
.
getModelFromTestCase
=
function
(
caseItem
)
{
const
store
=
new
ve
.
dm
.
HashValueStore
();
// Load storeItems into store
if
(
caseItem
.
storeItems
)
{
for
(
const
hash
in
caseItem
.
storeItems
)
{
store
.
hashStore
[
hash
]
=
ve
.
copy
(
caseItem
.
storeItems
[
hash
]
);
}
}
const
model
=
new
ve
.
dm
.
Document
(
ve
.
dm
.
example
.
preprocessAnnotations
(
caseItem
.
data
,
store
)
);
ve
.
fixBase
(
model
.
getHtmlDocument
(),
model
.
getHtmlDocument
(),
caseItem
.
base
);
model
.
innerWhitespace
=
caseItem
.
innerWhitespace
?
ve
.
copy
(
caseItem
.
innerWhitespace
)
:
new
Array
(
2
);
if
(
caseItem
.
modify
)
{
caseItem
.
modify
(
model
);
}
return
model
;
};
ve
.
test
.
utils
.
runGetDomFromModelTest
=
function
(
assert
,
caseItem
,
msg
)
{
const
model
=
ve
.
test
.
utils
.
getModelFromTestCase
(
caseItem
);
const
originalData
=
ve
.
copy
(
getSerializableData
(
model
)
);
const
fromDataBody
=
caseItem
.
fromDataBody
||
caseItem
.
normalizedBody
||
caseItem
.
body
;
const
html
=
'<body>'
+
fromDataBody
+
'</body>'
;
const
clipboardHtml
=
'<body>'
+
(
caseItem
.
clipboardBody
||
fromDataBody
)
+
'</body>'
;
const
previewHtml
=
'<body>'
+
(
caseItem
.
previewBody
||
fromDataBody
)
+
'</body>'
;
assert
.
equalDomElement
(
ve
.
dm
.
converter
.
getDomFromModel
(
model
),
ve
.
createDocumentFromHtml
(
html
,
caseItem
.
ignoreXmlWarnings
),
msg
);
assert
.
equalDomElement
(
ve
.
dm
.
converter
.
getDomFromModel
(
model
,
ve
.
dm
.
Converter
.
static
.
CLIPBOARD_MODE
),
ve
.
createDocumentFromHtml
(
clipboardHtml
,
caseItem
.
ignoreXmlWarnings
),
msg
+
' (clipboard mode)'
);
// Make this conditional on previewBody being present until downstream test-suites have been fixed.
// This should be changed to:
// if ( caseItem.previewBody !== false ) {
if
(
caseItem
.
previewBody
)
{
assert
.
equalDomElement
(
ve
.
dm
.
converter
.
getDomFromModel
(
model
,
ve
.
dm
.
Converter
.
static
.
PREVIEW_MODE
),
ve
.
createDocumentFromHtml
(
previewHtml
,
caseItem
.
ignoreXmlWarnings
),
msg
+
' (preview mode)'
);
}
assert
.
deepEqualWithDomElements
(
getSerializableData
(
model
),
originalData
,
msg
+
' (data hasn\'t changed)'
);
};
ve
.
test
.
utils
.
runGetDomFromModelTest
=
withCreateDocumentFromHtmlValidation
(
ve
.
test
.
utils
.
runGetDomFromModelTest
);
ve
.
test
.
utils
.
runDiffElementTest
=
function
(
assert
,
caseItem
)
{
const
oldDoc
=
ve
.
dm
.
converter
.
getModelFromDom
(
ve
.
createDocumentFromHtml
(
caseItem
.
oldDoc
)
),
newDoc
=
ve
.
dm
.
converter
.
getModelFromDom
(
ve
.
createDocumentFromHtml
(
caseItem
.
newDoc
)
);
// TODO: Differ expects newDoc to be derived from oldDoc and contain all its store data.
// We may want to remove that assumption from the differ?
newDoc
.
getStore
().
merge
(
oldDoc
.
getStore
()
);
const
visualDiff
=
new
ve
.
dm
.
VisualDiff
(
oldDoc
,
newDoc
,
caseItem
.
forceTimeout
?
-
1
:
undefined
);
const
diffElement
=
new
ve
.
ui
.
DiffElement
(
visualDiff
);
assert
.
equalDomElement
(
diffElement
.
$document
[
0
],
$
(
'<div>'
).
addClass
(
've-ui-diffElement-document'
).
html
(
caseItem
.
expected
)[
0
],
caseItem
.
msg
);
assert
.
deepEqualWithDomElements
(
diffElement
.
descriptions
.
items
.
map
(
(
item
)
=>
item
.
$label
.
contents
().
toArray
()
),
(
caseItem
.
expectedDescriptions
||
[]
).
map
(
(
expected
)
=>
$
.
parseHTML
(
expected
)
),
caseItem
.
msg
+
': sidebar'
);
assert
.
strictEqual
(
diffElement
.
$messages
.
children
().
length
,
caseItem
.
forceTimeout
?
1
:
0
,
'Timeout message '
+
(
caseItem
.
forceTimeout
?
'shown'
:
'not shown'
)
);
};
ve
.
test
.
utils
.
runDiffElementTest
=
withCreateDocumentFromHtmlValidation
(
ve
.
test
.
utils
.
runDiffElementTest
);
/**
* Create a UI surface from some HTML
*
* This is incredibly slow (>100ms) so consider creating just a ce.Surface
* or dm.Surface, or a mock surface using create(View|Model)OnlySurface*.
*
* @param {string} html
* @param {Object} config Surface config
* @return {ve.ui.Surface}
*/
ve
.
test
.
utils
.
createSurfaceFromHtml
=
function
(
html
,
config
=
{}
)
{
return
this
.
createSurfaceFromDocument
(
ve
.
dm
.
converter
.
getModelFromDom
(
ve
.
test
.
utils
.
createDocumentFromHtml
(
html
)
),
config
);
};
/**
* Create a UI surface from a document
*
* See warning in ve.test.utils.createSurfaceFromHtml.
*
* @param {ve.dm.Document} doc
* @param {Object} [config] Surface config
* @return {ve.ui.Surface}
*/
ve
.
test
.
utils
.
createSurfaceFromDocument
=
function
(
doc
,
config
=
{}
)
{
return
ve
.
init
.
target
.
addSurface
(
doc
,
config
);
};
/**
* Create a CE surface from some HTML
*
* @param {string} html
* @param {Object} config Surface config
* @return {ve.ce.Surface}
*/
ve
.
test
.
utils
.
createSurfaceViewFromHtml
=
function
(
html
,
config
=
{}
)
{
return
this
.
createSurfaceViewFromDocument
(
ve
.
dm
.
converter
.
getModelFromDom
(
ve
.
test
.
utils
.
createDocumentFromHtml
(
html
)
),
config
);
};
/**
* Create a CE surface from a document or surface model
*
* TODO: Rename to createSurfaceViewFromModel
*
* @param {ve.dm.Document|ve.dm.Surface} docOrSurface Document or surface model
* @param {Object} config Surface config
* @return {ve.ce.Surface}
*/
ve
.
test
.
utils
.
createSurfaceViewFromDocument
=
function
(
docOrSurface
,
config
=
{}
)
{
config
=
ve
.
init
.
target
.
getSurfaceConfig
(
config
);
let
model
=
null
,
view
=
null
;
const
mockSurface
=
{
$blockers
:
$
(
'<div>'
),
$selections
:
$
(
'<div>'
),
$element
:
$
(
'<div>'
),
$scrollContainer
:
$
(
document
.
documentElement
),
$scrollListener
:
$
(
window
),
isMobile
:
function
()
{
return
false
;
},
isMultiline
:
function
()
{
return
true
;
},
isReadOnly
:
function
()
{
return
false
;
},
getBoundingClientRect
:
function
()
{
return
this
.
$element
[
0
].
getClientRects
()[
0
]
||
null
;
},
getViewportDimensions
:
function
()
{
return
null
;
},
getImportRules
:
function
()
{
return
this
.
importRules
;
},
getMode
:
function
()
{
return
config
.
mode
||
'visual'
;
},
getModel
:
function
()
{
return
model
;
},
getView
:
function
()
{
return
view
;
},
getTarget
:
function
()
{
return
ve
.
init
.
target
;
},
getCommands
:
function
()
{
return
ve
.
ui
.
commandRegistry
.
getNames
();
},
getContext
:
function
()
{
return
{
toggle
:
function
()
{},
updateDimensions
:
function
()
{}
};
},
isDisabled
:
function
()
{
return
false
;
},
doesAllowTabFocusChange
:
function
()
{
return
!!
config
.
allowTabFocusChange
;
},
emit
:
function
()
{},
connect
:
function
()
{},
disconnect
:
function
()
{},
execute
:
ve
.
ui
.
Surface
.
prototype
.
execute
,
executeWithSource
:
ve
.
ui
.
Surface
.
prototype
.
executeWithSource
,
createView
:
ve
.
ui
.
Surface
.
prototype
.
createView
,
createModel
:
ve
.
ui
.
Surface
.
prototype
.
createModel
,
context
:
{
// Used by ContentAction
isVisible
:
function
()
{
return
false
;
}
}
};
// Copied from ui.Surface constructor
mockSurface
.
commandRegistry
=
config
.
commandRegistry
||
ve
.
ui
.
commandRegistry
;
mockSurface
.
sequenceRegistry
=
config
.
sequenceRegistry
||
ve
.
ui
.
sequenceRegistry
;
mockSurface
.
dataTransferHandlerFactory
=
config
.
dataTransferHandlerFactory
||
ve
.
ui
.
dataTransferHandlerFactory
;
mockSurface
.
commands
=
OO
.
simpleArrayDifference
(
config
.
includeCommands
||
mockSurface
.
commandRegistry
.
getNames
(),
config
.
excludeCommands
||
[]
);
mockSurface
.
triggerListener
=
new
ve
.
TriggerListener
(
mockSurface
.
commands
,
mockSurface
.
commandRegistry
);
mockSurface
.
importRules
=
config
.
importRules
||
{};
model
=
docOrSurface
instanceof
ve
.
dm
.
Surface
?
docOrSurface
:
mockSurface
.
createModel
(
docOrSurface
);
view
=
mockSurface
.
createView
(
model
);
view
.
surface
=
mockSurface
;
mockSurface
.
$element
.
append
(
view
.
$element
);
const
$focusHolder
=
$
(
'<div>'
).
prop
(
'tabIndex'
,
0
).
addClass
(
've-ui-testFocusHolder'
);
// eslint-disable-next-line no-jquery/no-global-selector
$
(
'#qunit-fixture'
).
append
(
mockSurface
.
$element
,
$focusHolder
);
view
.
initialize
();
model
.
initialize
();
return
view
;
};
/**
* Create a view-only UI surface from some HTML
*
* @param {string} html Document HTML
* @param {Object} config Surface config
* @return {Object} Mock UI surface which only returns a real view (and its model)
*/
ve
.
test
.
utils
.
createViewOnlySurfaceFromHtml
=
function
(
html
,
config
=
{}
)
{
const
surfaceView
=
ve
.
test
.
utils
.
createSurfaceViewFromDocument
(
ve
.
dm
.
converter
.
getModelFromDom
(
ve
.
test
.
utils
.
createDocumentFromHtml
(
html
)
),
config
);
return
surfaceView
.
surface
;
};
/**
* Create a model-only UI surface from some HTML
*
* @param {string} html Document HTML
* @param {Object} config Surface config
* @return {Object} Mock UI surface which only returns a real model
*/
ve
.
test
.
utils
.
createModelOnlySurfaceFromHtml
=
function
(
html
,
config
=
{}
)
{
const
model
=
new
ve
.
dm
.
Surface
(
ve
.
dm
.
converter
.
getModelFromDom
(
ve
.
test
.
utils
.
createDocumentFromHtml
(
html
)
),
null
,
config
);
return
{
getModel
:
function
()
{
return
model
;
},
getView
:
function
()
{
// Mock view
return
{
focus
:
function
()
{}
};
}
};
};
/**
* Create a DM selection from a range or a JSON selection
*
* @param {ve.dm.Document} doc
* @param {ve.Range|Object|string} rangeOrSelection Range or JSON selection
* @return {ve.dm.Selection}
*/
ve
.
test
.
utils
.
selectionFromRangeOrSelection
=
function
(
doc
,
rangeOrSelection
)
{
return
rangeOrSelection
instanceof
ve
.
Range
?
new
ve
.
dm
.
LinearSelection
(
rangeOrSelection
)
:
ve
.
dm
.
Selection
.
static
.
newFromJSON
(
rangeOrSelection
);
};
/**
* Build a DOM from a JSON structure.
*
* @param {Object} data JSON structure
* @param {string} data.type Tag name or '#text' or '#comment'
* @param {string} [data.text] Node text, only used if type is '#text' or '#comment'
* @param {Object[]} [data.children] Node's children; array of objects like data
* @return {Node} DOM node corresponding to data
*/
ve
.
test
.
utils
.
buildDom
=
function
buildDom
(
data
)
{
if
(
data
.
type
===
'#text'
)
{
return
document
.
createTextNode
(
data
.
text
);
}
if
(
data
.
type
===
'#comment'
)
{
return
document
.
createComment
(
data
.
text
);
}
const
node
=
document
.
createElement
(
data
.
type
);
if
(
data
.
children
)
{
for
(
let
i
=
0
;
i
<
data
.
children
.
length
;
i
++
)
{
node
.
appendChild
(
buildDom
(
data
.
children
[
i
]
)
);
}
}
return
node
;
};
/**
* Like a reduced outerHTML serialization, but with a position marker '|'.
*
* For clarity, also wraps each text node in a fake tag, and omits non-class attributes.
*
* @param {Node} rootNode The node to serialize
* @param {Object} position
* @param {Node} position.node The node at which the position marker lies
* @param {number} position.offset The offset at which the position marker lies
* @param {Object} [options]
* @param {Function|string} options.ignore Selector for nodes to omit from output
* @return {string} Serialization of the node and position
*/
ve
.
test
.
utils
.
serializePosition
=
function
(
rootNode
,
position
,
options
)
{
const
html
=
[];
function
add
(
node
)
{
if
(
options
&&
options
.
ignore
&&
$
(
node
).
is
(
options
.
ignore
)
)
{
return
;
}
else
if
(
node
.
nodeType
===
Node
.
TEXT_NODE
)
{
html
.
push
(
'<#text>'
);
if
(
node
===
position
.
node
)
{
html
.
push
(
ve
.
escapeHtml
(
node
.
textContent
.
slice
(
0
,
position
.
offset
)
+
'|'
+
node
.
textContent
.
slice
(
position
.
offset
)
)
);
}
else
{
html
.
push
(
ve
.
escapeHtml
(
node
.
textContent
)
);
}
html
.
push
(
'</#text>'
);
return
;
}
else
if
(
node
.
nodeType
!==
Node
.
ELEMENT_NODE
)
{
html
.
push
(
'<#unknown type=\''
+
node
.
nodeType
+
'\'/>'
);
return
;
}
// else node.nodeType === Node.ELEMENT_NODE
html
.
push
(
'<'
,
ve
.
escapeHtml
(
node
.
nodeName
.
toLowerCase
()
)
);
if
(
node
.
hasAttribute
(
'class'
)
)
{
// Single quotes are less annoying for JSON escaping
html
.
push
(
' class=\''
,
ve
.
escapeHtml
(
node
.
getAttribute
(
'class'
)
),
'\''
);
}
html
.
push
(
'>'
);
for
(
let
i
=
0
,
len
=
node
.
childNodes
.
length
;
i
<
len
;
i
++
)
{
if
(
node
===
position
.
node
&&
i
===
position
.
offset
)
{
html
.
push
(
'|'
);
}
add
(
node
.
childNodes
[
i
]
);
}
if
(
node
===
position
.
node
&&
node
.
childNodes
.
length
===
position
.
offset
)
{
html
.
push
(
'|'
);
}
html
.
push
(
'</'
,
ve
.
escapeHtml
(
node
.
nodeName
.
toLowerCase
()
),
'>'
);
}
add
(
rootNode
);
return
html
.
join
(
''
);
};
/**
* Take control of EventSequencer timeouts
*
* Modifies an EventSequencer object in-place to allow setTimeout behaviour to be
* simulated by test code. Replaces `postpone` and `cancelPostponed` with methods that
* queue/unqueue to an array, and adds an `endLoop` method to unqueue and run every
* queued call.
*
* @param {ve.EventSequencer} eventSequencer The EventSequencer to hijack
*/
ve
.
test
.
utils
.
hijackEventSequencerTimeouts
=
function
(
eventSequencer
)
{
eventSequencer
.
postponedCalls
=
[];
eventSequencer
.
postpone
=
function
(
f
)
{
this
.
postponedCalls
.
push
(
f
);
return
this
.
postponedCalls
.
length
-
1
;
};
eventSequencer
.
cancelPostponed
=
function
(
callId
)
{
this
.
postponedCalls
[
callId
]
=
null
;
};
eventSequencer
.
endLoop
=
function
()
{
// Run every postponed call in order of postponement. Do not cache
// list length, because postponed calls may add more postponed calls
for
(
let
i
=
0
;
i
<
this
.
postponedCalls
.
length
;
i
++
)
{
const
f
=
this
.
postponedCalls
[
i
];
if
(
f
)
{
// Exceptions thrown here will leave the postponed calls
// list in an inconsistent state
f
();
}
}
this
.
postponedCalls
.
length
=
0
;
};
};
}()
);
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Aug 18 2026, 10:21 (6 w, 11 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
da/c3/cd6dc434760a5db0492f7fd1c2c3
Default Alt Text
ve.test.utils.js (30 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment