Add Jest tests
Automate CSS and JS Banners Remove image fading for now Reference docker-compose.yml restored
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
export default class Article {
|
||||
constructor(cfg) {
|
||||
this.article = cfg.article;
|
||||
this.metadata = cfg.metadata;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
export default class ImageFade {
|
||||
constructor() {
|
||||
this.images = Array.prototype.slice.call(document.querySelectorAll('img'));
|
||||
this.images.forEach((image) => {
|
||||
this.prepareImage(image);
|
||||
this.setEvent(image);
|
||||
});
|
||||
}
|
||||
|
||||
prepareImage(image) {
|
||||
image.style.opacity = 0;
|
||||
}
|
||||
|
||||
setEvent(image) {
|
||||
image.addEventListener('load', () => {
|
||||
image.style.opacity = 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
23
js/index.js
23
js/index.js
@@ -1,22 +1,3 @@
|
||||
import Article from './article';
|
||||
import ImageFade from './imagefade';
|
||||
import Menu from './menu';
|
||||
import Nav from './nav';
|
||||
import windy from './windy';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const site = {};
|
||||
site.menu = new Menu({
|
||||
el: document.querySelector('.Menu'),
|
||||
});
|
||||
site.nav = new Nav({
|
||||
el: document.querySelector('.Nav'),
|
||||
menu: site.menu,
|
||||
});
|
||||
site.images = new ImageFade();
|
||||
|
||||
site.articles = [].slice.call(document.querySelectorAll('.Article'))
|
||||
.map((article, index) => new Article({
|
||||
article,
|
||||
metadata: document.querySelectorAll('.Metadata')[index],
|
||||
}));
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', windy);
|
||||
|
||||
46
js/menu.js
46
js/menu.js
@@ -1,13 +1,19 @@
|
||||
const CLS = {
|
||||
const STATES = {
|
||||
visible: 'is-visible',
|
||||
minimized: 'is-minimized',
|
||||
};
|
||||
|
||||
export default class Menu {
|
||||
constructor(cfg) {
|
||||
this.el = cfg.el;
|
||||
constructor(cfg = {}) {
|
||||
if (!cfg.container) return false;
|
||||
this.container = cfg.container;
|
||||
this.state = 'visible';
|
||||
this.lastKnownScrollPosition = 0;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.container.setAttribute('aria-haspopup', true);
|
||||
this.container.setAttribute('aria-expanded', false);
|
||||
this.attachEvents();
|
||||
setTimeout(() => {
|
||||
this.showMenu();
|
||||
@@ -15,33 +21,39 @@ export default class Menu {
|
||||
}
|
||||
|
||||
attachEvents() {
|
||||
let lastKnownPosition = 0;
|
||||
window.addEventListener('scroll', () => {
|
||||
if (this.state !== 'hidden') {
|
||||
if (this.state !== 'visible' && window.scrollY < lastKnownPosition) {
|
||||
this.showMenu();
|
||||
} else if (this.state !== 'minimized' && window.scrollY >
|
||||
lastKnownPosition) {
|
||||
this.minimizeMenu();
|
||||
}
|
||||
lastKnownPosition = window.scrollY;
|
||||
}
|
||||
this.updateState(this.lastKnownScrollPosition, window.scrollY);
|
||||
this.lastKnownScrollPosition = window.scrollY;
|
||||
});
|
||||
}
|
||||
|
||||
updateState(lastY = 0, currentY) {
|
||||
if (this.state !== 'hidden') {
|
||||
if (this.state !== 'visible' && currentY < lastY) {
|
||||
this.showMenu();
|
||||
} else if (this.state !== 'minimized' && currentY >
|
||||
lastY) {
|
||||
this.minimizeMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showMenu() {
|
||||
this.state = 'visible';
|
||||
this.el.classList.add(CLS.visible);
|
||||
this.el.classList.remove(CLS.minimized);
|
||||
this.container.setAttribute('aria-expanded', false);
|
||||
this.container.classList.add(STATES.visible);
|
||||
this.container.classList.remove(STATES.minimized);
|
||||
}
|
||||
|
||||
minimizeMenu() {
|
||||
this.state = 'minimized';
|
||||
this.el.classList.add(CLS.minimized);
|
||||
this.container.classList.add(STATES.minimized);
|
||||
}
|
||||
|
||||
hideMenu() {
|
||||
this.state = 'hidden';
|
||||
this.el.classList.remove(CLS.visible);
|
||||
this.container.setAttribute('aria-expanded', true);
|
||||
this.container.classList.remove(STATES.visible);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
114
js/menu.spec.js
Normal file
114
js/menu.spec.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import Menu from './menu';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
const menuHtml = `
|
||||
<button class="Menu">
|
||||
<span class="Menu-line"></span>
|
||||
<span class="Menu-line"></span>
|
||||
<span class="Menu-line"></span>
|
||||
<span class="Menu-text">Open menu</span>
|
||||
</button>`;
|
||||
|
||||
describe('Menu button', () => {
|
||||
let menu;
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = menuHtml;
|
||||
menu = new Menu({
|
||||
container: document.querySelector('.Menu'),
|
||||
});
|
||||
});
|
||||
|
||||
it('Should initialise', () => {
|
||||
menu.init();
|
||||
expect(menu).toMatchObject({});
|
||||
});
|
||||
|
||||
it('Should fail to initialise if container is missing', () => {
|
||||
menu.init();
|
||||
expect((new Menu()).state).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should default to visible state', () => {
|
||||
expect(menu.state).toBe('visible');
|
||||
});
|
||||
|
||||
it('Should be hidden initially', () => {
|
||||
menu.init();
|
||||
expect(menu.container.classList.contains('is-visible')).toBe(false);
|
||||
});
|
||||
|
||||
it('Should be visible after 1 second', () => {
|
||||
menu.init();
|
||||
jest.runAllTimers();
|
||||
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
|
||||
expect(menu.container.classList.contains('is-visible')).toBe(true);
|
||||
});
|
||||
|
||||
test('Menu can be shown', () => {
|
||||
menu.init();
|
||||
menu.showMenu();
|
||||
expect(menu.state).toBe('visible');
|
||||
expect(menu.container.classList.contains('is-visible')).toBe(true);
|
||||
expect(menu.container.classList.contains('is-minimized')).toBe(false);
|
||||
});
|
||||
|
||||
test('Menu can be hidden', () => {
|
||||
menu.init();
|
||||
menu.hideMenu();
|
||||
expect(menu.state).toBe('hidden');
|
||||
expect(menu.container.classList.contains('is-visible')).toBe(false);
|
||||
expect(menu.container.classList.contains('is-minimized')).toBe(false);
|
||||
});
|
||||
|
||||
test('Menu can be minimized', () => {
|
||||
menu.init();
|
||||
menu.minimizeMenu();
|
||||
expect(menu.state).toBe('minimized');
|
||||
expect(menu.container.classList.contains('is-visible')).toBe(false);
|
||||
expect(menu.container.classList.contains('is-minimized')).toBe(true);
|
||||
});
|
||||
|
||||
test('The state should not change on scroll if it is hidden', () => {
|
||||
menu.init();
|
||||
menu.hideMenu();
|
||||
menu.updateState(10, 0);
|
||||
expect(menu.state).toBe('hidden');
|
||||
menu.updateState(0, 10);
|
||||
expect(menu.state).toBe('hidden');
|
||||
});
|
||||
|
||||
test('The state should change to minimized when scrolling down', () => {
|
||||
menu.init();
|
||||
menu.showMenu();
|
||||
menu.updateState(0, 10);
|
||||
expect(menu.state).toBe('minimized');
|
||||
});
|
||||
|
||||
test('The state should change to visible when scrolling up', () => {
|
||||
menu.init();
|
||||
menu.minimizeMenu();
|
||||
menu.updateState(10, 0);
|
||||
expect(menu.state).toBe('visible');
|
||||
});
|
||||
|
||||
it('Should have initial ARIA attributes', () => {
|
||||
menu.init();
|
||||
expect(menu.container.getAttribute('aria-haspopup')).toBe('true');
|
||||
expect(menu.container.getAttribute('aria-expanded')).toBe('false');
|
||||
});
|
||||
|
||||
it('Should have updated ARIA attribute when menu is opened', () => {
|
||||
menu.init();
|
||||
menu.showMenu();
|
||||
expect(menu.container.getAttribute('aria-expanded')).toBe('false');
|
||||
});
|
||||
|
||||
it('Should have updated ARIA attribute when menu is closed', () => {
|
||||
menu.init();
|
||||
menu.showMenu();
|
||||
menu.hideMenu();
|
||||
expect(menu.container.getAttribute('aria-expanded')).toBe('true');
|
||||
});
|
||||
});
|
||||
31
js/nav.js
31
js/nav.js
@@ -1,31 +0,0 @@
|
||||
const CLS = {
|
||||
open: 'is-open',
|
||||
};
|
||||
|
||||
export default class Nav {
|
||||
constructor(cfg) {
|
||||
this.el = cfg.el;
|
||||
this.menu = cfg.menu;
|
||||
|
||||
this.attachEvents();
|
||||
}
|
||||
|
||||
attachEvents() {
|
||||
this.menu.el.addEventListener('click', () => {
|
||||
this.menu.hideMenu();
|
||||
this.openNav();
|
||||
});
|
||||
this.el.querySelector('.Nav-close').addEventListener('click', () => {
|
||||
this.menu.showMenu();
|
||||
this.closeNav();
|
||||
});
|
||||
}
|
||||
|
||||
openNav() {
|
||||
this.el.classList.add(CLS.open);
|
||||
}
|
||||
|
||||
closeNav() {
|
||||
this.el.classList.remove(CLS.open);
|
||||
}
|
||||
}
|
||||
49
js/navigation.js
Normal file
49
js/navigation.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const STATES = {
|
||||
open: 'is-open',
|
||||
};
|
||||
|
||||
export default class Navigation {
|
||||
constructor(cfg = {}) {
|
||||
if (!cfg.container) return false;
|
||||
if (!cfg.menu) return false;
|
||||
this.state = 'hidden';
|
||||
this.container = cfg.container;
|
||||
this.menu = cfg.menu;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.container.setAttribute('aria-hidden', true);
|
||||
this.container.querySelector('.Nav-list').setAttribute('role', 'menubar');
|
||||
this.container.querySelectorAll('.Nav-listItem').forEach((item) => {
|
||||
// Override implied listitem role
|
||||
item.setAttribute('role', 'none');
|
||||
});
|
||||
this.container.querySelectorAll('.Nav-link').forEach((link) => {
|
||||
link.setAttribute('role', 'menuitem');
|
||||
});
|
||||
this.attachEvents();
|
||||
}
|
||||
|
||||
attachEvents() {
|
||||
this.menu.container.addEventListener('click', () => {
|
||||
this.openNav();
|
||||
});
|
||||
this.container.querySelector('.Nav-close').addEventListener('click', () => {
|
||||
this.closeNav();
|
||||
});
|
||||
}
|
||||
|
||||
openNav() {
|
||||
this.state = 'visible';
|
||||
this.menu.hideMenu();
|
||||
this.container.classList.add(STATES.open);
|
||||
this.container.setAttribute('aria-hidden', false);
|
||||
}
|
||||
|
||||
closeNav() {
|
||||
this.state = 'hidden';
|
||||
this.menu.showMenu();
|
||||
this.container.classList.remove(STATES.open);
|
||||
this.container.setAttribute('aria-hidden', true);
|
||||
}
|
||||
}
|
||||
96
js/navigation.spec.js
Normal file
96
js/navigation.spec.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import Navigation from './navigation';
|
||||
|
||||
const navHtml = `<nav class="Nav">
|
||||
<button class="Menu">Menu</button>
|
||||
<div class="Nav-content">
|
||||
<h2 class="Nav-title">WindyCOYS</h2>
|
||||
<ul class="Nav-list">
|
||||
<li class="Nav-listItem">
|
||||
<a class="Nav-link" href="/">Home</a>
|
||||
</li>
|
||||
<li class="Nav-listItem">
|
||||
<a class="Nav-link" href="/about">About</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="Nav-close">close</button>
|
||||
</nav>`;
|
||||
|
||||
let nav;
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = navHtml;
|
||||
// Mock Menu
|
||||
const menu = {
|
||||
container: document.querySelector('.Menu'),
|
||||
showMenu: jest.fn(),
|
||||
hideMenu: jest.fn(),
|
||||
};
|
||||
nav = new Navigation({
|
||||
container: document.querySelector('.Nav'),
|
||||
menu,
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('Should initialise', () => {
|
||||
nav.init();
|
||||
expect(nav).toMatchObject({});
|
||||
});
|
||||
|
||||
it('Should fail to initialise if container is missing', () => {
|
||||
nav.init();
|
||||
expect((new Navigation()).state).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should be hidden by default', () => {
|
||||
nav.init();
|
||||
expect(nav.container.classList.contains('is-open')).toBe(false);
|
||||
});
|
||||
|
||||
it('Should have initial ARIA attributes', () => {
|
||||
nav.init();
|
||||
expect(nav.container.getAttribute('aria-hidden')).toBe('true');
|
||||
});
|
||||
|
||||
it('Should update the ARIA attributes when visible', () => {
|
||||
nav.init();
|
||||
nav.openNav();
|
||||
expect(nav.container.getAttribute('aria-hidden')).toBe('false');
|
||||
});
|
||||
|
||||
it('Should update the ARIA attributes when hidden', () => {
|
||||
nav.init();
|
||||
nav.openNav();
|
||||
nav.closeNav();
|
||||
expect(nav.container.getAttribute('aria-hidden')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation list', () => {
|
||||
it('Should have initial ARIA attributes', () => {
|
||||
nav.init();
|
||||
const ul = nav.container.querySelector('.Nav-list');
|
||||
expect(ul.getAttribute('role')).toBe('menubar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation list items', () => {
|
||||
it('Should have initial ARIA attributes', () => {
|
||||
nav.init();
|
||||
const lis = nav.container.querySelectorAll('.Nav-listItem');
|
||||
lis.forEach((li) => {
|
||||
expect(li.getAttribute('role')).toBe('none');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation links', () => {
|
||||
it('Should have initial ARIA attributes', () => {
|
||||
nav.init();
|
||||
const links = nav.container.querySelectorAll('.Nav-link');
|
||||
links.forEach((link) => {
|
||||
expect(link.getAttribute('role')).toBe('menuitem');
|
||||
});
|
||||
});
|
||||
});
|
||||
16
js/windy.js
Normal file
16
js/windy.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import Menu from './menu';
|
||||
import Navigation from './navigation';
|
||||
|
||||
const windy = {};
|
||||
|
||||
export default function init() {
|
||||
windy.menu = new Menu({
|
||||
container: document.querySelector('.Menu'),
|
||||
});
|
||||
windy.navigation = new Navigation({
|
||||
container: document.querySelector('.Nav'),
|
||||
menu: windy.menu,
|
||||
});
|
||||
windy.menu.init();
|
||||
windy.navigation.init();
|
||||
}
|
||||
Reference in New Issue
Block a user