xo: Lint *.js too.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
This commit is contained in:
Anders Kaseorg
2020-03-04 20:47:41 -08:00
parent dee2f05ac0
commit 40bf2a1f20
10 changed files with 185 additions and 192 deletions

View File

@@ -5,14 +5,14 @@ const electron = require('electron-connect').server.create({
}); });
const tape = require('gulp-tape'); const tape = require('gulp-tape');
const tapColorize = require('tap-colorize'); const tapColorize = require('tap-colorize');
const ts = require("gulp-typescript"); const ts = require('gulp-typescript');
const tsProject = ts.createProject("tsconfig.json"); const tsProject = ts.createProject('tsconfig.json');
const glob = require('glob'); const glob = require('glob');
const { execSync } = require('child_process'); const {execSync} = require('child_process');
const baseFilePattern = 'app/+(main|renderer)/**/*'; const baseFilePattern = 'app/+(main|renderer)/**/*';
const globOptions = { cwd: __dirname }; const globOptions = {cwd: __dirname};
const jsFiles = glob.sync(baseFilePattern + '.js', globOptions); const jsFiles = glob.sync(baseFilePattern + '.js', globOptions);
const tsFiles = glob.sync(baseFilePattern + '.ts', globOptions); const tsFiles = glob.sync(baseFilePattern + '.ts', globOptions);
if (jsFiles.length !== tsFiles.length) { if (jsFiles.length !== tsFiles.length) {
@@ -21,18 +21,18 @@ if (jsFiles.length !== tsFiles.length) {
execSync(`${npx} tsc`); execSync(`${npx} tsc`);
} }
gulp.task("compile", function () { gulp.task('compile', () => {
return tsProject.src() return tsProject.src()
.pipe(tsProject()) .pipe(tsProject())
.js.pipe(gulp.dest("app")); .js.pipe(gulp.dest('app'));
}); });
gulp.task('dev', () => { gulp.task('dev', () => {
// Start browser process // Start browser process
electron.start(); electron.start();
// Restart browser process // Restart browser process
gulp.watch('app/main/*.ts', gulp.series('compile', 'restart:browser')); gulp.watch('app/main/*.ts', gulp.series('compile', 'restart:browser'));
// Reload renderer process // Reload renderer process
gulp.watch('app/renderer/css/*.css', gulp.series('reload:renderer')); gulp.watch('app/renderer/css/*.css', gulp.series('reload:renderer'));
gulp.watch('app/renderer/*.html', gulp.series('reload:renderer')); gulp.watch('app/renderer/*.html', gulp.series('reload:renderer'));
gulp.watch('app/renderer/js/**/*.ts', gulp.series('compile', 'reload:renderer')); gulp.watch('app/renderer/js/**/*.ts', gulp.series('compile', 'reload:renderer'));
@@ -45,16 +45,16 @@ gulp.task('restart:browser', done => {
}); });
gulp.task('reload:renderer', done => { gulp.task('reload:renderer', done => {
// Reload renderer process // Reload renderer process
electron.reload(); electron.reload();
done(); done();
}); });
gulp.task('test-e2e', () => { gulp.task('test-e2e', () => {
return gulp.src('tests/*.js') return gulp.src('tests/*.js')
.pipe(tape({ .pipe(tape({
reporter: tapColorize() reporter: tapColorize()
})); }));
}); });
gulp.task('default', gulp.parallel('dev', 'test-e2e')); gulp.task('default', gulp.parallel('dev', 'test-e2e'));

View File

@@ -195,12 +195,14 @@
"electron-notarize": "^0.2.1", "electron-notarize": "^0.2.1",
"eslint-config-xo-typescript": "^0.26.0", "eslint-config-xo-typescript": "^0.26.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"glob": "^5.0.15",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-tape": "^1.0.0", "gulp-tape": "^1.0.0",
"gulp-typescript": "^6.0.0-alpha.1", "gulp-typescript": "^6.0.0-alpha.1",
"htmlhint": "^0.11.0", "htmlhint": "^0.11.0",
"nodemon": "^2.0.2", "nodemon": "^2.0.2",
"pre-commit": "^1.2.2", "pre-commit": "^1.2.2",
"rimraf": "^2.7.1",
"spectron": "^10.0.1", "spectron": "^10.0.1",
"stylelint": "^13.2.0", "stylelint": "^13.2.0",
"tap-colorize": "^1.2.0", "tap-colorize": "^1.2.0",
@@ -209,22 +211,16 @@
"xo": "^0.27.2" "xo": "^0.27.2"
}, },
"xo": { "xo": {
"extends": "xo-typescript",
"extensions": [ "extensions": [
"ts" "ts"
], ],
"parserOptions": { "parserOptions": {
"ecmaVersion": 6, "ecmaVersion": 2019
"sourceType": "module",
"ecmaFeatures": {
"globalReturn": true,
"modules": true
}
}, },
"esnext": true,
"overrides": [ "overrides": [
{ {
"files": "app/**/*.ts", "files": "**/*.ts",
"extends": "xo-typescript",
"rules": { "rules": {
"@typescript-eslint/member-ordering": "off", "@typescript-eslint/member-ordering": "off",
"@typescript-eslint/no-dynamic-delete": "off", "@typescript-eslint/no-dynamic-delete": "off",
@@ -250,17 +246,9 @@
} }
} }
], ],
"ignore": [
"tests/*.js",
"tools/locale-helper/*.js",
"*/**/*.js",
"*.js",
"typings.d.ts"
],
"envs": [ "envs": [
"node", "node",
"browser", "browser"
"mocha"
] ]
} }
} }

