First draft of integration tests
This commit is contained in:
parent
b387f66c69
commit
318f5c3c34
25 changed files with 514 additions and 196 deletions
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
@ -0,0 +1 @@
|
|||
8.9.4
|
37
.travis.yml
Normal file
37
.travis.yml
Normal file
|
@ -0,0 +1,37 @@
|
|||
language: node_js
|
||||
addons:
|
||||
firefox: "58.0"
|
||||
env:
|
||||
global:
|
||||
- GOLANG_VERSION=1.9
|
||||
- GOPATH=$HOME/gopath
|
||||
- REPO_ROOT=$GOPATH/src
|
||||
- PATH=$PATH:/$HOME/bin:$GOPATH/bin
|
||||
|
||||
before_install:
|
||||
- ./interfacer/contrib/setup_go.sh
|
||||
- cd $REPO_ROOT/webext
|
||||
install:
|
||||
- npm run get-gobindata
|
||||
- npm install
|
||||
- npm run build
|
||||
script:
|
||||
- npm test
|
||||
after_failure:
|
||||
- cat $REPO_ROOT/interfacer/debug.log
|
||||
- cat $REPO_ROOT/interfacer/spec.log
|
||||
deploy:
|
||||
- provider: releases
|
||||
api_key:
|
||||
secure: <your key>
|
||||
file:
|
||||
- linux
|
||||
- linux.sha
|
||||
- os
|
||||
- os.sha
|
||||
- windows
|
||||
- windows.sha
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
|
150
README.md
150
README.md
|
@ -1,136 +1,40 @@
|
|||
# Texttop
|
||||
**A fully interactive X Linux desktop rendered to TTY and streamed over SSH**
|
||||
# Browsh
|
||||
**A fully interactive, realtime and modern browser rendered to TTY**
|
||||
|
||||
or Firefox in your terminal 😲
|
||||
|
||||
![Alt Text](https://i.imgur.com/jX3vhO4.gif)
|
||||
|
||||
This [Youtube video](https://www.youtube.com/watch?v=TE_D_fx_ut8) gives a more faithful rendition of the experience.
|
||||
|
||||
## Why?
|
||||
I'm travelling around the world and sometimes I don't have very good Internet. If all I have is a 3kbps connection
|
||||
tethered from my phone then it's good to SSH into my server and browse the web through [elinks](http://www.xteddy.org/elinks/).
|
||||
That way my _server_ downloads the web pages and uses the limited bandwidth of my SSH connection to display the result. But
|
||||
it lacks JS support and all that other modern HTML5 goodness. Texttop is simply a way to have the power of a remote
|
||||
server running a desktop, but interfaced through the simplicity of a terminal and very low bandwidth.
|
||||
I'm travelling around the world and sometimes I don't have very good
|
||||
Internet. If all I have is a 3kbps connection tethered from my phone
|
||||
then it's good to SSH into my server and browse the web through
|
||||
[elinks](http://www.xteddy.org/elinks/). That way my _server_ downloads
|
||||
the web pages and uses the limited bandwidth of my SSH connection to
|
||||
display the result. But it lacks JS support and all that other modern
|
||||
HTML5 goodness. Browsh is simply a way to have the power of a remote
|
||||
server running a desktop, but interfaced through the simplicity of a
|
||||
terminal and very low bandwidth.
|
||||
|
||||
Why not VNC? Well VNC is certainly one solution but it doesn't quite have the same ability to deal with extremely bad
|
||||
Internet. Texttop uses MoSH to further reduce the bandwidth and stability requirements of the connection. Mosh offers features like
|
||||
automatic reconnection of dropped connections and diff-only screen updates. Also, other than SSH or MoSH, Texttop doesn't
|
||||
require a client like VNC. But of course another big reason for Texttop is that it's just very cool geekery.
|
||||
|
||||
## Quickstart
|
||||
If you just want to have a play on your local machine:
|
||||
```
|
||||
docker run --rm -it tombh/texttop sh
|
||||
./run.sh
|
||||
```
|
||||
Why not VNC? Well VNC is certainly one solution but it doesn't quite
|
||||
have the same ability to deal with extremely bad Internet. Browsh can
|
||||
use MoSH to further reduce the bandwidth and stability requirements
|
||||
of the connection. Mosh offers features like automatic reconnection
|
||||
of dropped connections and diff-only screen updates. Also, other than
|
||||
SSH or MoSH, Browsh doesn't require a client like VNC. But of course
|
||||
another big reason for Browsh is that it's just very cool geekery.
|
||||
|
||||
## Installation
|
||||
You can either pull from the Docker Registry:
|
||||
`docker pull tombh/texttop`
|
||||
or, build yourself:
|
||||
```
|
||||
git clone https://github.com/tombh/texttop.git
|
||||
cd texttop
|
||||
docker build -t texttop .
|
||||
```
|
||||
The docker image is only ~275MB.
|
||||
|
||||
Download a [](release). You will need to have Firefox 57+ aleady
|
||||
installed.
|
||||
|
||||
Or download and run the Docker image with
|
||||
`docker run -it tombh/browsh`
|
||||
|
||||
## Usage
|
||||
On your remote server (this will pull the docker image the first time you issue it):
|
||||
```
|
||||
docker run -d \
|
||||
-p 7777:7777 -p 60000-60020:60000-60020/udp \
|
||||
-v ${HOME}/.ssh:/root/.ssh:ro \
|
||||
tombh/texttop
|
||||
```
|
||||
Note that this assumes you already have SSH setup on your server and that you have your public key there. Password
|
||||
logins work fine too. The `60000-60020` port range is for MoSH.
|
||||
Most keys and mouse gestures should work as you'd expect on a desktop
|
||||
browser.
|
||||
|
||||
Then on your local machine:
|
||||
```
|
||||
mosh user@yourserver:7777
|
||||
./run.sh
|
||||
```
|
||||
MoSH is available through most system package managers. SSH can be used exactly the same, just replace `mosh` with `ssh`.
|
||||
|
||||
`user@yourserver` is the normal URI you would use to connect via SSH.
|
||||
|
||||
**Exiting**
|
||||
`CTRL+ALT+Q` will drop you back to the docker container's CLI. You can start again with `./run.sh`
|
||||
|
||||
If MoSH or SSH become unresponsive you can exit MoSH with `CTRL+^ .` or SSH with `ENTER ~ .`
|
||||
|
||||
## Interaction
|
||||
* `CTRL + mousewheel` to zoom
|
||||
* `CTRL + click/drag` to pan
|
||||
|
||||
Most mouse and keyboard input is exactly the same as a normal desktop. If your terminal is active then you can click,
|
||||
type, scroll, use arrow keys and drag things around. However there are still some things not available, like copy and
|
||||
paste. The main difference from a normal desktop is that you can zoom and pan the desktop by using `CTRL + mousewheel` and
|
||||
`CTRL + drag`. This is very handy as it's hard to see what's what when you're zoomed right out.
|
||||
|
||||
### Keyboard Mode
|
||||
If your terminal doesn't support mouse input then you can switch in and out of keyboard mode with `CTRL+ALT+M`.
|
||||
This will give you the following shortcuts:
|
||||
|
||||
`u` mouse up
|
||||
`n` mouse down
|
||||
`h` mouse left
|
||||
`k` mouse right
|
||||
|
||||
`SHIFT+u` pan up
|
||||
`SHIFT+n` pan down
|
||||
`SHIFT+h` pan left
|
||||
`SHIFT+k` pan right
|
||||
|
||||
`CTRL+u` zoom in
|
||||
`CTRL+n` zoom out
|
||||
|
||||
`j` left-click
|
||||
`r` right-click
|
||||
`t` middle-click
|
||||
|
||||
### Adding new applications
|
||||
Currently, only Firefox is installed on this extremely minimal Alpine Linux distro. However you can add new packages
|
||||
with [apk](https://wiki.alpinelinux.org/wiki/Alpine_Linux_package_management). Example;
|
||||
```
|
||||
# Login with a seperate session
|
||||
apk --no-cache add xterm
|
||||
export DISPLAY=:0
|
||||
xterm &
|
||||
```
|
||||
Just remember that you will lose any system changes once you restart the docker container. I'm thinking about ways to
|
||||
save state. You may experiment with mounting certain system directories. Eg;
|
||||
```
|
||||
docker run -d \
|
||||
-p 7777:7777 -p 60000-60020:60000-60020/udp \
|
||||
-v ${HOME}/.ssh:/root/.ssh:ro \
|
||||
-v ${HOME}/.texttop/var:/var \
|
||||
tombh/texttop
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
The Docker Hub version is built against Intel CPU architectures, this causes hiptext to fail on AMD chips. In which
|
||||
case you will need to build texttop yourself:
|
||||
```
|
||||
git clone https://github.com/tombh/texttop.git
|
||||
cd texttop
|
||||
docker build -t texttop .
|
||||
```
|
||||
|
||||
**Working terminals**
|
||||
* [Tilda](https://github.com/lanoxx/tilda)
|
||||
* [Terminal](https://launchpad.net/pantheon-terminal)
|
||||
|
||||
**Problematic terminals**
|
||||
* konsole: neither `CTRL+click/drag` nor `CTRL+mousewheel` are forwarded (perhaps mouse reporting is disabled by default)
|
||||
* xterm: `CTRL+click/drag` is intercepted by the GUI menu
|
||||
* rxvt: rendering issues
|
||||
|
||||
## Contributions
|
||||
Yes please.
|
||||
`CTRL+l` Focus URL bar
|
||||
|
||||
## License
|
||||
GNU General Public License v3.0
|
||||
|
|
22
interfacer/contrib/setup_go.sh
Executable file
22
interfacer/contrib/setup_go.sh
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/bin/bash
|
||||
GOLANG_DEP_VERSION=0.3.2
|
||||
dep_url=https://github.com/golang/dep/releases/download/v$GOLANG_DEP_VERSION/dep-linux-amd64
|
||||
golang_archive=go$GOLANG_VERSION.linux-amd64.tar.gz
|
||||
golang_url=https://dl.google.com/go/$golang_archive
|
||||
|
||||
mkdir -p $HOME/bin
|
||||
mkdir -p $GOPATH/bin
|
||||
|
||||
# Install Golang
|
||||
curl -L -o $golang_archive $golang_url
|
||||
tar -C $HOME/bin -xzf $golang_archive
|
||||
|
||||
# Install `dep` the current defacto dependency for Golang
|
||||
curl -L -o $GOPATH/bin/dep $dep_url
|
||||
chmod +x $GOPATH/bin/dep
|
||||
|
||||
cp -rfp $TRAVIS_BUILD_DIR -T $REPO_ROOT
|
||||
|
||||
cd $REPO_ROOT/interfacer
|
||||
dep ensure
|
||||
|
|
@ -134,7 +134,7 @@ func webSocketReader(ws *websocket.Conn) {
|
|||
command := parts[0]
|
||||
if command == "/frame" {
|
||||
termbox.SetCursor(0, 0)
|
||||
os.Stdout.Write([]byte(strings.Join(parts[1:], ",")))
|
||||
os.Stdout.Write([]byte(parts[1]))
|
||||
termbox.HideCursor()
|
||||
termbox.Flush()
|
||||
} else {
|
||||
|
|
|
@ -9,12 +9,10 @@ set -e
|
|||
|
||||
PROJECT_ROOT=$(git rev-parse --show-toplevel)
|
||||
|
||||
webext=$PROJECT_ROOT/node_modules/.bin/web-ext
|
||||
NODE_BIN=$PROJECT_ROOT/webext/node_modules/.bin
|
||||
|
||||
cd $PROJECT_ROOT/webext
|
||||
$PROJECT_ROOT/node_modules/.bin/webpack
|
||||
cd $PROJECT_ROOT/webext/dist
|
||||
$webext build --overwrite-dest
|
||||
cd $PROJECT_ROOT/webext && $NODE_BIN/webpack
|
||||
cd $PROJECT_ROOT/webext/dist && $NODE_BIN/web-ext build --overwrite-dest
|
||||
|
||||
# Get the current version of Browsh
|
||||
version=$(cat $PROJECT_ROOT/webext/manifest.json | python2 -c \
|
||||
|
|
3
webext/contrib/firefoxheadless.sh
Executable file
3
webext/contrib/firefoxheadless.sh
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
FIREFOX_BIN=${FIREFOX:-firefox}
|
||||
$FIREFOX_BIN --headless "$@"
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/bash
|
||||
firefox-beta --headless "$@" &
|
||||
pid=$!
|
||||
trap "kill ${pid}; exit 1" INT
|
||||
wait
|
133
webext/package-lock.json
generated
133
webext/package-lock.json
generated
|
@ -324,6 +324,15 @@
|
|||
"is-fullwidth-code-point": "1.0.0",
|
||||
"strip-ansi": "3.0.1"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "2.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1625,6 +1634,17 @@
|
|||
"has-ansi": "2.0.0",
|
||||
"strip-ansi": "3.0.1",
|
||||
"supports-color": "2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "2.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"character-entities": {
|
||||
|
@ -1801,6 +1821,17 @@
|
|||
"requires": {
|
||||
"strip-ansi": "3.0.1",
|
||||
"wcwidth": "1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "2.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"combined-stream": {
|
||||
|
@ -2939,6 +2970,17 @@
|
|||
"string-width": "1.0.2",
|
||||
"strip-ansi": "3.0.1",
|
||||
"through": "2.3.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "2.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"onetime": {
|
||||
|
@ -2999,6 +3041,17 @@
|
|||
"code-point-at": "1.1.0",
|
||||
"is-fullwidth-code-point": "1.0.0",
|
||||
"strip-ansi": "3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "2.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
|
@ -6584,6 +6637,30 @@
|
|||
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
|
||||
"dev": true
|
||||
},
|
||||
"pty.js": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pty.js/-/pty.js-0.3.1.tgz",
|
||||
"integrity": "sha1-gfW+0zLW5eeraFaI0boDc0ENUbU=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"extend": "1.2.1",
|
||||
"nan": "2.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"extend": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz",
|
||||
"integrity": "sha1-oPX9bPyDpf5J72mNYOyKYk3UV2w=",
|
||||
"dev": true
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz",
|
||||
"integrity": "sha1-gioNwmYpDOTNOhIoLKPn42Rmigg=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"public-encrypt": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz",
|
||||
|
@ -7759,12 +7836,20 @@
|
|||
"dev": true
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
|
||||
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "2.1.1"
|
||||
"ansi-regex": "3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
|
||||
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"strip-bom": {
|
||||
|
@ -8472,6 +8557,17 @@
|
|||
"string-width": "1.0.2",
|
||||
"strip-ansi": "3.0.1",
|
||||
"wrap-ansi": "2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "2.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"find-up": {
|
||||
|
@ -8592,6 +8688,17 @@
|
|||
"code-point-at": "1.1.0",
|
||||
"is-fullwidth-code-point": "1.0.0",
|
||||
"strip-ansi": "3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "2.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"strip-bom": {
|
||||
|
@ -8801,6 +8908,15 @@
|
|||
"is-fullwidth-code-point": "1.0.0",
|
||||
"strip-ansi": "3.0.1"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "2.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -8924,6 +9040,15 @@
|
|||
"is-fullwidth-code-point": "1.0.0",
|
||||
"strip-ansi": "3.0.1"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "2.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
{
|
||||
"scripts": {
|
||||
"get-gobindata": "go get -u gopkg.in/shuLhan/go-bindata.v3/...",
|
||||
"build": "./contrib/bundle_webextension.sh",
|
||||
"test": "NODE_PATH=src:test mocha"
|
||||
"unitish": "NODE_PATH=src:test mocha test/unitish",
|
||||
"integration": "NODE_PATH=src:test mocha test/integration",
|
||||
"test": "npm run unitish && npm run integration"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
|
@ -17,7 +20,9 @@
|
|||
"copy-webpack-plugin": "^4.3.1",
|
||||
"eslint": "^4.15.0",
|
||||
"mocha": "^4.1.0",
|
||||
"pty.js": "^0.3.1",
|
||||
"sinon": "^4.1.3",
|
||||
"strip-ansi": "^4.0.0",
|
||||
"web-ext": "^2.3.1",
|
||||
"webpack": "^3.10.0"
|
||||
},
|
||||
|
|
|
@ -9,6 +9,7 @@ export default class TextBuillder extends BaseBuilder {
|
|||
super();
|
||||
this.graphics_builder = frame_builder.graphics_builder;
|
||||
this.frame_builder = frame_builder;
|
||||
this._parse_started_elements = [];
|
||||
}
|
||||
|
||||
getFormattedText() {
|
||||
|
|
14
webext/test/integration/basic_spec.js
Normal file
14
webext/test/integration/basic_spec.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import helper from 'integration/helper'
|
||||
import {expect} from 'chai';
|
||||
|
||||
describe('Basic', function () {
|
||||
this.retries(3);
|
||||
|
||||
it('basic', (done) => {
|
||||
helper.getPage('https://www.google.com', (page) => {
|
||||
expect(page.title).to.eq('Google');
|
||||
expect(page.url).to.eq('https://www.google.com/');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
165
webext/test/integration/helper.js
Normal file
165
webext/test/integration/helper.js
Normal file
|
@ -0,0 +1,165 @@
|
|||
import fs from 'fs';
|
||||
import pty from 'pty.js';
|
||||
import child from 'child_process';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
|
||||
before((done) => {
|
||||
helper.boot(done);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
helper.shutdown();
|
||||
});
|
||||
|
||||
class Helper {
|
||||
constructor () {
|
||||
this.frame = '';
|
||||
this.tty_width = 70;
|
||||
this.tty_height = 30;
|
||||
this.is_last_startup_message_consumed = false;
|
||||
this.browserFingerprint = ' ← | x | ';
|
||||
this.project_root = child.execSync('git rev-parse --show-toplevel').toString().trim();
|
||||
}
|
||||
|
||||
log(message) {
|
||||
const log_file = this.project_root + '/interfacer/spec.log';
|
||||
message = stripAnsi(message);
|
||||
fs.appendFileSync(log_file, message);
|
||||
}
|
||||
|
||||
boot(callback) {
|
||||
// Race condition is avoided because Firefox "should" consistently take longer
|
||||
// to start than the CLI
|
||||
this.startBrowshPTY();
|
||||
this.startFirefox();
|
||||
this.consumeStartupOutput(callback);
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
this.stopWatching = true;
|
||||
this.browshPTY.destroy();
|
||||
this.firefoxPTY.destroy();
|
||||
}
|
||||
|
||||
startBrowshPTY() {
|
||||
const dir = this.project_root + '/interfacer';
|
||||
this.browshPTY = pty.spawn('bash', [], {
|
||||
cols: this.tty_width,
|
||||
rows: this.tty_height,
|
||||
env: process.env
|
||||
});
|
||||
this.browshPTY.write(`cd ${dir} \r`);
|
||||
this.browshPTY.write(`go run *.go -use-existing-ff \r`);
|
||||
this.broadcastOutput();
|
||||
}
|
||||
|
||||
broadcastOutput() {
|
||||
let buffer = '';
|
||||
this.browshPTY.on('data', (data) => {
|
||||
if (this.is_last_startup_message_consumed) {
|
||||
buffer += data;
|
||||
buffer = this.broadcastBrowserOutput(buffer);
|
||||
} else {
|
||||
this.log(data);
|
||||
this.frame = this.cleanFrame(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// pty.js sends chunks, so we need to wait for a particular signature before
|
||||
// we have a whole browser frame with which we can work with.
|
||||
broadcastBrowserOutput(buffer) {
|
||||
const cursor_reset_sig = '\u001b[1;1H';
|
||||
if (buffer.includes(cursor_reset_sig)) {
|
||||
buffer = this.cleanFrame(buffer);
|
||||
this.frame = this.insertTTYLines(buffer);
|
||||
buffer = '';
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// TODO: Handle wide UTF8 chars in the same way the app does
|
||||
insertTTYLines(buffer) {
|
||||
let split = '';
|
||||
for (var i = 0; i < buffer.length; i++) {
|
||||
if (((i + 1) % this.tty_width) === 0) {
|
||||
split += buffer[i] + '\n';
|
||||
} else {
|
||||
split += buffer[i];
|
||||
}
|
||||
}
|
||||
return split;
|
||||
}
|
||||
|
||||
// Currently we're just converting the browser output into pure alphanumerical
|
||||
// text, ie no colour information.
|
||||
cleanFrame(buffer) {
|
||||
buffer = stripAnsi(buffer);
|
||||
buffer = buffer.replace(/▄/g, ' ');
|
||||
buffer = buffer.trim();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// Wait for the given string to appear anywhere in the entire frame, whether in
|
||||
// the UI or the webpage.
|
||||
watchOutputFor(match, callback) {
|
||||
const interval = setInterval(() => {
|
||||
const regex = new RegExp(match, 'g');
|
||||
if (this.stopWatching) clearInterval(interval);
|
||||
if (regex.test(this.frame)) {
|
||||
clearInterval(interval);
|
||||
callback(this.frame);
|
||||
}
|
||||
}, 5);
|
||||
}
|
||||
|
||||
getPage(url, done) {
|
||||
const signature = this.browserFingerprint + url;
|
||||
this.watchOutputFor(signature, (frame) => {
|
||||
done(this.buildPageObject(frame));
|
||||
});
|
||||
}
|
||||
|
||||
buildPageObject(frame) {
|
||||
let frame_lines = [];
|
||||
for(let line of frame.split(/\r?\n/)) {
|
||||
line = line.replace(/\s+$/, ''); // Right trim
|
||||
frame_lines.push(line);
|
||||
}
|
||||
return {
|
||||
title: frame_lines[0],
|
||||
url: frame_lines[1].replace(this.browserFingerprint, ''),
|
||||
body: frame_lines.slice(2)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait until the penultimate message before actual webpage content is shown
|
||||
consumeStartupOutput(done) {
|
||||
this.watchOutputFor('Waiting for a Firefox instance to connect', (_) => {
|
||||
this.is_last_startup_message_consumed = true;
|
||||
this.frame = '';
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
// Firefox doesn't actually need a PTY, but seeing as we're using pty.js already
|
||||
// then may as well keep consistent.
|
||||
startFirefox() {
|
||||
const dir = this.project_root + '/webext/dist';
|
||||
this.firefoxPTY = pty.spawn('bash', [], {
|
||||
env: process.env
|
||||
});
|
||||
this.firefoxPTY.write(`cd ${dir} \r`);
|
||||
let command = `../node_modules/.bin/web-ext run ` +
|
||||
`--firefox="${this.project_root}/webext/contrib/firefoxheadless.sh" ` +
|
||||
`--url https://google.com` +
|
||||
`\r`;
|
||||
this.firefoxPTY.write(command);
|
||||
this.firefoxPTY.on('data', (data) => {
|
||||
this.log(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let helper = new Helper();
|
||||
export default helper;
|
|
@ -1 +1,2 @@
|
|||
--require babel-register
|
||||
--timeout 15000
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
import sandbox from 'helper';
|
||||
import {expect} from 'chai';
|
||||
|
||||
import FrameBuilder from 'dom/frame_builder';
|
||||
import TextBuilder from 'dom/text_builder';
|
||||
import GraphicsBuilder from 'dom/graphics_builder';
|
||||
import text_nodes from 'fixtures/text_nodes';
|
||||
import {with_text, without_text, scaled} from 'fixtures/canvas_pixels';
|
||||
|
||||
let text_builder;
|
||||
|
||||
// To save us hand-writing large pixel arrays, let's just have an unrealistically
|
||||
// small window, it's not a problem, because we'll never actually have to view real
|
||||
// webpages on it.
|
||||
window.innerWidth = 3;
|
||||
window.innerHeight = 4;
|
||||
|
||||
function setup() {
|
||||
let frame_builder = new FrameBuilder();
|
||||
frame_builder.tty_width = 3
|
||||
frame_builder.tty_height = 2
|
||||
frame_builder.char_width = 1
|
||||
frame_builder.char_height = 2
|
||||
frame_builder.graphics_builder.getSnapshotWithText();
|
||||
frame_builder.graphics_builder.getSnapshotWithoutText();
|
||||
frame_builder.graphics_builder.getScaledSnapshot();
|
||||
text_builder = new TextBuilder(frame_builder);
|
||||
}
|
||||
|
||||
describe('Text Builder', ()=> {
|
||||
beforeEach(()=> {
|
||||
let getPixelsStub = sandbox.stub(GraphicsBuilder.prototype, '_getPixelData');
|
||||
getPixelsStub.onCall(0).returns(with_text);
|
||||
getPixelsStub.onCall(1).returns(without_text);
|
||||
getPixelsStub.onCall(2).returns(scaled);
|
||||
setup();
|
||||
text_builder.text_nodes = text_nodes;
|
||||
});
|
||||
|
||||
it('should convert text nodes to a grid', ()=> {
|
||||
text_builder._positionTextNodes();
|
||||
expect(text_builder.tty_grid[0]).to.deep.equal(['t', [255, 255, 255], [0, 0, 0]]);
|
||||
expect(text_builder.tty_grid[1]).to.deep.equal(['e', [255, 255, 255], [111, 111, 111]]);
|
||||
expect(text_builder.tty_grid[2]).to.deep.equal(['s', [255, 255, 255], [0, 0, 0]]);
|
||||
expect(text_builder.tty_grid[3]).to.be.undefined;
|
||||
expect(text_builder.tty_grid[4]).to.be.undefined;
|
||||
expect(text_builder.tty_grid[5]).to.be.undefined;
|
||||
});
|
||||
});
|
|
@ -1,11 +1,11 @@
|
|||
import sandbox from 'helper';
|
||||
import sandbox from 'unitish/helper';
|
||||
import {expect} from 'chai';
|
||||
|
||||
import FrameBuilder from 'dom/frame_builder';
|
||||
import GraphicsBuilder from 'dom/graphics_builder';
|
||||
import TextBuilder from 'dom/text_builder';
|
||||
import canvas_pixels from 'fixtures/canvas_pixels';
|
||||
import text_grid from 'fixtures/text_grid';
|
||||
import canvas_pixels from 'unitish/fixtures/canvas_pixels';
|
||||
import text_grid from 'unitish/fixtures/text_grid';
|
||||
|
||||
describe('Frame Builder', ()=> {
|
||||
let frame_builder;
|
||||
|
@ -18,13 +18,13 @@ describe('Frame Builder', ()=> {
|
|||
|
||||
it('should merge pixels and text into ANSI true colour syntax', ()=> {
|
||||
frame_builder.tty_width = 3;
|
||||
frame_builder.tty_height = 2;
|
||||
frame_builder.tty_height = 2 + 2;
|
||||
frame_builder.sendFrame();
|
||||
const frame = frame_builder.frame.replace(/\u001b\[/g, 'ESC');
|
||||
const frame = frame_builder.frame.join('').replace(/\u001b\[/g, 'ESC');
|
||||
expect(frame).to.eq(
|
||||
'ESC38;2;0;0;0mESC48;2;111;111;111m▄' +
|
||||
'ESC38;2;111;111;111mESC48;2;222;222;222m😐' +
|
||||
'ESC38;2;0;0;0mESC48;2;111;111;111m▄\n' +
|
||||
'ESC38;2;0;0;0mESC48;2;111;111;111m▄' +
|
||||
'ESC38;2;111;111;111mESC48;2;222;222;222m😄' +
|
||||
'ESC38;2;111;111;111mESC48;2;0;0;0m▄' +
|
||||
'ESC38;2;111;111;111mESC48;2;222;222;222m😂'
|
|
@ -2,7 +2,7 @@ import sinon from 'sinon';
|
|||
|
||||
import GraphicsBuilder from 'dom/graphics_builder';
|
||||
import FrameBuilder from 'dom/frame_builder';
|
||||
import MockRange from 'mocks/range'
|
||||
import MockRange from 'unitish/mocks/range'
|
||||
|
||||
var sandbox = sinon.sandbox.create();
|
||||
|
||||
|
@ -21,6 +21,11 @@ afterEach(() => {
|
|||
global.document = {
|
||||
addEventListener: () => {},
|
||||
getElementById: () => {},
|
||||
getElementsByTagName: () => {
|
||||
return [{
|
||||
innerHTML: 'Google'
|
||||
}]
|
||||
},
|
||||
createRange: () => {
|
||||
return new MockRange()
|
||||
},
|
||||
|
@ -28,13 +33,16 @@ global.document = {
|
|||
return {
|
||||
getContext: () => {}
|
||||
}
|
||||
},
|
||||
location: {
|
||||
href: 'https://www.google.com'
|
||||
}
|
||||
};
|
||||
|
||||
global.DEVELOPMENT = false;
|
||||
global.PRODUCTION = false;
|
||||
global.TEST = true;
|
||||
global.window = {};
|
||||
global.window = global.document;
|
||||
global.performance = {
|
||||
now: () => {}
|
||||
}
|
88
webext/test/unitish/text_builder_spec.js
Normal file
88
webext/test/unitish/text_builder_spec.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
import sandbox from 'unitish/helper';
|
||||
import {
|
||||
expect
|
||||
} from 'chai';
|
||||
|
||||
import FrameBuilder from 'dom/frame_builder';
|
||||
import TextBuilder from 'dom/text_builder';
|
||||
import GraphicsBuilder from 'dom/graphics_builder';
|
||||
import text_nodes from 'unitish/fixtures/text_nodes';
|
||||
import {
|
||||
with_text,
|
||||
without_text,
|
||||
scaled
|
||||
} from 'unitish/fixtures/canvas_pixels';
|
||||
|
||||
let text_builder;
|
||||
|
||||
// To save us hand-writing large pixel arrays, let's just have an unrealistically
|
||||
// small window, it's not a problem, because we'll never actually have to view real
|
||||
// webpages on it.
|
||||
window.innerWidth = 3;
|
||||
window.innerHeight = 4;
|
||||
|
||||
function setup() {
|
||||
let frame_builder = new FrameBuilder();
|
||||
frame_builder.tty_width = 3
|
||||
frame_builder.tty_height = 2 + 2
|
||||
frame_builder.char_width = 1
|
||||
frame_builder.char_height = 2
|
||||
frame_builder.graphics_builder.getSnapshotWithText();
|
||||
frame_builder.graphics_builder.getSnapshotWithoutText();
|
||||
frame_builder.graphics_builder.getScaledSnapshot();
|
||||
text_builder = new TextBuilder(frame_builder);
|
||||
}
|
||||
|
||||
describe('Text Builder', () => {
|
||||
beforeEach(() => {
|
||||
let getPixelsStub = sandbox.stub(GraphicsBuilder.prototype, '_getPixelData');
|
||||
getPixelsStub.onCall(0).returns(with_text);
|
||||
getPixelsStub.onCall(1).returns(without_text);
|
||||
getPixelsStub.onCall(2).returns(scaled);
|
||||
setup();
|
||||
text_builder.text_nodes = text_nodes;
|
||||
});
|
||||
|
||||
it('should convert text nodes to a grid', () => {
|
||||
text_builder._updateState();
|
||||
text_builder._positionTextNodes();
|
||||
const grid = text_builder.tty_grid;
|
||||
expect(grid[0]).to.deep.equal([
|
||||
't', [255, 255, 255],
|
||||
[0, 0, 0],
|
||||
{
|
||||
"style": {
|
||||
"textAlign": "left"
|
||||
}
|
||||
}, {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
]);
|
||||
expect(grid[1]).to.deep.equal([
|
||||
'e', [255, 255, 255],
|
||||
[111, 111, 111], {
|
||||
"style": {
|
||||
"textAlign": "left"
|
||||
}
|
||||
}, {
|
||||
"x": 1,
|
||||
"y": 0
|
||||
}
|
||||
]);
|
||||
expect(grid[2]).to.deep.equal([
|
||||
's', [255, 255, 255],
|
||||
[0, 0, 0], {
|
||||
"style": {
|
||||
"textAlign": "left"
|
||||
}
|
||||
}, {
|
||||
"x": 2,
|
||||
"y": 0
|
||||
}
|
||||
]);
|
||||
expect(grid[3]).to.be.undefined;
|
||||
expect(grid[4]).to.be.undefined;
|
||||
expect(grid[5]).to.be.undefined;
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue