chore: setup cucumber BBD environment

This commit is contained in:
tiddlygit-test 2021-04-18 21:47:05 +08:00
parent afe8ecd076
commit 7641376b89
17 changed files with 2685 additions and 144 deletions

30
cucumber.js Normal file
View file

@ -0,0 +1,30 @@
const feature = [
'--require-module ts-node/register',
'--require features/**/*.ts',
`--format progress-bar`,
'--format rerun:logs/@rerun.txt',
'--format usage:logs/usage.txt',
'--format message:logs/messages.ndjson',
'--publish-quiet',
].join(' ');
const cck = ['--require-module', 'ts-node/register', '--format', 'message'].join(' ');
const FORMATTERS_INCLUDE = ['attachments', 'data-tables', 'examples-tables', 'minimal', 'parameter-types', 'rules', 'stack-traces', '--publish-quiet'];
const htmlFormatter = [
`node_modules/@cucumber/compatibility-kit/features/{${FORMATTERS_INCLUDE.join(',')}}/*.feature`,
'--require-module',
'ts-node/register',
'--require',
`compatibility/features/{${FORMATTERS_INCLUDE.join(',')}}/*.ts`,
'--format',
'html:html-formatter.html',
'--publish-quiet',
].join(' ');
module.exports = {
default: feature,
// cck,
// htmlFormatter,
};

View file

@ -0,0 +1,8 @@
Feature: Open
As a user of TiddlyGit
I want to open the app
So I can be more productive
Scenario: Opening TiddlyGit
Given the app is launched
Then the element "#new-user-tip" is on the page

View file

@ -0,0 +1,16 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { setWorldConstructor, Given, Then, When } from '@cucumber/cucumber';
import { expect } from 'chai';
import { TiddlyGitWorld } from '../supports/world';
setWorldConstructor(TiddlyGitWorld);
Given('the app is launched', { timeout: 120 * 1000 }, async function (this: TiddlyGitWorld) {
await this.start();
});
Then('the element {string} is on the page', { timeout: 120 * 1000 }, async function (this: TiddlyGitWorld, elementSelector: string) {
const result = await this.getElement(elementSelector);
expect(result).to.not.be.undefined;
this.updateContext({ previousElement: result });
});

View file

@ -0,0 +1,28 @@
/* eslint-disable unicorn/filename-case */
import { After, Before, Status } from '@cucumber/cucumber';
import jetpack from 'fs-extra';
import path from 'path';
import { TiddlyGitWorld } from './world';
Before(function () {
// TODO: clear setting folder
});
After(async function (this: TiddlyGitWorld, testCase) {
if (this.app !== undefined && testCase.result?.status === Status.FAILED) {
console.log('main:\n---\n');
await this.app.client.getMainProcessLogs().then(function (logs) {
logs.forEach(function (log) {
console.log(log, '\n');
});
});
console.log('renderer:\n---\n');
await this.app.client.getRenderProcessLogs().then(function (logs) {
logs.forEach(function (log) {
console.log(JSON.stringify(log), '\n');
});
});
console.log('\n');
}
return this.close();
});

View file