View File

@@ -1,22 +1,22 @@
const path = require('path'); const path = require('path');
const dotenv = require('dotenv'); const dotenv = require('dotenv');
dotenv.config({ path: path.join(__dirname, '/../.env') }); dotenv.config({path: path.join(__dirname, '/../.env')});
const { notarize } = require('electron-notarize'); const {notarize} = require('electron-notarize');
exports.default = async function notarizing(context) { exports.default = async function (context) {
const { electronPlatformName, appOutDir } = context; const {electronPlatformName, appOutDir} = context;
if (electronPlatformName !== 'darwin') { if (electronPlatformName !== 'darwin') {
return; return;
} }
const appName = context.packager.appInfo.productFilename; const appName = context.packager.appInfo.productFilename;
return await notarize({ return notarize({
appBundleId: 'org.zulip.zulip-electron', appBundleId: 'org.zulip.zulip-electron',
appPath: `${appOutDir}/${appName}.app`, appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLE_ID, appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASS, appleIdPassword: process.env.APPLE_ID_PASS
}); });
}; };

View File

@@ -1,7 +1,5 @@
const path = require('path') const TEST_APP_PRODUCT_NAME = 'ZulipTest';
const TEST_APP_PRODUCT_NAME = 'ZulipTest'
module.exports = { module.exports = {
TEST_APP_PRODUCT_NAME TEST_APP_PRODUCT_NAME
} };

View File

@@ -1,13 +1,16 @@
const test = require('tape') const test = require('tape');
const setup = require('./setup') const setup = require('./setup');
test('app runs', function (t) { test('app runs', async t => {
t.timeoutAfter(10e3) t.timeoutAfter(10e3);
setup.resetTestDataDir() setup.resetTestDataDir();
const app = setup.createApp() const app = setup.createApp();
setup.waitForLoad(app, t) try {
.then(() => app.client.windowByIndex(1)) // focus on webview await setup.waitForLoad(app, t);
.then(() => app.client.waitForExist('//*[@id="connect"]')) // id of the connect button await app.client.windowByIndex(1); // Focus on webview
.then(() => setup.endTest(app, t), await app.client.waitForExist('//*[@id="connect"]'); // Id of the connect button
(err) => setup.endTest(app, t, err || 'error')) await setup.endTest(app, t);
}) } catch (error) {
await setup.endTest(app, t, error || 'error');
}
});

View File

