First draft of integration tests

This commit is contained in:
Thomas Buckley-Houston 2018-01-23 12:28:56 +08:00
parent b387f66c69
commit 318f5c3c34
25 changed files with 514 additions and 196 deletions

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
8.9.4

37
.travis.yml Normal file
View 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
View file

@ -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
View 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

View file

@ -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 {

View file

@ -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 \

View file

@ -0,0 +1,3 @@
#!/bin/bash
FIREFOX_BIN=${FIREFOX:-firefox}
$FIREFOX_BIN --headless "$@"

View file

@ -1,5 +0,0 @@
#!/bin/bash
firefox-beta --headless "$@" &
pid=$!
trap "kill ${pid}; exit 1" INT
wait

133
webext/package-lock.json generated
View file

@ -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"
}
}
}
}

View file

@ -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"
},

View file

@ -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() {

View 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();
});
});
});

View 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;

View file

@ -1 +1,2 @@
--require babel-register
--timeout 15000

View file

@ -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;
});
});

View file

@ -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😂'

View file

@ -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: () => {}
}

View 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;
});
});