first commit

This commit is contained in:
编码猿
2024-09-27 01:01:17 +08:00
commit 14b8ae24fa
198 changed files with 22193 additions and 0 deletions

View File

@@ -0,0 +1 @@
4eca1fdb-0cf9-45cc-8990-38c4f95d5c6a

View File

@@ -0,0 +1,20 @@
var test = require('tape');
var WebSocket = require('ws');
var server = new WebSocket.Server({ port: 8890 });
var sharedSocket = require('../../src/SharedSocket');
var url = 'ws://localhost:8890';
test('subscribing listeners', (t) => {
sharedSocket.onMessage((message) => {
t.equal(message, 'yay');
sharedSocket.close();
server.close(() => t.end());
});
sharedSocket.open(url);
server.on('connection', (ws) => {
ws.send('yay');
});
});

View File

@@ -0,0 +1,82 @@
const test = require('tape');
const path = require('path');
const fs = require('fs');
const WidgetBundler = require('../../src/WidgetBundler.js');
const fixturePath = path.resolve(__dirname, '../test_widgets');
const bundler = WidgetBundler();
var callback = () => {};
test('bundling widgets', (t) => {
const action = {
type: 'added',
filePath: path.join(fixturePath, 'widget-1.coffee'),
id: 'widget-1',
};
callback = (event) => {
t.equal(event.type, 'added', 'it emits an "added" event');
t.equal(typeof event.widget, 'object', 'it emits a widget object');
t.equal(event.widget.id, 'widget-1', 'the widget object has an id');
t.equal(
event.widget.filePath,
action.filePath,
'the widget object contains the original file path',
);
t.equal(
typeof event.widget.body,
'string',
'it also contains a string with the widget source code',
);
const widget = eval(event.widget.body)('widget-1');
t.equal(
widget.command,
'foo',
'the source evals to a require function which returns the widget by id',
);
callback = () => {};
t.end();
};
bundler.push(action, (event) => callback(event));
});
test('watching widgets', (t) => {
callback = (event) => {
t.equal(event.type, 'added', 'it emits another "added" event');
t.equal(event.widget.id, 'widget-1', 'for the correct widget');
t.equal(typeof event.widget.body, 'string', 'with the widget source code');
t.end();
};
fs.utimes(
path.join(fixturePath, 'widget-1.coffee'),
Date.now(),
Date.now(),
() => {},
);
});
test('removing widgets', (t) => {
const action = {
type: 'removed',
filePath: path.join(fixturePath, 'widget-1.coffee'),
id: 'widget-1',
};
callback = (event) => {
t.equal(event.type, 'removed', 'it emits a "removed" event');
t.equal(event.id, 'widget-1', 'for the correct widget');
callback = () => {};
t.end();
};
bundler.push(action, (event) => callback(event));
});
test('closing', (t) => {
bundler.close();
t.pass('it closes');
t.end();
});

View File

@@ -0,0 +1,78 @@
const test = require('tape');
const path = require('path');
const bundleWidget = require('../../src/bundleWidget');
const testDir = path.resolve(__dirname, path.join('..', 'test_widgets'));
test('bundling coffeescript widgets', (t) => {
const widgetPath = path.join(testDir, 'widget-1.coffee');
const bundle = bundleWidget('widget-id', widgetPath);
t.plan(2);
t.ok(
bundle.constructor.name === 'Browserify',
'it returns a browserify bundle',
);
bundle.bundle((err, src) => {
t.ok(
!err && src && src.indexOf('command') > -1,
'the source code it generates looks ok',
);
bundle.close();
});
});
test('bundling javascript widgets', (t) => {
const widgetPath = path.join(testDir, 'widget-2.js');
const bundle = bundleWidget('other-widget-id', widgetPath);
t.plan(2);
t.ok(
bundle.constructor.name === 'Browserify',
'it returns a browserify bundle',
);
bundle.bundle((err, src) => {
t.ok(
!err && src && src.indexOf('command') > -1,
'the source code looks ok',
);
bundle.close();
});
});
test('bundling jsx widgets', (t) => {
const widgetPath = path.join(testDir, 'widget-3.jsx');
const bundle = bundleWidget('widget-3', widgetPath);
t.plan(2);
t.ok(
bundle.constructor.name === 'Browserify',
'it returns a browserify bundle',
);
bundle.bundle((err, src) => {
t.ok(
!err && src && src.indexOf('command') > -1,
'the source code looks ok',
);
bundle.close();
});
});
test('bundling widgets with syntax errors', (t) => {
const widgetPath = path.join(testDir, 'broken-widget.coffee');
const bundle = bundleWidget('broken', widgetPath);
t.plan(2);
t.ok(
bundle.constructor.name === 'Browserify',
'it returns a browserify bundle',
);
bundle.bundle((err, src) => {
t.equal(
err && err.message,
'unexpected indentation while parsing file: ' + widgetPath,
'it spits out an error when bundling',
);
bundle.close();
});
});

