Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F2753125
tableOfContents.test.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
12 KB
Referenced Files
None
Subscribers
None
tableOfContents.test.js
View Options
const
mustache
=
require
(
'mustache'
);
const
fs
=
require
(
'fs'
);
const
tableOfContentsTemplate
=
fs
.
readFileSync
(
'includes/templates/TableOfContents.mustache'
,
'utf8'
);
const
tableOfContentsContentsTemplate
=
fs
.
readFileSync
(
'includes/templates/TableOfContents__list.mustache'
,
'utf8'
);
const
tableOfContentsLineTemplate
=
fs
.
readFileSync
(
'includes/templates/TableOfContents__line.mustache'
,
'utf8'
);
const
pinnableElementOpenTemplate
=
fs
.
readFileSync
(
'includes/templates/PinnableElement/Open.mustache'
,
'utf8'
);
const
pinnableElementCloseTemplate
=
fs
.
readFileSync
(
'includes/templates/PinnableElement/Close.mustache'
,
'utf8'
);
const
pinnableHeaderTemplate
=
fs
.
readFileSync
(
'includes/templates/PinnableHeader.mustache'
,
'utf8'
);
const
initTableOfContents
=
require
(
'../../resources/skins.vector.js/tableOfContents.js'
);
let
/** @type {HTMLElement} */
container
,
/** @type {HTMLElement} */
fooSection
,
/** @type {HTMLElement} */
barSection
,
/** @type {HTMLElement} */
bazSection
,
/** @type {HTMLElement} */
quxSection
,
/** @type {HTMLElement} */
quuxSection
;
const
onHeadingClick
=
jest
.
fn
();
const
onHashChange
=
jest
.
fn
();
const
onToggleClick
=
jest
.
fn
();
const
onTogglePinned
=
jest
.
fn
();
const
SECTIONS
=
[
{
toclevel
:
1
,
number
:
'1'
,
line
:
'foo'
,
anchor
:
'foo'
,
linkAnchor
:
'foo'
,
'is-top-level-section'
:
true
,
'is-parent-section'
:
false
,
'array-sections'
:
null
},
{
toclevel
:
1
,
number
:
'2'
,
line
:
'bar'
,
anchor
:
'bar'
,
linkAnchor
:
'bar'
,
'is-top-level-section'
:
true
,
'is-parent-section'
:
true
,
'vector-button-label'
:
'Toggle bar subsection'
,
'array-sections'
:
[
{
toclevel
:
2
,
number
:
'2.1'
,
line
:
'baz'
,
anchor
:
'baz'
,
linkAnchor
:
'baz'
,
'is-top-level-section'
:
false
,
'is-parent-section'
:
true
,
'array-sections'
:
[
{
toclevel
:
3
,
number
:
'2.1.1'
,
line
:
'qux'
,
anchor
:
'qux'
,
linkAnchor
:
'qux'
,
'is-top-level-section'
:
false
,
'is-parent-section'
:
false
,
'array-sections'
:
null
}
]
}
]
},
{
toclevel
:
1
,
number
:
'3'
,
line
:
'quux'
,
anchor
:
'quux'
,
linkAnchor
:
'quux'
,
'is-top-level-section'
:
true
,
'is-parent-section'
:
false
,
'array-sections'
:
null
}
];
/**
* @param {Object} templateProps
* @return {string}
*/
function
render
(
templateProps
=
{}
)
{
const
templateData
=
Object
.
assign
(
{
'msg-vector-toc-beginning'
:
'Beginning'
,
'vector-is-collapse-sections-enabled'
:
false
,
'array-sections'
:
SECTIONS
,
id
:
'vector-toc'
,
'data-pinnable-header'
:
{
'is-pinned'
:
true
,
'data-feature-name'
:
'pinned'
,
'data-pinnable-element-id'
:
'vector-toc'
,
label
:
'Contents'
,
'label-tag-name'
:
'h2'
,
'pin-label'
:
'move to sidebar'
,
'unpin-label'
:
'hide'
}
},
templateProps
);
return
mustache
.
render
(
tableOfContentsTemplate
,
templateData
,
{
'PinnableElement/Open'
:
pinnableElementOpenTemplate
,
'PinnableElement/Close'
:
pinnableElementCloseTemplate
,
PinnableHeader
:
pinnableHeaderTemplate
,
TableOfContents__list
:
tableOfContentsContentsTemplate
,
// eslint-disable-line camelcase
TableOfContents__line
:
tableOfContentsLineTemplate
// eslint-disable-line camelcase
}
);
}
/**
* @param {Object} templateProps
* @return {module:TableOfContents~TableOfContents}
*/
function
mount
(
templateProps
=
{}
)
{
document
.
body
.
innerHTML
=
render
(
templateProps
);
container
=
/** @type {HTMLElement} */
(
document
.
getElementById
(
'vector-toc'
)
);
fooSection
=
/** @type {HTMLElement} */
(
document
.
getElementById
(
'toc-foo'
)
);
barSection
=
/** @type {HTMLElement} */
(
document
.
getElementById
(
'toc-bar'
)
);
bazSection
=
/** @type {HTMLElement} */
(
document
.
getElementById
(
'toc-baz'
)
);
quxSection
=
/** @type {HTMLElement} */
(
document
.
getElementById
(
'toc-qux'
)
);
quuxSection
=
/** @type {HTMLElement} */
(
document
.
getElementById
(
'toc-quux'
)
);
return
initTableOfContents
(
{
container
,
onHeadingClick
,
onHashChange
,
onToggleClick
,
onTogglePinned
}
);
}
describe
(
'Table of contents'
,
()
=>
{
/**
* @type {module:TableOfContents~TableOfContents}
*/
let
toc
;
beforeEach
(
()
=>
{
global
.
window
.
matchMedia
=
jest
.
fn
(
()
=>
(
{}
)
);
}
);
afterEach
(
()
=>
{
if
(
toc
)
{
toc
.
unmount
();
toc
=
undefined
;
}
mw
.
util
.
getTargetFromFragment
=
undefined
;
}
);
describe
(
'renders'
,
()
=>
{
test
(
'when `vector-is-collapse-sections-enabled` is false'
,
()
=>
{
toc
=
mount
();
expect
(
document
.
body
.
innerHTML
).
toMatchSnapshot
();
expect
(
barSection
.
classList
.
contains
(
toc
.
EXPANDED_SECTION_CLASS
)
).
toEqual
(
true
);
}
);
test
(
'when `vector-is-collapse-sections-enabled` is true'
,
()
=>
{
toc
=
mount
(
{
'vector-is-collapse-sections-enabled'
:
true
}
);
expect
(
document
.
body
.
innerHTML
).
toMatchSnapshot
();
expect
(
barSection
.
classList
.
contains
(
toc
.
EXPANDED_SECTION_CLASS
)
).
toEqual
(
false
);
}
);
test
(
'toggles for top level parent sections'
,
()
=>
{
toc
=
mount
();
expect
(
fooSection
.
getElementsByClassName
(
toc
.
TOGGLE_CLASS
).
length
).
toEqual
(
0
);
expect
(
barSection
.
getElementsByClassName
(
toc
.
TOGGLE_CLASS
).
length
).
toEqual
(
1
);
expect
(
bazSection
.
getElementsByClassName
(
toc
.
TOGGLE_CLASS
).
length
).
toEqual
(
0
);
expect
(
quxSection
.
getElementsByClassName
(
toc
.
TOGGLE_CLASS
).
length
).
toEqual
(
0
);
expect
(
quuxSection
.
getElementsByClassName
(
toc
.
TOGGLE_CLASS
).
length
).
toEqual
(
0
);
}
);
}
);
describe
(
'binds event listeners'
,
()
=>
{
test
(
'for onHeadingClick'
,
()
=>
{
toc
=
mount
();
const
heading
=
/** @type {HTMLElement} */
(
document
.
querySelector
(
`#toc-foo .
${
toc
.
LINK_CLASS
}
`
)
);
heading
.
click
();
expect
(
onToggleClick
).
not
.
toBeCalled
();
expect
(
onHashChange
).
not
.
toBeCalled
();
expect
(
onHeadingClick
).
toBeCalled
();
}
);
test
(
'for onToggleClick'
,
()
=>
{
toc
=
mount
();
const
toggle
=
/** @type {HTMLElement} */
(
document
.
querySelector
(
`#toc-bar .
${
toc
.
TOGGLE_CLASS
}
`
)
);
toggle
.
click
();
expect
(
onHeadingClick
).
not
.
toBeCalled
();
expect
(
onHashChange
).
not
.
toBeCalled
();
expect
(
onToggleClick
).
toBeCalled
();
}
);
test
(
'for onHashChange'
,
()
=>
{
mw
.
util
.
getTargetFromFragment
=
jest
.
fn
().
mockImplementation
(
(
hash
)
=>
hash
===
'toc-foo'
?
fooSection
:
null
);
mount
();
// Jest doesn't trigger a hashchange event when setting a hash location.
location
.
hash
=
'foo'
;
window
.
dispatchEvent
(
new
HashChangeEvent
(
'hashchange'
,
{
oldURL
:
'http://example.com'
,
newURL
:
'http://example.com#foo'
}
)
);
expect
(
onHeadingClick
).
not
.
toBeCalled
();
expect
(
onToggleClick
).
not
.
toBeCalled
();
expect
(
onHashChange
).
toBeCalled
();
}
);
}
);
describe
(
'applies correct classes'
,
()
=>
{
test
(
'when changing active sections'
,
()
=>
{
toc
=
mount
(
{
'vector-is-collapse-sections-enabled'
:
true
}
);
let
activeSections
;
let
activeTopSections
;
/**
* @param {string} id
* @param {HTMLElement} activeSection
* @param {HTMLElement} activeTopSection
*/
function
testActiveClasses
(
id
,
activeSection
,
activeTopSection
)
{
toc
.
changeActiveSection
(
id
);
activeSections
=
container
.
querySelectorAll
(
`.
${
toc
.
ACTIVE_SECTION_CLASS
}
`
);
activeTopSections
=
container
.
querySelectorAll
(
`.
${
toc
.
ACTIVE_TOP_SECTION_CLASS
}
`
);
expect
(
activeSections
.
length
).
toEqual
(
1
);
expect
(
activeTopSections
.
length
).
toEqual
(
1
);
expect
(
activeSections
[
0
]
).
toEqual
(
activeSection
);
expect
(
activeTopSections
[
0
]
).
toEqual
(
activeTopSection
);
}
testActiveClasses
(
'toc-foo'
,
fooSection
,
fooSection
);
testActiveClasses
(
'toc-bar'
,
barSection
,
barSection
);
testActiveClasses
(
'toc-baz'
,
bazSection
,
barSection
);
testActiveClasses
(
'toc-qux'
,
quxSection
,
barSection
);
testActiveClasses
(
'toc-quux'
,
quuxSection
,
quuxSection
);
}
);
test
(
'when expanding sections'
,
()
=>
{
toc
=
mount
();
toc
.
expandSection
(
'toc-bar'
);
expect
(
barSection
.
classList
.
contains
(
toc
.
EXPANDED_SECTION_CLASS
)
).
toEqual
(
true
);
}
);
test
(
'when toggling sections'
,
()
=>
{
toc
=
mount
();
toc
.
toggleExpandSection
(
'toc-bar'
);
expect
(
barSection
.
classList
.
contains
(
toc
.
EXPANDED_SECTION_CLASS
)
).
toEqual
(
false
);
toc
.
toggleExpandSection
(
'toc-bar'
);
expect
(
barSection
.
classList
.
contains
(
toc
.
EXPANDED_SECTION_CLASS
)
).
toEqual
(
true
);
}
);
}
);
describe
(
'applies the correct aria attributes'
,
()
=>
{
test
(
'when initialized'
,
()
=>
{
toc
=
mount
();
const
toggleButton
=
/** @type {HTMLElement} */
(
barSection
.
querySelector
(
`.
${
toc
.
TOGGLE_CLASS
}
`
)
);
expect
(
toggleButton
.
getAttribute
(
'aria-expanded'
)
).
toEqual
(
'true'
);
}
);
test
(
'when expanding sections'
,
()
=>
{
toc
=
mount
();
const
toggleButton
=
/** @type {HTMLElement} */
(
barSection
.
querySelector
(
`.
${
toc
.
TOGGLE_CLASS
}
`
)
);
toc
.
expandSection
(
'toc-bar'
);
expect
(
toggleButton
.
getAttribute
(
'aria-expanded'
)
).
toEqual
(
'true'
);
}
);
test
(
'when toggling sections'
,
()
=>
{
toc
=
mount
();
const
toggleButton
=
/** @type {HTMLElement} */
(
barSection
.
querySelector
(
`.
${
toc
.
TOGGLE_CLASS
}
`
)
);
toc
.
toggleExpandSection
(
'toc-bar'
);
expect
(
toggleButton
.
getAttribute
(
'aria-expanded'
)
).
toEqual
(
'false'
);
toc
.
toggleExpandSection
(
'toc-bar'
);
expect
(
toggleButton
.
getAttribute
(
'aria-expanded'
)
).
toEqual
(
'true'
);
}
);
}
);
describe
(
'when the hash fragment changes'
,
()
=>
{
test
(
'expands and activates corresponding section'
,
()
=>
{
mw
.
util
.
getTargetFromFragment
=
jest
.
fn
().
mockImplementation
(
(
hash
)
=>
hash
===
'toc-qux'
?
quxSection
:
null
);
toc
=
mount
(
{
'vector-is-collapse-sections-enabled'
:
true
}
);
expect
(
quxSection
.
classList
.
contains
(
toc
.
ACTIVE_SECTION_CLASS
)
).
toEqual
(
false
);
// Jest doesn't trigger a hashchange event when setting a hash location.
location
.
hash
=
'qux'
;
window
.
dispatchEvent
(
new
HashChangeEvent
(
'hashchange'
,
{
oldURL
:
'http://example.com'
,
newURL
:
'http://example.com#qux'
}
)
);
const
activeSections
=
container
.
querySelectorAll
(
`.
${
toc
.
ACTIVE_SECTION_CLASS
}
`
);
const
activeTopSections
=
container
.
querySelectorAll
(
`.
${
toc
.
ACTIVE_TOP_SECTION_CLASS
}
`
);
expect
(
activeSections
.
length
).
toEqual
(
1
);
expect
(
activeTopSections
.
length
).
toEqual
(
1
);
expect
(
barSection
.
classList
.
contains
(
toc
.
ACTIVE_TOP_SECTION_CLASS
)
).
toEqual
(
true
);
expect
(
barSection
.
classList
.
contains
(
toc
.
EXPANDED_SECTION_CLASS
)
).
toEqual
(
true
);
expect
(
quxSection
.
classList
.
contains
(
toc
.
ACTIVE_SECTION_CLASS
)
).
toEqual
(
true
);
}
);
}
);
describe
(
'reloadTableOfContents'
,
()
=>
{
test
(
're-renders toc when wikipage.tableOfContents hook is fired with empty sections'
,
async
()
=>
{
toc
=
mount
();
await
toc
.
reloadTableOfContents
(
[]
);
expect
(
document
.
body
.
innerHTML
).
toMatchSnapshot
();
}
);
test
(
're-renders toc when wikipage.tableOfContents hook is fired with sections'
,
async
()
=>
{
jest
.
spyOn
(
mw
.
loader
,
'using'
).
mockImplementation
(
()
=>
Promise
.
resolve
()
);
mw
.
template
.
getCompiler
=
()
=>
{};
jest
.
spyOn
(
mw
,
'message'
).
mockImplementation
(
(
msg
)
=>
{
const
msgFactory
=
(
/** @type {string} */
text
)
=>
(
{
parse
:
()
=>
''
,
plain
:
()
=>
''
,
escaped
:
()
=>
''
,
exists
:
()
=>
true
,
text
:
()
=>
text
}
);
switch
(
msg
)
{
case
'vector-toc-beginning'
:
return
msgFactory
(
'Beginning'
);
default
:
return
msgFactory
(
''
);
}
}
);
jest
.
spyOn
(
mw
.
template
,
'getCompiler'
).
mockImplementation
(
()
=>
(
{
compile
:
()
=>
(
{
render
:
(
/** @type {Object} */
data
)
=>
(
{
html
:
()
=>
render
(
data
)
}
)
}
)
}
)
);
toc
=
mount
();
const
toggleButton
=
/** @type {HTMLElement} */
(
barSection
.
querySelector
(
`.
${
toc
.
TOGGLE_CLASS
}
`
)
);
// Collapse section.
toc
.
toggleExpandSection
(
'toc-bar'
);
expect
(
toggleButton
.
getAttribute
(
'aria-expanded'
)
).
toEqual
(
'false'
);
// wikipage.tableOfContents includes the nested sections in the top level
// of the array.
await
toc
.
reloadTableOfContents
(
[
// foo
SECTIONS
[
0
],
// bar
SECTIONS
[
1
],
// baz
SECTIONS
[
1
][
'array-sections'
][
0
],
// qux
SECTIONS
[
1
][
'array-sections'
][
0
][
'array-sections'
][
0
],
// quux
SECTIONS
[
2
],
// Add new section to see how the re-render performs.
{
toclevel
:
1
,
number
:
'4'
,
line
:
'bat'
,
anchor
:
'bat'
,
linkAnchor
:
'bat'
,
'is-top-level-section'
:
true
,
'is-parent-section'
:
false
,
'array-sections'
:
null
}
]
);
const
newToggleButton
=
/** @type {HTMLElement} */
(
document
.
querySelector
(
`#toc-bar .
${
toc
.
TOGGLE_CLASS
}
`
)
);
expect
(
newToggleButton
).
not
.
toBeNull
();
// Check that the sections render in their expanded form.
expect
(
newToggleButton
.
getAttribute
(
'aria-expanded'
)
).
toEqual
(
'true'
);
// Verify newly rendered TOC html matches the expected html.
expect
(
document
.
body
.
innerHTML
).
toMatchSnapshot
();
}
);
}
);
}
);
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Jul 3, 20:50 (1 d, 3 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
0e/bc/26826ad64835622366e068db25f8
Default Alt Text
tableOfContents.test.js (12 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment