commit
33a97a0e8c
23 changed files with 462 additions and 240 deletions
@ -0,0 +1,12 @@ |
|||||||
|
import settingReducer from 'settings/reducers/setting'; |
||||||
|
|
||||||
|
// Make setting reducer instead of re-use
|
||||||
|
const defaultState = { |
||||||
|
setting: settingReducer(undefined, {}), |
||||||
|
}; |
||||||
|
|
||||||
|
export default function reducer(state = defaultState, action = {}) { |
||||||
|
return Object.assign({}, state, { |
||||||
|
setting: settingReducer(state.setting, action), |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,169 @@ |
|||||||
|
import messages from 'shared/messages'; |
||||||
|
import Hint from './hint'; |
||||||
|
|
||||||
|
const TARGET_SELECTOR = [ |
||||||
|
'a', 'button', 'input', 'textarea', |
||||||
|
'[contenteditable=true]', '[contenteditable=""]' |
||||||
|
].join(','); |
||||||
|
|
||||||
|
const inViewport = (win, element, viewSize, framePosition) => { |
||||||
|
let { |
||||||
|
top, left, bottom, right |
||||||
|
} = element.getBoundingClientRect(); |
||||||
|
let doc = win.doc; |
||||||
|
let frameWidth = win.innerWidth || doc.documentElement.clientWidth; |
||||||
|
let frameHeight = win.innerHeight || doc.documentElement.clientHeight; |
||||||
|
|
||||||
|
if (right < 0 || bottom < 0 || top > frameHeight || left > frameWidth) { |
||||||
|
// out of frame
|
||||||
|
return false; |
||||||
|
} |
||||||
|
if (right + framePosition.x < 0 || bottom + framePosition.y < 0 || |
||||||
|
left + framePosition.x > viewSize.width || |
||||||
|
top + framePosition.y > viewSize.height) { |
||||||
|
// out of viewport
|
||||||
|
return false; |
||||||
|
} |
||||||
|
return true; |
||||||
|
}; |
||||||
|
|
||||||
|
export default class Follow { |
||||||
|
constructor(win, store) { |
||||||
|
this.win = win; |
||||||
|
this.store = store; |
||||||
|
this.newTab = false; |
||||||
|
this.hints = {}; |
||||||
|
this.targets = []; |
||||||
|
} |
||||||
|
|
||||||
|
update() { |
||||||
|
} |
||||||
|
|
||||||
|
key(key) { |
||||||
|
if (Object.keys(this.hints).length === 0) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
this.win.parent.postMessage(JSON.stringify({ |
||||||
|
type: messages.FOLLOW_KEY_PRESS, |
||||||
|
key, |
||||||
|
}), '*'); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
openLink(element) { |
||||||
|
if (!this.newTab) { |
||||||
|
element.click(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let href = element.getAttribute('href'); |
||||||
|
|
||||||
|
// eslint-disable-next-line no-script-url
|
||||||
|
if (!href || href === '#' || href.toLowerCase().startsWith('javascript:')) { |
||||||
|
return; |
||||||
|
} |
||||||
|
return browser.runtime.sendMessage({ |
||||||
|
type: messages.OPEN_URL, |
||||||
|
url: element.href, |
||||||
|
newTab: this.newTab, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
countHints(sender, viewSize, framePosition) { |
||||||
|
this.targets = Follow.getTargetElements(this.win, viewSize, framePosition); |
||||||
|
sender.postMessage(JSON.stringify({ |
||||||
|
type: messages.FOLLOW_RESPONSE_COUNT_TARGETS, |
||||||
|
count: this.targets.length, |
||||||
|
}), '*'); |
||||||
|
} |
||||||
|
|
||||||
|
createHints(keysArray, newTab) { |
||||||
|
if (keysArray.length !== this.targets.length) { |
||||||
|
throw new Error('illegal hint count'); |
||||||
|
} |
||||||
|
|
||||||
|
this.newTab = newTab; |
||||||
|
this.hints = {}; |
||||||
|
for (let i = 0; i < keysArray.length; ++i) { |
||||||
|
let keys = keysArray[i]; |
||||||
|
let hint = new Hint(this.targets[i], keys); |
||||||
|
this.hints[keys] = hint; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
showHints(keys) { |
||||||
|
Object.keys(this.hints).filter(key => key.startsWith(keys)) |
||||||
|
.forEach(key => this.hints[key].show()); |
||||||
|
Object.keys(this.hints).filter(key => !key.startsWith(keys)) |
||||||
|
.forEach(key => this.hints[key].hide()); |
||||||
|
} |
||||||
|
|
||||||
|
removeHints() { |
||||||
|
Object.keys(this.hints).forEach((key) => { |
||||||
|
this.hints[key].remove(); |
||||||
|
}); |
||||||
|
this.hints = {}; |
||||||
|
this.targets = []; |
||||||
|
} |
||||||
|
|
||||||
|
activateHints(keys) { |
||||||
|
let hint = this.hints[keys]; |
||||||
|
if (!hint) { |
||||||
|
return; |
||||||
|
} |
||||||
|
let element = hint.target; |
||||||
|
switch (element.tagName.toLowerCase()) { |
||||||
|
case 'a': |
||||||
|
return this.openLink(element, this.newTab); |
||||||
|
case 'input': |
||||||
|
switch (element.type) { |
||||||
|
case 'file': |
||||||
|
case 'checkbox': |
||||||
|
case 'radio': |
||||||
|
case 'submit': |
||||||
|
case 'reset': |
||||||
|
case 'button': |
||||||
|
case 'image': |
||||||
|
case 'color': |
||||||
|
return element.click(); |
||||||
|
default: |
||||||
|
return element.focus(); |
||||||
|
} |
||||||
|
case 'textarea': |
||||||
|
return element.focus(); |
||||||
|
case 'button': |
||||||
|
return element.click(); |
||||||
|
default: |
||||||
|
// it may contenteditable
|
||||||
|
return element.focus(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
onMessage(message, sender) { |
||||||
|
switch (message.type) { |
||||||
|
case messages.FOLLOW_REQUEST_COUNT_TARGETS: |
||||||
|
return this.countHints(sender, message.viewSize, message.framePosition); |
||||||
|
case messages.FOLLOW_CREATE_HINTS: |
||||||
|
return this.createHints(message.keysArray, message.newTab); |
||||||
|
case messages.FOLLOW_SHOW_HINTS: |
||||||
|
return this.showHints(message.keys); |
||||||
|
case messages.FOLLOW_ACTIVATE: |
||||||
|
return this.activateHints(message.keys); |
||||||
|
case messages.FOLLOW_REMOVE_HINTS: |
||||||
|
return this.removeHints(message.keys); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static getTargetElements(win, viewSize, framePosition) { |
||||||
|
let all = win.document.querySelectorAll(TARGET_SELECTOR); |
||||||
|
let filtered = Array.prototype.filter.call(all, (element) => { |
||||||
|
let style = win.getComputedStyle(element); |
||||||
|
return style.display !== 'none' && |
||||||
|
style.visibility !== 'hidden' && |
||||||
|
element.type !== 'hidden' && |
||||||
|
element.offsetHeight > 0 && |
||||||
|
inViewport(win, element, viewSize, framePosition); |
||||||
|
}); |
||||||
|
return filtered; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,45 @@ |
|||||||
|
import InputComponent from './input'; |
||||||
|
import KeymapperComponent from './keymapper'; |
||||||
|
import FollowComponent from './follow'; |
||||||
|
import * as inputActions from 'content/actions/input'; |
||||||
|
import messages from 'shared/messages'; |
||||||
|
|
||||||
|
export default class Common { |
||||||
|
constructor(win, store) { |
||||||
|
const follow = new FollowComponent(win, store); |
||||||
|
const input = new InputComponent(win.document.body, store); |
||||||
|
const keymapper = new KeymapperComponent(store); |
||||||
|
|
||||||
|
input.onKey((key, ctrl) => follow.key(key, ctrl)); |
||||||
|
input.onKey((key, ctrl) => keymapper.key(key, ctrl)); |
||||||
|
|
||||||
|
this.store = store; |
||||||
|
this.children = [ |
||||||
|
follow, |
||||||
|
input, |
||||||
|
keymapper, |
||||||
|
]; |
||||||
|
|
||||||
|
this.reloadSettings(); |
||||||
|
} |
||||||
|
|
||||||
|
update() { |
||||||
|
this.children.forEach(c => c.update()); |
||||||
|
} |
||||||
|
|
||||||
|
onMessage(message, sender) { |
||||||
|
switch (message) { |
||||||
|
case messages.SETTINGS_CHANGED: |
||||||
|
this.reloadSettings(); |
||||||
|
} |
||||||
|
this.children.forEach(c => c.onMessage(message, sender)); |
||||||
|
} |
||||||
|
|
||||||
|
reloadSettings() { |
||||||
|
browser.runtime.sendMessage({ |
||||||
|
type: messages.SETTINGS_QUERY, |
||||||
|
}).then((settings) => { |
||||||
|
this.store.dispatch(inputActions.setKeymaps(settings.keymaps)); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -1,175 +0,0 @@ |
|||||||
import * as followActions from 'content/actions/follow'; |
|
||||||
import messages from 'shared/messages'; |
|
||||||
import Hint from 'content/hint'; |
|
||||||
import HintKeyProducer from 'content/hint-key-producer'; |
|
||||||
|
|
||||||
const DEFAULT_HINT_CHARSET = 'abcdefghijklmnopqrstuvwxyz'; |
|
||||||
const TARGET_SELECTOR = [ |
|
||||||
'a', 'button', 'input', 'textarea', |
|
||||||
'[contenteditable=true]', '[contenteditable=""]' |
|
||||||
].join(','); |
|
||||||
|
|
||||||
const inWindow = (window, element) => { |
|
||||||
let { |
|
||||||
top, left, bottom, right |
|
||||||
} = element.getBoundingClientRect(); |
|
||||||
return ( |
|
||||||
top >= 0 && left >= 0 && |
|
||||||
bottom <= (window.innerHeight || document.documentElement.clientHeight) && |
|
||||||
right <= (window.innerWidth || document.documentElement.clientWidth) |
|
||||||
); |
|
||||||
}; |
|
||||||
|
|
||||||
export default class FollowComponent { |
|
||||||
constructor(wrapper, store) { |
|
||||||
this.wrapper = wrapper; |
|
||||||
this.store = store; |
|
||||||
this.hintElements = {}; |
|
||||||
this.state = {}; |
|
||||||
} |
|
||||||
|
|
||||||
update() { |
|
||||||
let prevState = this.state; |
|
||||||
this.state = this.store.getState().follow; |
|
||||||
if (!prevState.enabled && this.state.enabled) { |
|
||||||
this.create(); |
|
||||||
} else if (prevState.enabled && !this.state.enabled) { |
|
||||||
this.remove(); |
|
||||||
} else if (prevState.keys !== this.state.keys) { |
|
||||||
this.updateHints(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
key(key) { |
|
||||||
if (!this.state.enabled) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
switch (key) { |
|
||||||
case 'Enter': |
|
||||||
this.activate(this.hintElements[this.state.keys].target); |
|
||||||
return; |
|
||||||
case 'Escape': |
|
||||||
this.store.dispatch(followActions.disable()); |
|
||||||
return; |
|
||||||
case 'Backspace': |
|
||||||
case 'Delete': |
|
||||||
this.store.dispatch(followActions.backspace()); |
|
||||||
break; |
|
||||||
default: |
|
||||||
if (DEFAULT_HINT_CHARSET.includes(key)) { |
|
||||||
this.store.dispatch(followActions.keyPress(key)); |
|
||||||
} |
|
||||||
break; |
|
||||||
} |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
updateHints() { |
|
||||||
let keys = this.state.keys; |
|
||||||
let shown = Object.keys(this.hintElements).filter((key) => { |
|
||||||
return key.startsWith(keys); |
|
||||||
}); |
|
||||||
let hidden = Object.keys(this.hintElements).filter((key) => { |
|
||||||
return !key.startsWith(keys); |
|
||||||
}); |
|
||||||
if (shown.length === 0) { |
|
||||||
this.remove(); |
|
||||||
return; |
|
||||||
} else if (shown.length === 1) { |
|
||||||
this.activate(this.hintElements[keys].target); |
|
||||||
this.store.dispatch(followActions.disable()); |
|
||||||
} |
|
||||||
|
|
||||||
shown.forEach((key) => { |
|
||||||
this.hintElements[key].show(); |
|
||||||
}); |
|
||||||
hidden.forEach((key) => { |
|
||||||
this.hintElements[key].hide(); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
activate(element) { |
|
||||||
switch (element.tagName.toLowerCase()) { |
|
||||||
case 'a': |
|
||||||
if (this.state.newTab) { |
|
||||||
// getAttribute() to avoid to resolve absolute path
|
|
||||||
let href = element.getAttribute('href'); |
|
||||||
|
|
||||||
// eslint-disable-next-line no-script-url
|
|
||||||
if (!href || href === '#' || href.startsWith('javascript:')) { |
|
||||||
return; |
|
||||||
} |
|
||||||
return browser.runtime.sendMessage({ |
|
||||||
type: messages.OPEN_URL, |
|
||||||
url: element.href, |
|
||||||
newTab: this.state.newTab, |
|
||||||
}); |
|
||||||
} |
|
||||||
if (element.href.startsWith('http://') || |
|
||||||
element.href.startsWith('https://') || |
|
||||||
element.href.startsWith('ftp://')) { |
|
||||||
return browser.runtime.sendMessage({ |
|
||||||
type: messages.OPEN_URL, |
|
||||||
url: element.href, |
|
||||||
newTab: this.state.newTab, |
|
||||||
}); |
|
||||||
} |
|
||||||
return element.click(); |
|
||||||
case 'input': |
|
||||||
switch (element.type) { |
|
||||||
case 'file': |
|
||||||
case 'checkbox': |
|
||||||
case 'radio': |
|
||||||
case 'submit': |
|
||||||
case 'reset': |
|
||||||
case 'button': |
|
||||||
case 'image': |
|
||||||
case 'color': |
|
||||||
return element.click(); |
|
||||||
default: |
|
||||||
return element.focus(); |
|
||||||
} |
|
||||||
case 'textarea': |
|
||||||
return element.focus(); |
|
||||||
case 'button': |
|
||||||
return element.click(); |
|
||||||
default: |
|
||||||
// it may contenteditable
|
|
||||||
return element.focus(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
create() { |
|
||||||
let doc = this.wrapper.ownerDocument; |
|
||||||
let elements = FollowComponent.getTargetElements(doc); |
|
||||||
let producer = new HintKeyProducer(DEFAULT_HINT_CHARSET); |
|
||||||
let hintElements = {}; |
|
||||||
Array.prototype.forEach.call(elements, (ele) => { |
|
||||||
let keys = producer.produce(); |
|
||||||
let hint = new Hint(ele, keys); |
|
||||||
hintElements[keys] = hint; |
|
||||||
}); |
|
||||||
this.hintElements = hintElements; |
|
||||||
} |
|
||||||
|
|
||||||
remove() { |
|
||||||
let hintElements = this.hintElements; |
|
||||||
Object.keys(this.hintElements).forEach((key) => { |
|
||||||
hintElements[key].remove(); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
static getTargetElements(doc) { |
|
||||||
let all = doc.querySelectorAll(TARGET_SELECTOR); |
|
||||||
let filtered = Array.prototype.filter.call(all, (element) => { |
|
||||||
let style = window.getComputedStyle(element); |
|
||||||
return style.display !== 'none' && |
|
||||||
style.visibility !== 'hidden' && |
|
||||||
element.type !== 'hidden' && |
|
||||||
element.offsetHeight > 0 && |
|
||||||
inWindow(window, element); |
|
||||||
}); |
|
||||||
return filtered; |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,16 @@ |
|||||||
|
import CommonComponent from './common'; |
||||||
|
|
||||||
|
export default class FrameContent { |
||||||
|
|
||||||
|
constructor(win, store) { |
||||||
|
this.children = [new CommonComponent(win, store)]; |
||||||
|
} |
||||||
|
|
||||||
|
update() { |
||||||
|
this.children.forEach(c => c.update()); |
||||||
|
} |
||||||
|
|
||||||
|
onMessage(message, sender) { |
||||||
|
this.children.forEach(c => c.onMessage(message, sender)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,131 @@ |
|||||||
|
import * as followActions from 'content/actions/follow'; |
||||||
|
import messages from 'shared/messages'; |
||||||
|
import HintKeyProducer from 'content/hint-key-producer'; |
||||||
|
|
||||||
|
const DEFAULT_HINT_CHARSET = 'abcdefghijklmnopqrstuvwxyz'; |
||||||
|
|
||||||
|
const broadcastMessage = (win, message) => { |
||||||
|
let json = JSON.stringify(message); |
||||||
|
let frames = [window.self].concat(Array.from(window.frames)); |
||||||
|
frames.forEach(frame => frame.postMessage(json, '*')); |
||||||
|
}; |
||||||
|
|
||||||
|
export default class FollowController { |
||||||
|
constructor(win, store) { |
||||||
|
this.win = win; |
||||||
|
this.store = store; |
||||||
|
this.state = {}; |
||||||
|
this.keys = []; |
||||||
|
this.producer = null; |
||||||
|
} |
||||||
|
|
||||||
|
onMessage(message, sender) { |
||||||
|
switch (message.type) { |
||||||
|
case messages.FOLLOW_START: |
||||||
|
return this.store.dispatch(followActions.enable(message.newTab)); |
||||||
|
case messages.FOLLOW_RESPONSE_COUNT_TARGETS: |
||||||
|
return this.create(message.count, sender); |
||||||
|
case messages.FOLLOW_KEY_PRESS: |
||||||
|
return this.keyPress(message.key); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
update() { |
||||||
|
let prevState = this.state; |
||||||
|
this.state = this.store.getState().follow; |
||||||
|
|
||||||
|
if (!prevState.enabled && this.state.enabled) { |
||||||
|
this.count(); |
||||||
|
} else if (prevState.enabled && !this.state.enabled) { |
||||||
|
this.remove(); |
||||||
|
} else if (prevState.keys !== this.state.keys) { |
||||||
|
this.updateHints(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
updateHints() { |
||||||
|
let shown = this.keys.filter(key => key.startsWith(this.state.keys)); |
||||||
|
if (shown.length === 1) { |
||||||
|
this.activate(); |
||||||
|
this.store.dispatch(followActions.disable()); |
||||||
|
} |
||||||
|
|
||||||
|
broadcastMessage(this.win, { |
||||||
|
type: messages.FOLLOW_SHOW_HINTS, |
||||||
|
keys: this.state.keys, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
activate() { |
||||||
|
broadcastMessage(this.win, { |
||||||
|
type: messages.FOLLOW_ACTIVATE, |
||||||
|
keys: this.state.keys, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
keyPress(key) { |
||||||
|
switch (key) { |
||||||
|
case 'Enter': |
||||||
|
this.activate(); |
||||||
|
this.store.dispatch(followActions.disable()); |
||||||
|
break; |
||||||
|
case 'Escape': |
||||||
|
this.store.dispatch(followActions.disable()); |
||||||
|
break; |
||||||
|
case 'Backspace': |
||||||
|
case 'Delete': |
||||||
|
this.store.dispatch(followActions.backspace()); |
||||||
|
break; |
||||||
|
default: |
||||||
|
if (DEFAULT_HINT_CHARSET.includes(key)) { |
||||||
|
this.store.dispatch(followActions.keyPress(key)); |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
count() { |
||||||
|
this.producer = new HintKeyProducer(DEFAULT_HINT_CHARSET); |
||||||
|
let doc = this.win.document; |
||||||
|
let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth; |
||||||
|
let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight; |
||||||
|
let frameElements = this.win.document.querySelectorAll('frame,iframe'); |
||||||
|
|
||||||
|
this.win.postMessage(JSON.stringify({ |
||||||
|
type: messages.FOLLOW_REQUEST_COUNT_TARGETS, |
||||||
|
viewSize: { width: viewWidth, height: viewHeight }, |
||||||
|
framePosition: { x: 0, y: 0 }, |
||||||
|
}), '*'); |
||||||
|
frameElements.forEach((element) => { |
||||||
|
let { left: frameX, top: frameY } = element.getBoundingClientRect(); |
||||||
|
let message = JSON.stringify({ |
||||||
|
type: messages.FOLLOW_REQUEST_COUNT_TARGETS, |
||||||
|
viewSize: { width: viewWidth, height: viewHeight }, |
||||||
|
framePosition: { x: frameX, y: frameY }, |
||||||
|
}); |
||||||
|
element.contentWindow.postMessage(message, '*'); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
create(count, sender) { |
||||||
|
let produced = []; |
||||||
|
for (let i = 0; i < count; ++i) { |
||||||
|
produced.push(this.producer.produce()); |
||||||
|
} |
||||||
|
this.keys = this.keys.concat(produced); |
||||||
|
|
||||||
|
sender.postMessage(JSON.stringify({ |
||||||
|
type: messages.FOLLOW_CREATE_HINTS, |
||||||
|
keysArray: produced, |
||||||
|
newTab: this.state.newTab, |
||||||
|
}), '*'); |
||||||
|
} |
||||||
|
|
||||||
|
remove() { |
||||||
|
this.keys = []; |
||||||
|
broadcastMessage(this.win, { |
||||||
|
type: messages.FOLLOW_REMOVE_HINTS, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
import CommonComponent from '../common'; |
||||||
|
import FollowController from './follow-controller'; |
||||||
|
import * as consoleFrames from '../../console-frames'; |
||||||
|
import messages from 'shared/messages'; |
||||||
|
|
||||||
|
export default class TopContent { |
||||||
|
|
||||||
|
constructor(win, store) { |
||||||
|
this.win = win; |
||||||
|
this.children = [ |
||||||
|
new CommonComponent(win, store), |
||||||
|
new FollowController(win, store), |
||||||
|
]; |
||||||
|
|
||||||
|
// TODO make component
|
||||||
|
consoleFrames.initialize(window.document); |
||||||
|
} |
||||||
|
|
||||||
|
update() { |
||||||
|
this.children.forEach(c => c.update()); |
||||||
|
} |
||||||
|
|
||||||
|
onMessage(message, sender) { |
||||||
|
switch (message.type) { |
||||||
|
case messages.CONSOLE_HIDE_COMMAND: |
||||||
|
this.win.focus(); |
||||||
|
consoleFrames.blur(window.document); |
||||||
|
return Promise.resolve(); |
||||||
|
} |
||||||
|
this.children.forEach(c => c.onMessage(message, sender)); |
||||||
|
} |
||||||
|
} |
@ -1,14 +1,17 @@ |
|||||||
import { expect } from "chai"; |
import { expect } from "chai"; |
||||||
import FollowComponent from 'content/components/follow'; |
import FollowComponent from 'content/components/common/follow'; |
||||||
|
|
||||||
describe('FollowComponent', () => { |
describe('FollowComponent', () => { |
||||||
describe('#getTargetElements', () => { |
describe('#getTargetElements', () => { |
||||||
beforeEach(() => { |
beforeEach(() => { |
||||||
document.body.innerHTML = __html__['test/content/components/follow.html']; |
document.body.innerHTML = __html__['test/content/components/common/follow.html']; |
||||||
}); |
}); |
||||||
|
|
||||||
it('returns visible links', () => { |
it('returns visible links', () => { |
||||||
let targets = FollowComponent.getTargetElements(window.document); |
let targets = FollowComponent.getTargetElements( |
||||||
|
window, |
||||||
|
{ width: window.innerWidth, height: window.innerHeight }, |
||||||
|
{ x: 0, y: 0 }); |
||||||
expect(targets).to.have.lengthOf(3); |
expect(targets).to.have.lengthOf(3); |
||||||
|
|
||||||
let ids = Array.prototype.map.call(targets, (e) => e.id); |
let ids = Array.prototype.map.call(targets, (e) => e.id); |
@ -1,9 +1,9 @@ |
|||||||
import { expect } from "chai"; |
import { expect } from "chai"; |
||||||
import Hint from 'content/hint'; |
import Hint from 'content/components/common/hint'; |
||||||
|
|
||||||
describe('Hint class', () => { |
describe('Hint class', () => { |
||||||
beforeEach(() => { |
beforeEach(() => { |
||||||
document.body.innerHTML = __html__['test/content/hint.html']; |
document.body.innerHTML = __html__['test/content/components/common/hint.html']; |
||||||
}); |
}); |
||||||
|
|
||||||
describe('#constructor', () => { |
describe('#constructor', () => { |
Reference in new issue