View File

@@ -0,0 +1,94 @@
var test = require('tape');
var connect = require('connect');
var path = require('path');
var httpGet = require('../helpers/httpGet');
var httpPost = require('../helpers/httpPost');
var commandServer = require('../../src/command_server.coffee');
var workingDir = path.resolve(__dirname, path.join('..', 'test_widgets'));
var server = connect().use(commandServer(workingDir)).listen(8887);
var url = 'http://localhost:8887/run/';
test('responding to POST /run/', (t) => {
t.plan(3);
httpPost(url, 'echo', (res) => {
t.equal(res.statusCode, 200, 'it reponds');
});
httpPost('http://localhost:8887/foo/', 'echo', (res) => {
t.equal(res.statusCode, 404, 'it ignores requests to other paths');
});
httpGet(url, (res) => {
t.equal(res.statusCode, 404, 'it ignores GET requests');
});
});
test('running commands', (t) => {
t.plan(2);
httpPost(url, 'echo "yay"', (res, body) => {
t.equal(body, 'yay\n', 'it runs commands');
});
httpPost(url, 'pwd', (res, body) => {
t.equal(
body,
workingDir + '\n',
'it runs commands in the supplied working dir',
);
});
});
test('shell type', (t) => {
httpPost(url, 'echo $(shopt | grep login_shell)', (res, body) => {
t.equal(body, 'login_shell off\n', 'it is not a login shell');
t.end();
});
});
test('running broken commands', (t) => {
t.plan(2);
httpPost(url, 'fake-command', (res, body) => {
t.equal(res.statusCode, 500, 'it responds with a 500 code');
t.equal(
body,
'bash: line 1: fake-command: command not found\n',
'it responds with an error message',
);
});
});
test('forwarding stderr', (t) => {
t.plan(2);
httpPost(url, 'echo "yay" >&2', (res, body) => {
t.equal(res.statusCode, 500, 'it responds with a 500 code');
t.equal(body, 'yay\n', 'it sends stderr along');
});
});
test('closing', (t) => {
server.close();
t.pass('it closes');
t.end();
});
test('using a login shell', (t) => {
server = connect().use(commandServer(workingDir, true)).listen(8887);
httpPost(url, 'echo $(shopt | grep login_shell)', (res, body) => {
const lines = body.trim().split('\n');
t.equal(
lines[lines.length - 1],
'login_shell on',
'it indeed runs in a login shell',
);
server.close();
t.end();
});
});

View File