@@ -1,97 +1,95 @@
const Application = require('spectron').Application const {Application} = require('spectron');
const fs = require('fs') const fs = require('fs');
const mkdirp = require('mkdirp') const path = require('path');
const path = require('path') const rimraf = require('rimraf');
const rimraf = require('rimraf')
const config = require('./config') const config = require('./config');
module.exports = { module.exports = {
createApp, createApp,
endTest, endTest,
waitForLoad, waitForLoad,
wait, wait,
resetTestDataDir resetTestDataDir
} };
// Runs Zulip Desktop. // Runs Zulip Desktop.
// Returns a promise that resolves to a Spectron Application once the app has loaded. // Returns a promise that resolves to a Spectron Application once the app has loaded.
// Takes a Tape test. Makes some basic assertions to verify that the app loaded correctly. // Takes a Tape test. Makes some basic assertions to verify that the app loaded correctly.
function createApp (t) { function createApp() {
generateTestAppPackageJson() generateTestAppPackageJson();
return new Application({ return new Application({
path: path.join(__dirname, '..', 'node_modules', '.bin', path: path.join(__dirname, '..', 'node_modules', '.bin',
'electron' + (process.platform === 'win32' ? '.cmd' : '')), 'electron' + (process.platform === 'win32' ? '.cmd' : '')),
args: [path.join(__dirname)], // Ensure this dir has a package.json file with a 'main' entry piont args: [path.join(__dirname)], // Ensure this dir has a package.json file with a 'main' entry piont
env: {NODE_ENV: 'test'}, env: {NODE_ENV: 'test'},
waitTimeout: 10e3 waitTimeout: 10e3
}) });
} }
// Generates package.json for test app // Generates package.json for test app
// Reads app package.json and updates the productName to config.TEST_APP_PRODUCT_NAME // Reads app package.json and updates the productName to config.TEST_APP_PRODUCT_NAME
// We do this so that the app integration doesn't doesn't share the same appDataDir as the dev application // We do this so that the app integration doesn't doesn't share the same appDataDir as the dev application
function generateTestAppPackageJson () { function generateTestAppPackageJson() {
let packageJson = require(path.join(__dirname, '../package.json')) const packageJson = require(path.join(__dirname, '../package.json'));
packageJson.productName = config.TEST_APP_PRODUCT_NAME packageJson.productName = config.TEST_APP_PRODUCT_NAME;
packageJson.main = '../app/main' packageJson.main = '../app/main';
const testPackageJsonPath = path.join(__dirname, 'package.json') const testPackageJsonPath = path.join(__dirname, 'package.json');
fs.writeFileSync(testPackageJsonPath, JSON.stringify(packageJson, null, ' '), 'utf-8') fs.writeFileSync(testPackageJsonPath, JSON.stringify(packageJson, null, ' '), 'utf-8');
} }
// Starts the app, waits for it to load, returns a promise // Starts the app, waits for it to load, returns a promise
function waitForLoad (app, t, opts) { async function waitForLoad(app, t, opts) {
if (!opts) opts = {} if (!opts) {
return app.start().then(function () { opts = {};
return app.client.waitUntilWindowLoaded() }
})
.then(function() { await app.start();
return app.client.pause(2000); await app.client.waitUntilWindowLoaded();
}) await app.client.pause(2000);
.then(function () { const title = await app.webContents.getTitle();
return app.webContents.getTitle() t.equal(title, 'Zulip', 'html title');
}).then(function (title) {
t.equal(title, 'Zulip', 'html title')
})
} }
// Returns a promise that resolves after 'ms' milliseconds. Default: 1 second // Returns a promise that resolves after 'ms' milliseconds. Default: 1 second
function wait (ms) { async function wait(ms) {
if (ms === undefined) ms = 1000 // Default: wait long enough for the UI to update if (ms === undefined) {
return new Promise(function (resolve, reject) { ms = 1000;
setTimeout(resolve, ms) } // Default: wait long enough for the UI to update
})
return new Promise((resolve => {
setTimeout(resolve, ms);
}));
} }
// Quit the app, end the test, either in success (!err) or failure (err) // Quit the app, end the test, either in success (!err) or failure (err)
function endTest (app, t, err) { async function endTest(app, t, err) {
return app.stop().then(function () { await app.stop();
t.end(err) t.end(err);
})
} }
function getAppDataDir () { function getAppDataDir() {
let base let base;
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
base = path.join(process.env.HOME, 'Library', 'Application Support') base = path.join(process.env.HOME, 'Library', 'Application Support');
} else if (process.platform === 'linux') { } else if (process.platform === 'linux') {
base = process.env.XDG_CONFIG_HOME ? base = process.env.XDG_CONFIG_HOME ?
process.env.XDG_CONFIG_HOME : path.join(process.env.HOME, '.config') process.env.XDG_CONFIG_HOME : path.join(process.env.HOME, '.config');
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
base = process.env.APPDATA base = process.env.APPDATA;
} else { } else {
console.log('Could not detect app data dir base. Exiting...') throw new Error('Could not detect app data dir base.');
process.exit(1) }
}
console.log('Detected App Data Dir base:', base) console.log('Detected App Data Dir base:', base);
return path.join(base, config.TEST_APP_PRODUCT_NAME) return path.join(base, config.TEST_APP_PRODUCT_NAME);
} }
// Resets the test directory, containing domain.json, window-state.json, etc // Resets the test directory, containing domain.json, window-state.json, etc
function resetTestDataDir () { function resetTestDataDir() {
appDataDir = getAppDataDir() const appDataDir = getAppDataDir();
rimraf.sync(appDataDir) rimraf.sync(appDataDir);
rimraf.sync(path.join(__dirname, 'package.json')) rimraf.sync(path.join(__dirname, 'package.json'));
} }

View File