@ -0,0 +1,91 @@
import { setDefaultTimeout, World } from '@cucumber/cucumber';
import path from 'path';
import { delay } from 'bluebird';
import { Application } from 'spectron';
import { keyboard, Key } from '@nut-tree/nut-js';
setDefaultTimeout(30 * 1000);
const projectRoot = path.join(__dirname, '..', '..');
const packageName = process.env.npm_package_name ?? 'TiddlyGit';
interface IContext {
previousElement?: WebdriverIO.Element;
}
/**
* Execution environment for TiddlyGit in cucumber-js
*/
export class TiddlyGitWorld extends World {
/** our electron app instance created by spectron */
public app?: Application;
/** store selected element and other things, so subsequent cucumber statement can get context */
public context?: IContext;
/** the compiled src/main.ts */
private readonly appPath = path.join(projectRoot, '.webpack', 'main', 'index.js');
/** cold start the electron app */
public async start(): Promise<void> {
this.app = new Application({
path: path.join(
projectRoot,
// The path to the binary depends on your platform and architecture
`out/${packageName}-darwin-x64/${packageName}.app/Contents/MacOS/${packageName}`,
),
args: [this.appPath],
chromeDriverArgs: ['--disable-extensions'],
cwd: projectRoot,
env: {
NODE_ENV: 'test',
},
port: 9156,
});
await this.app.start();
while (undefined === (await this.getElement('#test'))) {
await delay(500);
}
}
public async getElement(selector: string): Promise<WebdriverIO.Element | undefined> {
const element = await this.app?.client?.$?.(selector);
// sometimes element exist, but has an error field
/* Element {
sessionId: 'ae55dccb0daecda748fa4239f89d03e5',
error: {
error: 'no such element',
message: 'no such element: Unable to locate element: {"method":"css selector","selector":"#test"}\n' +
' (Session info: chrome=89.0.4389.114)', */
if (element !== undefined && !('error' in element)) {
return element;
}
}
public updateContext(context: Partial<IContext>): void {
this.context = this.context === undefined ? context : { ...this.context, ...context };
}
public async type(input: string): Promise<void> {
await keyboard.type(input);
}
public async hitKey(key: Key, modifier?: Key): Promise<void> {
if (modifier !== undefined) {
await keyboard.pressKey(modifier);
await keyboard.pressKey(key);
await keyboard.releaseKey(key);
await keyboard.releaseKey(modifier);
} else {
await keyboard.pressKey(key);
await keyboard.releaseKey(key);
}
}
public async close(): Promise<void> {
await this.app?.stop();
}
public readClipboard(): string | undefined {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
return this.app?.electron?.clipboard?.readText?.();
}
}

