Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-docs.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorPaul Slaughter <pslaughter@gitlab.com>2020-04-29 07:57:08 +0300
committerPaul Slaughter <pslaughter@gitlab.com>2020-05-20 01:06:11 +0300
commit26bb98496b060ef60d6edc265fa7a12ee9d612a4 (patch)
treeadb7f036b85683099bca6a23d3d76ee06ec1396b /spec
parenta61458419fe97029cc9d1c198bd313dd6af548b6 (diff)
Create Vue table_of_contents
Also: - Updates toc.scss to look for .table-of-contents - Creates some dom helpers in shared - Stick to footer directive - Moves toc collapse button to component - Fixes issue where collapsed on mobile affects full view
Diffstat (limited to 'spec')
-rw-r--r--spec/frontend/default/components/__snapshots__/table_of_contents_list_spec.js.snap7
-rw-r--r--spec/frontend/default/components/__snapshots__/table_of_contents_spec.js.snap137
-rw-r--r--spec/frontend/default/components/collapsible_container_spec.js124
-rw-r--r--spec/frontend/default/components/table_of_contents_list_spec.js46
-rw-r--r--spec/frontend/default/components/table_of_contents_spec.js102
-rw-r--r--spec/frontend/shared/dom_parse_toc_spec.js18
-rw-r--r--spec/frontend/shared/dom_spec.js32
-rw-r--r--spec/frontend/shared/toc_helper.js30
8 files changed, 496 insertions, 0 deletions
diff --git a/spec/frontend/default/components/__snapshots__/table_of_contents_list_spec.js.snap b/spec/frontend/default/components/__snapshots__/table_of_contents_list_spec.js.snap
new file mode 100644
index 00000000..6a00c5ea
--- /dev/null
+++ b/spec/frontend/default/components/__snapshots__/table_of_contents_list_spec.js.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`frontend/default/components/table_of_contents_list with empty items shows empty ul 1`] = `
+<ul
+ class="nav nav-pills flex-column"
+/>
+`;
diff --git a/spec/frontend/default/components/__snapshots__/table_of_contents_spec.js.snap b/spec/frontend/default/components/__snapshots__/table_of_contents_spec.js.snap
new file mode 100644
index 00000000..6483dea6
--- /dev/null
+++ b/spec/frontend/default/components/__snapshots__/table_of_contents_spec.js.snap
@@ -0,0 +1,137 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`frontend/default/components/table_of_contents matches snapshot 1`] = `
+<div
+ class="table-of-contents-container position-relative"
+ id="markdown-toc"
+>
+ <a
+ aria-controls="markdown-toc"
+ class="toc-collapse collapsed"
+ data-testid="collapse"
+ href="#"
+ role="button"
+ />
+
+ <div
+ class="table-of-contents sm-collapsed"
+ data-testid="container"
+ >
+ <h4>
+ On this page:
+ </h4>
+
+ <ul
+ class="nav nav-pills flex-column my-0"
+ data-testid="main-list"
+ >
+ <li>
+ <a
+ class="nav-link"
+ href="#lorem"
+ id="Lorem-anchor"
+ >
+ Lorem
+ </a>
+
+ <ul
+ class="nav nav-pills flex-column"
+ >
+ <li>
+ <a
+ class="nav-link"
+ href="#lorem-"
+ id="Lorem 2-anchor"
+ >
+ Lorem 2
+ </a>
+
+ <!---->
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a
+ class="nav-link"
+ href="#ipsum"
+ id="Ipsum-anchor"
+ >
+ Ipsum
+ </a>
+
+ <ul
+ class="nav nav-pills flex-column"
+ >
+ <li>
+ <a
+ class="nav-link"
+ href="#dolar"
+ id="Dolar-anchor"
+ >
+ Dolar
+ </a>
+
+ <ul
+ class="nav nav-pills flex-column"
+ >
+ <li>
+ <a
+ class="nav-link"
+ href="#sit"
+ id="Sit-anchor"
+ >
+ Sit
+ </a>
+
+ <!---->
+ </li>
+ <li>
+ <a
+ class="nav-link"
+ href="#amit"
+ id="Amit-anchor"
+ >
+ Amit
+ </a>
+
+ <!---->
+ </li>
+ <li>
+ <a
+ class="nav-link"
+ href="#test"
+ id="Test-anchor"
+ >
+ Test
+ </a>
+
+ <!---->
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ </ul>
+
+ <div
+ class="border-top mt-3 pt-3"
+ >
+ <ul
+ class="nav nav-pills flex-column my-0"
+ data-testid="help-and-feedback"
+ >
+ <li>
+ <a
+ class="nav-link"
+ href="#test-help-and-feedback"
+ >
+ Help and feedback
+ </a>
+
+ <!---->
+ </li>
+ </ul>
+ </div>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/default/components/collapsible_container_spec.js b/spec/frontend/default/components/collapsible_container_spec.js
new file mode 100644
index 00000000..259cdf8c
--- /dev/null
+++ b/spec/frontend/default/components/collapsible_container_spec.js
@@ -0,0 +1,124 @@
+import { shallowMount } from '@vue/test-utils';
+import CollapsibleContainer from '../../../../content/frontend/default/components/collapsible_container.vue';
+import * as dom from '../../../../content/frontend/shared/dom';
+
+const TEST_COLLAPSING_CLASS = 'test-collapsing';
+const TEST_COLLAPSED_CLASS = 'test-collapsed';
+const TEST_SLOT = 'Lorem ipsum dolar sit amit';
+const TEST_OUTER_HEIGHT = 400;
+const KICKOFF_DELAY = 50;
+const FINISH_DELAY = 400;
+
+describe('frontend/default/components/collapsible_container', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ // jquery is not available in Jest yet so we need to mock this method
+ jest.spyOn(dom, 'getOuterHeight').mockImplementation(x => Number(x.dataset.testOuterHeight));
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+
+ jest.useRealTimers();
+ });
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(CollapsibleContainer, {
+ propsData: {
+ collapsingClass: TEST_COLLAPSING_CLASS,
+ collapsedClass: TEST_COLLAPSED_CLASS,
+ ...props,
+ },
+ slots: {
+ default: TEST_SLOT,
+ },
+ attrs: {
+ 'data-test-outer-height': TEST_OUTER_HEIGHT.toString(),
+ },
+ });
+ };
+ const findStyleHeight = () => wrapper.element.style.height;
+ const waitForKickoff = () => jest.advanceTimersByTime(KICKOFF_DELAY);
+ const waitForTransition = () => jest.advanceTimersByTime(FINISH_DELAY - KICKOFF_DELAY);
+
+ describe.each`
+ isCollapsed | startHeight | endHeight | startClasses | endClasses
+ ${true} | ${0} | ${TEST_OUTER_HEIGHT} | ${[TEST_COLLAPSED_CLASS]} | ${[]}
+ ${false} | ${TEST_OUTER_HEIGHT} | ${0} | ${[]} | ${[TEST_COLLAPSED_CLASS]}
+ `(
+ 'when isCollapsed = $isCollapsed',
+ ({ isCollapsed, startHeight, endHeight, startClasses, endClasses }) => {
+ beforeEach(() => {
+ createComponent({ isCollapsed });
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('renders slot', () => {
+ expect(wrapper.text()).toBe(TEST_SLOT);
+ });
+
+ it('has starting classes', () => {
+ expect(wrapper.classes()).toEqual(startClasses);
+ });
+
+ it('has not emitted anything', () => {
+ expect(wrapper.emitted()).toEqual({});
+ });
+
+ describe('when collapse is triggered', () => {
+ beforeEach(() => {
+ wrapper.vm.collapse(!isCollapsed);
+
+ // set props because this is what would naturally happen with `v-model`
+ wrapper.setProps({ isCollapsed: !isCollapsed });
+ });
+
+ it('has collapsing class', () => {
+ expect(wrapper.classes()).toEqual([TEST_COLLAPSING_CLASS]);
+ });
+
+ it('emits change', () => {
+ expect(wrapper.emitted().change).toEqual([[!isCollapsed]]);
+ });
+
+ it('sets starting height', () => {
+ expect(findStyleHeight()).toEqual(`${startHeight}px`);
+ });
+
+ it('triggering collapse again does not do anything', () => {
+ wrapper.vm.collapse(isCollapsed);
+
+ expect(wrapper.emitted().change).toEqual([[!isCollapsed]]);
+ });
+
+ describe('after animation kickoff delay', () => {
+ beforeEach(() => {
+ waitForKickoff();
+ });
+
+ it('sets ending height', () => {
+ expect(findStyleHeight()).toEqual(`${endHeight}px`);
+ });
+
+ describe('after transition', () => {
+ beforeEach(() => {
+ waitForTransition();
+ });
+
+ it('does not set height', () => {
+ expect(findStyleHeight()).toBe('');
+ });
+
+ it('sets ending classes', () => {
+ expect(wrapper.classes()).toEqual(endClasses);
+ });
+ });
+ });
+ });
+ },
+ );
+});
diff --git a/spec/frontend/default/components/table_of_contents_list_spec.js b/spec/frontend/default/components/table_of_contents_list_spec.js
new file mode 100644
index 00000000..2d69f880
--- /dev/null
+++ b/spec/frontend/default/components/table_of_contents_list_spec.js
@@ -0,0 +1,46 @@
+import { mount } from '@vue/test-utils';
+import { parseTOC } from '../../../../content/frontend/shared/dom_parse_toc';
+import TableOfContentsList from '../../../../content/frontend/default/components/table_of_contents_list.vue';
+import { createExampleToc } from '../../shared/toc_helper';
+
+describe('frontend/default/components/table_of_contents_list', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(TableOfContentsList, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findItemsData = () => parseTOC(wrapper.element);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('with items', () => {
+ let items;
+
+ beforeEach(() => {
+ items = createExampleToc();
+ createComponent({ items });
+ });
+
+ it('renders all items', () => {
+ expect(findItemsData()).toEqual(items);
+ });
+ });
+
+ describe('with empty items', () => {
+ beforeEach(() => {
+ createComponent({ items: [] });
+ });
+
+ it('shows empty ul', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/default/components/table_of_contents_spec.js b/spec/frontend/default/components/table_of_contents_spec.js
new file mode 100644
index 00000000..a256cae5
--- /dev/null
+++ b/spec/frontend/default/components/table_of_contents_spec.js
@@ -0,0 +1,102 @@
+import { shallowMount, mount } from '@vue/test-utils';
+import TableOfContents from '../../../../content/frontend/default/components/table_of_contents.vue';
+import * as dom from '../../../../content/frontend/shared/dom';
+import { createExampleToc } from '../../shared/toc_helper';
+
+const TEST_ITEMS = createExampleToc();
+const TEST_HELP_AND_FEEDBACK_ID = 'test-help-and-feedback';
+
+describe('frontend/default/components/table_of_contents', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ beforeEach(() => {
+ // jquery is not available in Jest yet so we need to mock this method
+ jest.spyOn(dom, 'getOuterHeight').mockReturnValue(100);
+ });
+
+ const createComponent = (props = {}, mountFn = shallowMount) => {
+ wrapper = mountFn(TableOfContents, {
+ propsData: {
+ items: TEST_ITEMS,
+ helpAndFeedbackId: TEST_HELP_AND_FEEDBACK_ID,
+ ...props,
+ },
+ });
+ };
+
+ const findCollapseButton = () => wrapper.find('[data-testid="collapse"]');
+ const findCollapsibleContainer = () => wrapper.find('[data-testid="container"]');
+ const findMainList = () => wrapper.find('[data-testid="main-list"]');
+ const findHelpAndFeedback = () => wrapper.find('[data-testid="help-and-feedback"]');
+ const clickCollapseButton = () => findCollapseButton().trigger('click');
+
+ it('matches snapshot', () => {
+ createComponent({ hasHelpAndFeedback: true }, mount);
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('with hasHelpAndFeedback', () => {
+ beforeEach(() => {
+ createComponent({ hasHelpAndFeedback: true });
+ });
+
+ it('shows help and feedback', () => {
+ expect(findHelpAndFeedback().exists()).toBe(true);
+ expect(findHelpAndFeedback().props('items')).toEqual([
+ {
+ href: `#${TEST_HELP_AND_FEEDBACK_ID}`,
+ id: null,
+ items: [],
+ text: 'Help and feedback',
+ },
+ ]);
+ });
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent({}, mount);
+ });
+
+ it('does not show help and feedback', () => {
+ expect(findHelpAndFeedback().exists()).toBe(false);
+ });
+
+ it('renders toc list', () => {
+ expect(findMainList().props('items')).toEqual(TEST_ITEMS);
+ });
+
+ it('is initially collapsed', () => {
+ expect(findCollapseButton().classes('collapsed')).toBe(true);
+ expect(findCollapsibleContainer().classes('sm-collapsed')).toBe(true);
+ });
+
+ describe('when collapse button is pressed', () => {
+ beforeEach(() => {
+ clickCollapseButton();
+ });
+
+ it('starts expanding', () => {
+ expect(findCollapsibleContainer().classes('sm-collapsing')).toBe(true);
+ });
+
+ it('updates button class', () => {
+ expect(findCollapseButton().classes('collapsed')).toBe(false);
+ });
+
+ it('when button pressed again, nothing happens because in the middle of collapsing', () => {
+ clickCollapseButton();
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findCollapsibleContainer().classes('sm-collapsing')).toBe(true);
+ expect(findCollapseButton().classes('collapsed')).toBe(false);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/shared/dom_parse_toc_spec.js b/spec/frontend/shared/dom_parse_toc_spec.js
new file mode 100644
index 00000000..adcacb90
--- /dev/null
+++ b/spec/frontend/shared/dom_parse_toc_spec.js
@@ -0,0 +1,18 @@
+import { parseTOC } from '../../../content/frontend/shared/dom_parse_toc';
+import { createItem, createTOCElement, createExampleToc } from './toc_helper';
+
+describe('frontend/shared/dom_parse_toc', () => {
+ it('parses nested HTML list', () => {
+ const list = createExampleToc();
+ const el = createTOCElement(list);
+
+ expect(parseTOC(el)).toEqual(list);
+ });
+
+ it('skips items that do not have links', () => {
+ const list = [createItem('Lorem'), { items: [createItem('no link')] }, createItem('Ipsum')];
+ const el = createTOCElement(list);
+
+ expect(parseTOC(el)).toEqual([createItem('Lorem'), createItem('Ipsum')]);
+ });
+});
diff --git a/spec/frontend/shared/dom_spec.js b/spec/frontend/shared/dom_spec.js
new file mode 100644
index 00000000..ceb09708
--- /dev/null
+++ b/spec/frontend/shared/dom_spec.js
@@ -0,0 +1,32 @@
+import { findChildByTagName } from '../../../content/frontend/shared/dom';
+
+describe('frontend/shared/dom', () => {
+ const createElementWithChildren = children => {
+ const el = document.createElement('div');
+
+ children.forEach(tag => {
+ const child = document.createElement(tag);
+ el.appendChild(child);
+ });
+
+ return el;
+ };
+
+ describe('findChildByTagName', () => {
+ it.each`
+ children | tagName | expectedIndex
+ ${['div', 'p', 'ul', 'p']} | ${'p'} | ${1}
+ ${['div', 'ul']} | ${'p'} | ${-1}
+ ${['li', 'li', 'li']} | ${'li'} | ${0}
+ ${[]} | ${'li'} | ${-1}
+ `(
+ 'with children $children and $tagName, returns $expectedIndex child',
+ ({ children, tagName, expectedIndex }) => {
+ const el = createElementWithChildren(children);
+ const expectedChild = el.childNodes[expectedIndex];
+
+ expect(findChildByTagName(el, tagName)).toBe(expectedChild);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/shared/toc_helper.js b/spec/frontend/shared/toc_helper.js
new file mode 100644
index 00000000..574f1d17
--- /dev/null
+++ b/spec/frontend/shared/toc_helper.js
@@ -0,0 +1,30 @@
+export const createItem = (text, items = []) => ({
+ text,
+ href: `#${text.replace(/[^a-zA-Z-]+/g, '-').toLowerCase()}`,
+ id: `${text}-anchor`,
+ items,
+});
+
+export const buildHTML = list =>
+ list
+ .map(
+ ({ text, href, id, items }) =>
+ `<li>
+${text ? `<a href="${href}" id="${id}">${text}</a>` : ''}
+${items?.length ? `<ul>${buildHTML(items)}</ul>` : ''}
+</li>`,
+ )
+ .join('');
+
+export const createTOCElement = list => {
+ const ul = document.createElement('ul');
+ ul.innerHTML = buildHTML(list);
+ return ul;
+};
+
+export const createExampleToc = () => [
+ createItem('Lorem', [createItem('Lorem 2')]),
+ createItem('Ipsum', [
+ createItem('Dolar', [createItem('Sit'), createItem('Amit'), createItem('Test')]),
+ ]),
+];