@@ -1,19 +1,21 @@
const test = require('tape') const test = require('tape');
const setup = require('./setup') const setup = require('./setup');
test('add-organization', function (t) {
t.timeoutAfter(50e3)
setup.resetTestDataDir()
const app = setup.createApp()
setup.waitForLoad(app, t)
.then(() => app.client.windowByIndex(1)) // focus on webview
.then(() => app.client.setValue('.setting-input-value', 'chat.zulip.org'))
.then(() => app.client.click('#connect'))
.then(() => setup.wait(5000))
.then(() => app.client.windowByIndex(0)) // Switch focus back to main win
.then(() => app.client.windowByIndex(1)) // Switch focus back to org webview
.then(() => app.client.waitForExist('//*[@id="id_username"]'))
.then(() => setup.endTest(app, t),
(err) => setup.endTest(app, t, err || 'error'))
})
test('add-organization', async t => {
t.timeoutAfter(50e3);
setup.resetTestDataDir();
const app = setup.createApp();
try {
await setup.waitForLoad(app, t);
await app.client.windowByIndex(1); // Focus on webview
await app.client.setValue('.setting-input-value', 'chat.zulip.org');
await app.client.click('#connect');
await setup.wait(5000);
await app.client.windowByIndex(0); // Switch focus back to main win
await app.client.windowByIndex(1); // Switch focus back to org webview
await app.client.waitForExist('//*[@id="id_username"]');
await setup.endTest(app, t);
} catch (error) {
await setup.endTest(app, t, error || 'error');
}
});

View File

@@ -1,17 +1,19 @@
const test = require('tape') const test = require('tape');
const setup = require('./setup') const setup = require('./setup');
// Create new org link should open in the default browser [WIP] // Create new org link should open in the default browser [WIP]
test('new-org-link', function (t) { test('new-org-link', async t => {
t.timeoutAfter(50e3) t.timeoutAfter(50e3);
setup.resetTestDataDir() setup.resetTestDataDir();
const app = setup.createApp() const app = setup.createApp();
setup.waitForLoad(app, t) try {
.then(() => app.client.windowByIndex(1)) // focus on webview await setup.waitForLoad(app, t);
.then(() => app.client.click('#open-create-org-link')) // Click on new org link button await app.client.windowByIndex(1); // Focus on webview
.then(() => setup.wait(5000)) await app.client.click('#open-create-org-link'); // Click on new org link button
.then(() => setup.endTest(app, t), await setup.wait(5000);
(err) => setup.endTest(app, t, err || 'error')) await setup.endTest(app, t);
}) } catch (error) {
await setup.endTest(app, t, error || 'error');
}
});

View File

@@ -5,28 +5,30 @@ const fs = require('fs');
const translationDir = path.resolve(__dirname, '../../app/translations'); const translationDir = path.resolve(__dirname, '../../app/translations');
function writeJSON(file, data) { function writeJSON(file, data) {
const filePath = path.resolve(translationDir, file); const filePath = path.resolve(translationDir, file);
fs.writeFileSync(filePath, `${JSON.stringify(data, null, '\t')}\n`, 'utf8'); fs.writeFileSync(filePath, `${JSON.stringify(data, null, '\t')}\n`, 'utf8');
} }
const { phrases } = require('./locale-template'); const {phrases} = require('./locale-template');
const supportedLocales = require('./supported-locales.json'); const supportedLocales = require('./supported-locales.json');
phrases.sort(); phrases.sort();
for (let locale in supportedLocales) { for (const [locale, name] of Object.entries(supportedLocales)) {
console.log(`fetching translation for: ${supportedLocales[locale]} - ${locale}..`); console.log(`fetching translation for: ${name} - ${locale}..`);
translate(phrases.join('\n'), { to: locale }) (async () => {
.then(res => { try {
const localeFile = `${locale}.json`; const res = await translate(phrases.join('\n'), {to: locale});
const translatedText = res.text.split('\n'); const localeFile = `${locale}.json`;
const translationJSON = {}; const translatedText = res.text.split('\n');
phrases.forEach((phrase, index) => { const translationJSON = {};
translationJSON[phrase] = translatedText[index]; phrases.forEach((phrase, index) => {
}); translationJSON[phrase] = translatedText[index];
});
writeJSON(localeFile, translationJSON); writeJSON(localeFile, translationJSON);
console.log(`create: ${localeFile}`); console.log(`create: ${localeFile}`);
}).catch(err => { } catch (error) {
console.error(err); console.error(error);
}); }
})();
} }

18
typings.d.ts vendored
View File

@@ -3,20 +3,20 @@ declare module 'node-mac-notifier';
declare module 'wurl'; declare module 'wurl';
interface PageParamsObject { interface PageParamsObject {
realm_uri: string; realm_uri: string;
default_language: string; default_language: string;
external_authentication_methods: any; external_authentication_methods: any;
} }
declare var page_params: PageParamsObject; declare let page_params: PageParamsObject;
// This is mostly zulip side of code we access from window // This is mostly zulip side of code we access from window
interface Window { interface Window {
$: any; $: any;
narrow: any narrow: any;
} }
interface ZulipWebWindow extends Window { interface ZulipWebWindow extends Window {
electron_bridge: any; electron_bridge: any;
tray: any; tray: any;
lightbox: any; lightbox: any;
} }