@@ -0,0 +1,116 @@
var test = require('tape');
var path = require('path');
var fs = require('fs');
var execSync = require('child_process').execSync;
var DirWatcher = require('../../src/directory_watcher.coffee');
var fixturePath = path.resolve(__dirname, '../test_widgets');
var newWidgetPath = path.join(fixturePath, 'new-widget.coffee');
var stopWatching;
var callback;
const throwError = (err) => {
if (err) throw err;
};
test('files that are already present in the widget dir', (t) => {
t.timeoutAfter(300);
var expectedWidgets = [
path.join(fixturePath, 'widget-1.coffee'),
path.join(fixturePath, 'widget-2.js'),
path.join(fixturePath, 'some-dir.widget', 'index-1.coffee'),
path.join(fixturePath, 'broken-widget.coffee'),
path.join(fixturePath, 'invalid-widget.coffee'),
];
callback = (event) => {
if (event.type !== 'added') {
return;
}
var idx = expectedWidgets.indexOf(event.filePath);
if (idx > -1) {
expectedWidgets.splice(idx, 1);
}
if (expectedWidgets.length === 0) {
callback = () => {};
t.pass('it emits an event for all widgets already in the folder');
t.end();
}
};
stopWatching = DirWatcher(fixturePath, (event) => callback(event));
});
test('adding files', (t) => {
t.timeoutAfter(300);
callback = (event) => {
if (event.type === 'added' && event.filePath === newWidgetPath) {
callback = () => {};
t.pass('it emits an event for new files');
t.equal(event.rootPath, fixturePath, 'the event includes the root path');
t.end();
}
};
fs.writeFile(newWidgetPath, "command: ''", throwError);
});
test('removing files', (t) => {
t.timeoutAfter(300);
callback = (event) => {
if (event.type === 'removed' && event.filePath === newWidgetPath) {
callback = () => {};
t.pass('it emits a removed event when a widget file is removed');
t.equal(event.rootPath, fixturePath, 'the event includes the root path');
t.end();
}
};
fs.unlink(newWidgetPath, throwError);
});
test('adding folders', (t) => {
t.timeoutAfter(300);
var aWidgetFolder = path.resolve(__dirname, '../tmp2');
if (fs.existsSync(aWidgetFolder)) {
execSync('rm -rf ' + aWidgetFolder);
}
fs.mkdirSync(aWidgetFolder);
fs.writeFileSync(path.join(aWidgetFolder, 'widget.js'), "command: 'yay'");
var expectedPath = path.join(fixturePath, 'another', 'widget.js');
callback = (event) => {
if (event.type === 'added' && event.filePath === expectedPath) {
callback = () => {};
t.pass('it emits an event when a subfolder containing a widget is added');
t.end();
}
};
fs.rename(aWidgetFolder, path.join(fixturePath, 'another'), throwError);
});
test('removing folders', (t) => {
t.timeoutAfter(300);
var expectedPath = path.join(fixturePath, 'another', 'widget.js');
callback = (event) => {
if (event.type === 'removed' && event.filePath === expectedPath) {
callback = () => {};
t.pass(
'it emits a removed event when a subfolder containing a ' +
'widget is removed',
);
t.end();
}
};
var newPath = path.resolve(__dirname, '../tmp3');
fs.renameSync(path.join(fixturePath, 'another'), newPath);
execSync('rm -rf ' + newPath);
});
test('stopping', (t) => {
stopWatching();
t.pass('it can be stopped');
t.end();
});

View File

@@ -0,0 +1,33 @@
var test = require('tape');
var WebSocket = require('ws');
var server = new WebSocket.Server({ port: 8888 });
var url = 'ws://localhost:8888';
var sharedSocket = require('../../src/SharedSocket');
var dispatch = require('../../src/dispatch');
test('queuing up messages', (t) => {
expectedMessages = ['a', 'b'];
server.on('connection', (ws) => {
ws.on('message', (message) => {
parsed = JSON.parse(message);
var idx = expectedMessages.indexOf(parsed);
if (idx > -1) {
expectedMessages.splice(idx, 1);
}
if (expectedMessages.length === 0) {
t.pass('it queues up messages and sends them once the socket opens');
server.close(() => t.end());
}
});
});
dispatch('a');
dispatch('b');
sharedSocket.open(url);
});

View File

@@ -0,0 +1,26 @@
var test = require('tape');
var WebSocket = require('ws');
var server = new WebSocket.Server({ port: 8889 });
var sharedSocket = require('../../src/SharedSocket');
var listen = require('../../src/listen');
test('listen', (t) => {
sharedSocket.open('ws://localhost:8889');
listen((message) => {
t.looseEqual(
message,
{ type: 'YASS', payload: 'yay' },
'it calls listeners with deserialized messages'
);
server.close(() => t.end());
});
server.on('connection', (ws) => {
ws.send(JSON.stringify({
type: 'YASS',
payload: 'yay',
}));
});
});

View File