2581
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,15 @@
{
"name": "tiddly-git",
"name": "TiddlyGit",
"productName": "TiddlyGit",
"description": "Customizable personal knowledge-base with Github as unlimited storage and blogging platform.",
"version": "0.3.6",
"scripts": {
"start": "NODE_ENV=development electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"start": "npm run clean && cross-env NODE_ENV=development electron-forge start",
"test": "npm run clean && cross-env NODE_ENV=test npm run package && npm run test-without-package",
"test-without-package": "mkdir -p logs && cross-env NODE_ENV=test cucumber-js",
"package": "cross-env NODE_ENV=production electron-forge package",
"make": "cross-env NODE_ENV=production electron-forge make",
"clean": "rimraf ./out ./settings-dev ./logs",
"lint": "eslint ./src --ext js",
"lint:fix": "eslint ./src --ext js --fix",
"installType": "typesync",
@ -125,6 +128,7 @@
},
"devDependencies": {
"@authing/sso": "1.8.3",
"@cucumber/cucumber": "^7.1.0",
"@date-io/date-fns": "2.10.8",
"@electron-forge/cli": "6.0.0-beta.54",
"@electron-forge/maker-deb": "6.0.0-beta.54",
@ -136,7 +140,9 @@
"@material-ui/core": "^5.0.0-alpha.27",
"@material-ui/icons": "^5.0.0-alpha.27",
"@material-ui/lab": "^5.0.0-alpha.27",
"@nut-tree/nut-js": "^1.6.0",
"@types/bluebird": "^3.5.33",
"@types/chai": "^4.2.16",
"@types/circular-dependency-plugin": "^5.0.1",
"@types/classnames": "2.2.11",
"@types/copy-webpack-plugin": "^6.4.0",
@ -165,13 +171,16 @@
"@typescript-eslint/eslint-plugin": "4.18.0",
"@typescript-eslint/parser": "4.18.0",
"ace-builds": "1.4.12",
"chai": "^4.3.4",
"circular-dependency-plugin": "^5.2.2",
"classnames": "2.2.6",
"copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.3",
"csp-html-webpack-plugin": "5.1.0",
"css-loader": "^5.0.2",
"date-fns": "2.19.0",
"electron": "^13.0.0-beta.6",
"electron": "^12.0.4",
"electron-rebuild": "^2.3.5",
"eslint": "7.22.0",
"eslint-config-prettier": "8.1.0",
"eslint-config-standard": "16.0.2",
@ -202,12 +211,14 @@
"rimraf": "^3.0.2",
"simplebar": "6.0.0-beta.4",
"simplebar-react": "3.0.0-beta.5",
"spectron": "^14.0.0",
"style-loader": "^2.0.0",
"styled-components": "5.2.1",
"subscriptions-transport-ws": "^0.9.18",
"ts-import-plugin": "^1.6.7",
"ts-loader": "^8.0.18",
"ts-migrate": "^0.1.16",
"ts-node": "^9.1.1",
"typeface-roboto": "1.1.13",
"typescript": "4.2.3",
"typescript-plugin-styled-components": "^1.4.4",

View file

@ -0,0 +1,4 @@
import isDevelopment from 'electron-is-dev';
export const isTest = process.env.NODE_ENV === 'test';
export const isDevelopmentOrTest = isDevelopment || isTest;

View file

@ -288,14 +288,14 @@ export default function Main(): JSX.Element {
{sidebar === true ? (
<>
<Arrow image={themeSource === 'dark' ? arrowWhite : arrowBlack} />
<Tip>
<Tip id="new-user-tip">
<Tip2Text>Click</Tip2Text>
<Avatar>+</Avatar>
<Tip2Text>to get started!</Tip2Text>
</Tip>
</>
) : (
<Tip2>
<Tip2 id="new-user-tip">
<Tip2Text>
<span>Click </span>
<strong>Workspaces &gt; Add Workspace</strong>

View file

@ -0,0 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
if (process.env.NODE_ENV === 'test') {
// @ts-expect-error for spectron https://github.com/electron-userland/spectron#node-integration
window.electronRequire = require;
delete window.require;
}

View file

@ -1,6 +1,7 @@
import 'reflect-metadata';
import { contextBridge, ipcRenderer } from 'electron';
import './common/test';
import './common/i18n';
import './common/remote';
import './common/authing-postmessage';

View file

@ -50,7 +50,7 @@ async function runApp(): Promise<void> {
if (window.meta.windowName === WindowNames.preferences && preventClosingWindow) {
return;
}
window.remote.closeCurrentWindow();
void window.remote.closeCurrentWindow();
})();
});
}
@ -62,6 +62,7 @@ async function runApp(): Promise<void> {
<CssBaseline />
<React.Suspense fallback={<div />}>
<I18nextProvider i18n={i18n}>
<div id="test" data-usage="For spectron automating testing" />
<App />
</I18nextProvider>
</React.Suspense>

View file

@ -1,7 +1,7 @@
import { app } from 'electron';
import isDev from 'electron-is-dev';
import path from 'path';
import os from 'os';
import { isDevelopmentOrTest } from '@/constants/environment';
const isMac = process.platform === 'darwin';
@ -11,19 +11,21 @@ export const buildResourcePath = path.resolve(sourcePath, '..', 'build-resources
const REACT_PATH = MAIN_WINDOW_WEBPACK_ENTRY;
// .app/Contents/Resources/wiki/
const TIDDLYWIKI_TEMPLATE_FOLDER_PATH = isDev ? path.resolve(sourcePath, '..', 'template', 'wiki') : path.resolve(process.resourcesPath, '..', 'wiki');
const TIDDLYWIKI_TEMPLATE_FOLDER_PATH = isDevelopmentOrTest
? path.resolve(sourcePath, '..', 'template', 'wiki')
: path.resolve(process.resourcesPath, '..', 'wiki');
const TIDDLERS_PATH = 'tiddlers';
const ICON_PATH = isDev ? path.resolve(buildResourcePath, 'icon.png') : `file://${path.resolve(__dirname, '..', 'icon.png')}`;
const ICON_PATH = isDevelopmentOrTest ? path.resolve(buildResourcePath, 'icon.png') : `file://${path.resolve(__dirname, '..', 'icon.png')}`;
const CHROME_ERROR_PATH = 'chrome-error://chromewebdata/';
const LOGIN_REDIRECT_PATH = 'http://localhost:3000/?code=';
const DESKTOP_PATH = path.join(os.homedir(), 'Desktop');
const LOG_FOLDER = isDev
const LOG_FOLDER = isDevelopmentOrTest
? path.resolve(sourcePath, '..', 'logs')
: isMac
? path.resolve(process.resourcesPath, '..', 'logs')
: path.resolve(os.homedir(), '.tg-note', 'logs');
const SETTINGS_FOLDER = isDev ? path.resolve(sourcePath, '..', 'settings-dev') : path.resolve(app.getPath('userData'), 'settings');
const LOCALIZATION_FOLDER = isDev ? path.resolve(sourcePath, '..', 'localization') : path.resolve(process.resourcesPath, 'localization');
const SETTINGS_FOLDER = isDevelopmentOrTest ? path.resolve(sourcePath, '..', 'settings-dev') : path.resolve(app.getPath('userData'), 'settings');
const LOCALIZATION_FOLDER = isDevelopmentOrTest ? path.resolve(sourcePath, '..', 'localization') : path.resolve(process.resourcesPath, 'localization');
export {
REACT_PATH,

View file

@ -3,10 +3,10 @@ import { Menu, Tray, ipcMain, nativeImage } from 'electron';
import windowStateKeeper from 'electron-window-state';
import { menubar, Menubar } from 'menubar';
import path from 'path';
import isDevelopment from 'electron-is-dev';
import { REACT_PATH, buildResourcePath } from '@services/constants/paths';
import { WindowNames } from './WindowProperties';
import { isDevelopmentOrTest, isTest } from '@/constants/environment';
export default async function handleAttachToMenuBar(): Promise<Menubar> {
const menubarWindowState = windowStateKeeper({
@ -37,10 +37,10 @@ export default async function handleAttachToMenuBar(): Promise<Menubar> {
minHeight: 100,
minWidth: 250,
webPreferences: {
devTools: true,
devTools: !isTest,
nodeIntegration: false,
enableRemoteModule: false,
webSecurity: !isDevelopment,
webSecurity: !isDevelopmentOrTest,
allowRunningInsecureContent: false,
contextIsolation: true,
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,

View file

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import { BrowserWindow, ipcMain, dialog, app, webFrame, clipboard, BrowserWindowConstructorOptions } from 'electron';
import isDevelopment from 'electron-is-dev';
import { BrowserWindow, ipcMain, dialog, app, clipboard, BrowserWindowConstructorOptions } from 'electron';
import { injectable } from 'inversify';
import { Menubar } from 'menubar';
import windowStateKeeper, { State as windowStateKeeperState } from 'electron-window-state';
@ -20,6 +19,7 @@ import getFromRenderer from '@services/libs/getFromRenderer';
import { lazyInject } from '@services/container';
import handleAttachToMenuBar from './handleAttachToMenuBar';
import { IWindowService } from './interface';
import { isDevelopmentOrTest, isTest } from '@/constants/environment';
@injectable()
export class Window implements IWindowService {
@ -154,10 +154,10 @@ export class Window implements IWindowService {
autoHideMenuBar: false,
titleBarStyle: titleBar ? 'default' : 'hidden',
webPreferences: {
devTools: true,
devTools: !isTest,
nodeIntegration: false,
enableRemoteModule: false,
webSecurity: !isDevelopment,
webSecurity: !isDevelopmentOrTest,
allowRunningInsecureContent: false,
contextIsolation: true,
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,

View file

@ -13,6 +13,7 @@
"src/**/*.jsx",
"public/**/*.ts",
"public/**/*.js",
"features/**/*.ts",
"src/**/*.d.ts",
"public/**/*.d.ts",
"./*.json",

View file

@ -1,10 +1,11 @@
{
"exclude": ["template/**/*.js"],
"include": ["src", "features"],
"compilerOptions": {
"baseUrl": "./src",
"baseUrl": "./",
"paths": {
"@services/*": ["./services/*"],
"@/*": ["./*"]
"@services/*": ["./src/services/*"],
"@/*": ["./src/*"]
},
/* Basic Options */
"target": "es2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,