@@ -0,0 +1,115 @@
var test = require('tape');
var reduce = require('../../src/reducer');
test('WIDGET_ADDED', (t) => {
var action = {
type: 'WIDGET_ADDED',
payload: { id: 'foo', error: 'oh no', filePath: '/foo/' },
};
var newState = reduce({ widgets: {} }, action);
t.looseEqual(
newState.widgets,
{ foo: { id: 'foo', error: 'oh no', filePath: '/foo/' } },
'it adds new widgets'
);
t.ok(
typeof newState.settings === 'object',
'it creates a new settings hash if none exists'
);
t.looseEqual(
newState.settings.foo, {
showOnAllScreens: true,
showOnMainScreen: false,
showOnSelectedScreens: false,
hidden: false,
screens: [],
},
'it initializes settings for a widget'
);
action = {
type: 'WIDGET_ADDED',
payload: { id: 'foo', body: 'yay', filePath: '/foo/' },
};
newState = reduce(newState, action);
t.looseEqual(
newState.widgets,
{ foo: { id: 'foo', body: 'yay', filePath: '/foo/' } },
'it updates existing widgets'
);
t.end();
});
test('WIDGET_REMOVED', (t) => {
var action = { type: 'WIDGET_REMOVED', payload: 'foo' };
var state = { widgets: {} };
var newState = reduce(state, action);
t.equal(state, newState, 'it ignores non existing widgets');
newState = reduce({
widgets: { foo: {}, bar: {}},
}, action);
t.looseEqual(newState.widgets, {bar: {}}, 'it removes existing widgets');
t.end();
});
test('WIDGET_SETTINGS_CHANGED', (t) => {
var action = {
type: 'WIDGET_SETTINGS_CHANGED',
payload: { id: 'foo', settings: { a: 'b' } },
};
newState = reduce({ settings: {} }, action);
t.looseEqual(
newState.settings,
{ foo: { a: 'b' } },
'it applies new settings'
);
newState = reduce({ settings: { bar: {} } }, action);
t.looseEqual(
newState.settings,
{ foo: { a: 'b' }, bar: {}},
'it merges with existing settings'
);
t.end();
});
test('WIDGET_SET_TO_HIDE / SHOW', (t) => {
var action = { type: 'WIDGET_SET_TO_HIDE', payload: 'bar' };
var state = {
settings: {
foo: { hidden: false, some: 'other', stuff: 1 },
bar: { hidden: false, many: 'other', things: 42 },
},
};
var newState = reduce(state, action);
t.looseEqual(
state.settings,
{
foo: { hidden: false, some: 'other', stuff: 1 },
bar: { hidden: false, many: 'other', things: 42 },
},
'it hides widgets'
);
action = { type: 'WIDGET_SET_TO_SHOW', payload: 'bar' };
newState = reduce(newState, action);
t.looseEqual(
state.settings,
{
foo: { hidden: false, some: 'other', stuff: 1 },
bar: { hidden: false, many: 'other', things: 42 },
},
'it shows widgets'
);
t.end();
});

View File

@@ -0,0 +1,102 @@
const test = require('tape');
const resolveWidget = require('../../src/resolveWidget.js');
test('resolving widget actions from file events', (t) => {
const action = resolveWidget({
type: 'added',
filePath: '/widget/dir/widget File name.js',
rootPath: '/widget/dir/',
});
t.plan(3);
t.equal(action.id, 'widget_File_name-js', 'it derives a widget id');
t.equal(
action.filePath, '/widget/dir/widget File name.js',
'it contains the original file path'
);
const action2 = resolveWidget({
type: 'removed',
filePath: '/widget/dir/widget File name.js',
rootPath: '/widget/dir/',
});
t.ok(
action.type === 'added' && action2.type === 'removed',
'it passes on the action type'
);
});
test('separating widgets from non-wigets', (t) => {
var action = resolveWidget({
filePath: '/widget/dir/file.js',
rootPath: '/widget/dir/',
});
t.ok(!!action, 'it accepts js files');
action = resolveWidget({
filePath: '/widget/dir/file.coffee',
rootPath: '/widget/dir/',
});
t.ok(!!action, 'it accepts coffee files');
action = resolveWidget({
filePath: '/widget/dir/file.jsx',
rootPath: '/widget/dir/',
});
t.ok(!!action, 'it accepts jsx files');
action = resolveWidget({
filePath: '/widget/dir/file.txt',
rootPath: '/widget/dir/',
});
t.ok(!action, 'it ignores other files');
action = resolveWidget({
filePath: '/widget/dir/node_modules/file.js',
rootPath: '/widget/dir/',
});
t.ok(!action, 'it ignores files inside node_modules');
action = resolveWidget({
filePath: '/widget/dir/src/file.js',
rootPath: '/widget/dir/',
});
t.ok(!action, 'it ignores files inside a src dir');
action = resolveWidget({
filePath: '/widget/dir/lib/file.js',
rootPath: '/widget/dir/',
});
t.ok(!action, 'it ignores files inside a lib dir');
action = resolveWidget({
filePath: '/widget/dir/some/other/dir/file.js',
rootPath: '/widget/dir/',
});
t.ok(!!action, 'it detects widgets in any other dir');
t.end();
});
test('deriving widget ids', (t) => {
var action = resolveWidget({
type: 'added',
filePath: '/widget/dir/spaces in the Name.js',
rootPath: '/widget/dir/',
});
t.equal(
action.id, 'spaces_in____the__Name-js', 'it replaces spaces with underscores'
);
action = resolveWidget({
type: 'added',
filePath: '/widget/dir/some-dir/widget.js',
rootPath: '/widget/dir/',
});
t.equal(
action.id, 'some-dir-widget-js', 'it replaces slashes with dashes'
);
t.end();
});

View File

@@ -0,0 +1,36 @@
var test = require('tape');
var validateWidget = require('../../src/validateWidget');
test('checking for empty implementations', (t) => {
var issues = validateWidget();
t.ok(
issues.indexOf('empty implementation') > -1,
'it does not allow them'
);
t.end();
});
test('checking for commands', (t) => {
var issues = validateWidget({});
t.ok(
issues.indexOf('no command given') > -1,
'it complains if when there is no command'
);
issues = validateWidget({ refreshFrequency: false });
t.ok(
issues.indexOf('no command given') === -1,
'it allows no commands when refreshFrequency is false'
);
t.end();
});
test('valid widgets', (t) => {
var issues = validateWidget({
command: 'yay'
});
t.ok(issues.length === 0, 'it finds no issues');
t.end();
});

View File

@@ -0,0 +1,83 @@
var test = require('tape');
var widgetify = require('../../src/widgetify');
var through = require('through2');
function grabOutput(then) {
var output = '';
return through(
(chunk, enc, next) => { output += chunk; next(); },
(next) => { then(output); next(); }
);
}
test('transforming valid widgets', (t) => {
var transform = widgetify('path/', { id: 'foo' });
var src = `
var color = '#ff';
var stuff = 1+2;
color = color + 'f';
({
foo: 14,
style: 'color: ' + color,
refreshFrequency: '2s'
})
`;
transform.pipe( grabOutput((transformed) => {
const module = {};
new Function('module', transformed)(module);
t.ok(
typeof module.exports === 'object',
'it assigns the last object expression to module.exports'
);
t.equal(
module.exports.id, 'foo',
'it adds the widget id'
);
t.equal(
module.exports.refreshFrequency, 2000,
'it parses string refresh frequencies'
);
t.equal(
module.exports.css, '#foo {\n color: #fff;\n}\n',
'it parses and scopes styles, including interpolated variables'
);
t.equal(
module.exports.style, undefined,
'it cleans up the style property'
);
t.end();
}));
transform.write(src);
transform.end();
});
test('transforming a widget with a syntax error', (t) => {
var transform = widgetify('path/', { id: 'foo' });
var src = `
({
foo: 14,
style: 'color: ' + color,
refreshFrequency: '2s'
})
`;
transform
.on('error', (e) => {
t.pass('it emits an error');
t.ok(
e.name === 'ReferenceError' && e.message === 'color is not defined',
'the error looks ok'
);
t.end();
})
.pipe(grabOutput((transformed) => {
t.ok(!transformed, 'and there is no outout');
}));
transform.write(src);
transform.end();
});