Compare commits

...
19 Commits
Author SHA1 Message Date
Thomas Nordquist 03e228e5a0 Fix ci fullscreen flag 2019-05-01 00:58:04 +02:00
Thomas Nordquist 14f2f9ff1e Fix mac release artifact publish settings 2019-04-30 21:22:54 +02:00
Thomas Nordquist 86544c1334 Disable sandbox 2019-04-30 18:35:29 +02:00
Thomas Nordquist 9e71c3132e Use proper provisioning profile for OSX builds 2019-04-30 18:28:35 +02:00
Thomas Nordquist 817202fc20 Disable electron sandbox due to crashes 2019-04-30 17:44:53 +02:00
Thomas Nordquist 931ec0113c Fix ui glitch 2019-04-30 17:44:25 +02:00
Thomas Nordquist 365ebc78ab Fix ui tests 2019-04-30 16:25:37 +02:00
Thomas Nordquist a1d3f32f73 Fix mouse-dummy pointer events for demo video 2019-04-27 10:32:45 +02:00
Thomas Nordquist e9e6ea618d Change leak acceptance level 2019-04-27 10:31:16 +02:00
Thomas Nordquist 6021df7150 Fix minor ui-bugs 2019-04-27 10:31:06 +02:00
Thomas Nordquist 070b72b304 Document license 2019-04-27 10:29:37 +02:00
Thomas Nordquist 1f65a0f316 Upgrade electron to v5.0.0 2019-04-26 17:45:35 +02:00
Thomas Nordquist 0f21f10c0d Fix backend tests 2019-04-26 14:57:12 +02:00
Thomas Nordquist af2ff0149d Add memory leak test-suite 2019-04-25 13:50:15 +02:00
Thomas Nordquist 8cd11cde3b Disable redux dev tools on default 2019-04-25 09:01:07 +02:00
Thomas Nordquist fdbe6344d9 Disable diff hover highlight 2019-04-25 09:00:44 +02:00
Thomas Nordquist a56b41635c Fix indentation 2019-04-25 00:28:48 +02:00
Thomas Nordquist 2c5c218fd1 Remove unsubscription since component would unount when node changes 2019-04-25 00:05:33 +02:00
Thomas Nordquist 749df70d5c Fix memory leaks 2019-04-24 23:40:28 +02:00
39 changed files with 386 additions and 121 deletions
+6 -3
View File
@@ -4,11 +4,12 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --mode production",
"dev": "node_modules/.bin/webpack-dev-server --mode development --progress"
"build": "yarn rebuild && webpack --mode production",
"dev": "node_modules/.bin/webpack-dev-server --mode development --progress",
"rebuild": "cd node_modules/heapdump && node-gyp rebuild --target=5.0.0 --arch=x64 --dist-url=https://atom.io/download/electron || echo Could not build heapdump; cd -"
},
"author": "",
"license": "ISC",
"license": "CC-BY-ND-4.0",
"dependencies": {
"@material-ui/core": "^4.0.0-alpha.6",
"@material-ui/icons": "^4.0.0-alpha.1",
@@ -60,7 +61,9 @@
"awesome-typescript-loader": "^5.2.1",
"css-loader": "^2.1.0",
"hard-source-webpack-plugin": "^0.13.1",
"heapdump": "^0.3.12",
"html-webpack-plugin": "^4.0.0-beta.5",
"node-loader": "^0.6.0",
"source-map-loader": "^0.2.4",
"style-loader": "^0.23.1",
"typescript": "^3.2.2",
+3
View File
@@ -78,6 +78,8 @@ export const disconnect = () => (dispatch: Dispatch<any>, getState: () => AppSta
}
tree && tree.stopUpdating()
tree && tree.destroy()
// Clear topic filter
dispatch({
topicFilter: '',
@@ -88,4 +90,5 @@ export const disconnect = () => (dispatch: Dispatch<any>, getState: () => AppSta
dispatch({
type: ActionTypes.CONNECTION_SET_DISCONNECTED,
})
dispatch(showTree(undefined))
}
+16 -8
View File
@@ -53,20 +53,28 @@ const debouncedSelectTopic = debounce((topic: q.TreeNode<TopicViewModel>, dispat
}
}, 70)
export const resetStore = () => (dispatch: Dispatch<any>): AnyAction => {
function destroyUnreferencedTree(state: AppState) {
const visibleTree = state.tree.get('tree')
const connectionTree = state.connection.tree
// Stop updates of old tree
if (visibleTree && visibleTree !== connectionTree) {
console.warn('destroy')
visibleTree.stopUpdating()
visibleTree.destroy()
}
}
export const resetStore = () => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
destroyUnreferencedTree(getState())
return dispatch({
type: ActionTypes.TREE_RESET_STORE,
})
}
export const showTree = (tree: q.Tree<TopicViewModel> | undefined) => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
const visibleTree = getState().tree.get('tree')
const connectionTree = getState().connection.tree
// Stop updates of old tree
if (visibleTree !== connectionTree && visibleTree) {
visibleTree.stopUpdating()
}
destroyUnreferencedTree(getState())
return dispatch({
tree,
+8 -6
View File
@@ -89,6 +89,12 @@ class App extends React.PureComponent<Props, {}> {
const styles = (theme: Theme) => {
const drawerWidth = 300
const contentBaseStyle = {
width: '100vw',
overflow: 'hidden' as 'hidden',
backgroundColor: theme.palette.background.default,
}
return {
heightProperty: {
height: 'calc(100vh - 64px) !important',
@@ -106,9 +112,7 @@ const styles = (theme: Theme) => {
overflow: 'hidden' as 'hidden',
},
content: {
width: '100vw',
overflowX: 'hidden' as 'hidden',
backgroundColor: theme.palette.background.default,
...contentBaseStyle,
transition: theme.transitions.create('transform', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
@@ -116,9 +120,7 @@ const styles = (theme: Theme) => {
transform: 'translateX(0px)',
},
contentShift: {
overflowX: 'hidden' as 'hidden',
width: '100vw',
padding: 0,
...contentBaseStyle,
backgroundColor: theme.palette.background.default,
transition: theme.transitions.create('transform', {
easing: theme.transitions.easing.easeOut,
+1
View File
@@ -77,6 +77,7 @@ const style = (theme: Theme) => ({
position: 'fixed' as 'fixed',
zIndex: 1000000,
filter: theme.palette.type === 'light' ? undefined : 'invert(100%)',
pointerEvents: 'none' as 'none',
},
})
+13 -1
View File
@@ -1,8 +1,20 @@
import * as React from 'react'
import ShowText from './ShowText'
import Mouse from './Mouse'
let heapdump: any
(window as any).demo = {}
function writeHeapdump(path?: string) {
if (!heapdump) {
heapdump = require('heapdump')
}
heapdump.writeSnapshot(path || `${Date.now()}.heapsnapshot`)
return path
}
(window as any).demo = {
writeHeapdump,
}
export default function render(props: any) {
return (
-3
View File
@@ -78,9 +78,6 @@ const styles: StyleRulesCallback = theme => ({
paddingLeft: theme.spacing(6),
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('md')]: {
width: 200,
},
},
menuButton: {
marginLeft: -12,
@@ -109,7 +109,7 @@ class Settings extends React.Component<Props, {}> {
private toggleTheme() {
const { actions, theme } = this.props
return <BooleanSwitch title="Dark Theme" tooltip="Enable dark theme" value={theme === 'light'} action={actions.settings.toggleTheme} />
return <BooleanSwitch title="Dark Mode" tooltip="Enable dark theme" value={theme === 'dark'} action={actions.settings.toggleTheme} />
}
private renderAutoExpand() {
@@ -120,12 +120,12 @@ class Settings extends React.Component<Props, {}> {
<div style={{ padding: '8px', display: 'flex' }}>
<InputLabel htmlFor="auto-expand" style={{ flex: '1', marginTop: '8px' }}>Auto Expand</InputLabel>
<Select
value={autoExpandLimit}
onChange={this.onChangeAutoExpand}
input={<Input name="auto-expand" id="auto-expand-label-placeholder" />}
name="auto-expand"
className={classes.input}
style={{ flex: '1' }}
value={autoExpandLimit}
onChange={this.onChangeAutoExpand}
input={<Input name="auto-expand" id="auto-expand-label-placeholder" />}
name="auto-expand"
className={classes.input}
style={{ flex: '1' }}
>
{limits}
</Select>
+6 -6
View File
@@ -182,16 +182,16 @@ const style = (theme: Theme) => {
deletion: {
...gutterBaseStyle,
backgroundColor: 'rgba(255, 10, 10, 0.3)',
'&:hover': {
backgroundColor: 'rgba(255, 10, 10, 0.6)',
},
// '&:hover': {
// backgroundColor: 'rgba(255, 10, 10, 0.3)',
// },
},
addition: {
...gutterBaseStyle,
backgroundColor: 'rgba(10, 255, 10, 0.3)',
'&:hover': {
backgroundColor: 'rgba(10, 255, 10, 0.5)',
},
// '&:hover': {
// backgroundColor: 'rgba(10, 255, 10, 0.5)',
// },
},
}
}
+1 -4
View File
@@ -35,7 +35,6 @@ interface Props {
}
interface State {
node: q.TreeNode<TopicViewModel>
compareMessage?: q.Message
valueRenderWidth: number
}
@@ -49,8 +48,7 @@ class Sidebar extends React.Component<Props, State> {
constructor(props: any) {
super(props)
console.error('Find and fix me #state')
this.state = { node: new q.Tree(), valueRenderWidth: 300 }
this.state = { valueRenderWidth: 300 }
}
private registerUpdateListener(node: q.TreeNode<TopicViewModel>) {
@@ -156,7 +154,6 @@ class Sidebar extends React.Component<Props, State> {
public componentWillReceiveProps(nextProps: Props) {
this.props.node && this.removeUpdateListener(this.props.node)
nextProps.node && this.registerUpdateListener(nextProps.node)
this.props.node && this.setState({ node: this.props.node })
if (this.props.node !== nextProps.node) {
this.setState({ compareMessage: undefined })
+2 -2
View File
@@ -30,7 +30,7 @@ interface State {
lastUpdate: number
}
class Tree extends React.PureComponent<Props, State> {
class TreeComponent extends React.PureComponent<Props, State> {
private updateTimer?: any
private perf: number = 0
private renderTime = 0
@@ -137,4 +137,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Tree)
export default connect(mapStateToProps, mapDispatchToProps)(TreeComponent)
+4 -11
View File
@@ -70,7 +70,7 @@ interface State {
selected: boolean
}
class TreeNode extends React.Component<Props, State> {
class TreeNodeComponent extends React.Component<Props, State> {
private animationDirty: boolean = false
private cssAnimationWasSetAt?: number
@@ -97,8 +97,8 @@ class TreeNode extends React.Component<Props, State> {
treeNode.viewModel.change.subscribe(this.viewStateHasChanged)
}
private viewStateHasChanged = (msg: void, viewModel: TopicViewModel) => {
this.setState({ selected: viewModel.isSelected() })
private viewStateHasChanged = (msg: void) => {
this.setState({ selected: this.props.treeNode.viewModel!.isSelected() })
}
private removeSubscriber(treeNode: q.TreeNode<TopicViewModel>) {
@@ -173,13 +173,6 @@ class TreeNode extends React.Component<Props, State> {
this.addSubscriber(treeNode)
}
public componentWillReceiveProps(nextProps: Props) {
if (nextProps.treeNode !== this.props.treeNode) {
this.removeSubscriber(this.props.treeNode)
this.addSubscriber(nextProps.treeNode)
}
}
public componentWillUnmount() {
const { treeNode } = this.props
this.removeSubscriber(treeNode)
@@ -243,4 +236,4 @@ class TreeNode extends React.Component<Props, State> {
}
}
export default withStyles(styles, { withTheme: true })(TreeNode)
export default withStyles(styles, { withTheme: true })(TreeNodeComponent)
+1 -1
View File
@@ -2,7 +2,7 @@ import { EventDispatcher } from '../../../events'
export class TopicViewModel {
private selected: boolean
public change = new EventDispatcher<void, TopicViewModel>(this)
public change = new EventDispatcher<void, TopicViewModel>()
public constructor() {
this.selected = false
+16 -2
View File
@@ -14,6 +14,7 @@ module.exports = {
path: `${__dirname}/build`,
},
optimization: {
minimize: false,
splitChunks: {
chunks: 'all',
minSize: 30000,
@@ -47,7 +48,7 @@ module.exports = {
devtool: 'source-map',
resolve: {
// Add '.ts' and '.tsx' as resolvable extensions.
extensions: ['.ts', '.tsx', '.js', '.json'],
extensions: ['.ts', '.tsx', '.js', '.json', '.node'],
},
module: {
rules: [
@@ -77,6 +78,15 @@ module.exports = {
},
],
},
{
test: /\.node$/,
use: {
loader: 'node-loader',
options: {
modules: true,
}
}
},
],
},
@@ -84,7 +94,11 @@ module.exports = {
new HtmlWebpackPlugin({ template: './index.html', file: './build/index.html', inject: false }),
// new BundleAnalyzerPlugin(),
new HardSourceWebpackPlugin(),
new webpack.HotModuleReplacementPlugin()
new webpack.HotModuleReplacementPlugin(),
new webpack.IgnorePlugin({
resourceRegExp: /\.\/build\/Debug\/addon/,
contextRegExp: /heapdump$/
}),
],
// When importing a module whose path matches one of the following, just
+13 -1
View File
@@ -2354,6 +2354,13 @@ he@1.2.x:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
heapdump@^0.3.12:
version "0.3.12"
resolved "https://registry.yarnpkg.com/heapdump/-/heapdump-0.3.12.tgz#5623be7816a8a92ab2d42b1b422f9e829a58da89"
integrity sha512-OSVdkxGd9xAJNEP0RA/ZvKEzGTEjCnkvmMEGF1ScR/87tH92v5Dpv6g/Fs8GvZA9UxdMpxU1G9NEC2D0OxZjJQ==
dependencies:
nan "^2.11.1"
hmac-drbg@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -3346,7 +3353,7 @@ multicast-dns@^6.0.1:
dns-packet "^1.3.1"
thunky "^1.0.2"
nan@^2.9.2:
nan@^2.11.1, nan@^2.9.2:
version "2.13.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
@@ -3438,6 +3445,11 @@ node-libs-browser@^2.0.0:
util "^0.11.0"
vm-browserify "0.0.4"
node-loader@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/node-loader/-/node-loader-0.6.0.tgz#c797ef51095ed5859902b157f6384f6361e05ae8"
integrity sha1-x5fvUQle1YWZArFX9jhPY2HgWug=
node-object-hash@^1.2.0:
version "1.4.2"
resolved "https://registry.yarnpkg.com/node-object-hash/-/node-object-hash-1.4.2.tgz#385833d85b229902b75826224f6077be969a9e94"
+1 -1
View File
@@ -12,7 +12,7 @@
"postinstall": "yarn build"
},
"author": "",
"license": "ISC",
"license": "CC-BY-ND-4.0",
"nyc": {
"include": [
"src/**/*.ts",
+9 -3
View File
@@ -1,13 +1,15 @@
import * as FileAsync from 'lowdb/adapters/FileAsync'
import * as fs from 'fs-extra'
import * as lowdb from 'lowdb'
import * as path from 'path'
import { backendEvents } from '../../events'
import {
makeStorageAcknowledgementEvent,
makeStorageResponseEvent,
storageClearEvent,
storageLoadEvent,
storageStoreEvent,
makeStorageAcknowledgementEvent
} from '../../events/StorageEvents'
storageStoreEvent
} from '../../events/StorageEvents'
export default class ConfigStorage {
private file: string
@@ -17,6 +19,10 @@ export default class ConfigStorage {
}
private async getDb() {
const pathInfo = path.parse(this.file)
// Ensure that Settings dir exists
await fs.mkdirp(pathInfo.dir)
const adapter = new FileAsync(this.file)
if (!this.database) {
this.database = await lowdb(adapter)
+1 -1
View File
@@ -7,7 +7,7 @@ export interface DataSourceState {
}
export class DataSourceStateMachine {
public onUpdate = new EventDispatcher<DataSourceState, DataSourceStateMachine>(this)
public onUpdate = new EventDispatcher<DataSourceState, DataSourceStateMachine>()
private state: DataSourceState = {
error: undefined,
connected: false,
+8 -1
View File
@@ -16,7 +16,7 @@ export class Tree<ViewModel> extends TreeNode<ViewModel> {
public isTree = true
private cachedHash = `${Math.random()}`
private unmergedMessages: ChangeBuffer = new ChangeBuffer()
public didReceive = new EventDispatcher<void, Tree<ViewModel>>(this)
public didReceive = new EventDispatcher<void, Tree<ViewModel>>()
constructor() {
super(undefined, undefined)
@@ -27,6 +27,13 @@ export class Tree<ViewModel> extends TreeNode<ViewModel> {
this.didReceive.dispatch()
}
public destroy() {
super.destroy()
this.updateSource && this.updateSource.unsubscribe(this.subscriptionEvent, this.handleNewData)
this.updateSource = undefined
this.didReceive.removeAllListeners()
}
public updateWithConnection(emitter: EventBusInterface, connectionId: string, nodeFilter?: (node: TreeNode<ViewModel>) => boolean) {
this.updateSource = emitter
this.connectionId = connectionId
+18 -3
View File
@@ -12,9 +12,9 @@ export class TreeNode<ViewModel> {
public collapsed = false
public messages: number = 0
public lastUpdate: number = Date.now()
public onMerge = new EventDispatcher<void, TreeNode<ViewModel>>(this)
public onEdgesChange = new EventDispatcher<void, TreeNode<ViewModel>>(this)
public onMessage = new EventDispatcher<Message, TreeNode<ViewModel>>(this)
public onMerge = new EventDispatcher<void, TreeNode<ViewModel>>()
public onEdgesChange = new EventDispatcher<void, TreeNode<ViewModel>>()
public onMessage = new EventDispatcher<Message, TreeNode<ViewModel>>()
public isTree = false
private cachedPath?: string
@@ -99,6 +99,21 @@ export class TreeNode<ViewModel> {
}
}
public destroy() {
for (const edge of this.edgeArray) {
edge.target.destroy()
}
this.edgeArray = []
this.edges = {}
this.cachedChildTopics = []
this.sourceEdge = undefined
this.onMerge.removeAllListeners()
this.onEdgesChange.removeAllListeners()
this.onMessage.removeAllListeners()
this.messageHistory = new RingBuffer<Message>(1, 1)
this.message = undefined
}
public unconnectedClone() {
const node = new TreeNode<ViewModel>()
node.message = this.message
@@ -1,11 +1,10 @@
import 'mocha'
import { EventDispatcher } from '../../../../events'
import { expect } from 'chai'
import 'mocha'
describe('EventDispatcher', async () => {
it('should dispatch', async function () {
const dispatcher = new EventDispatcher<string, string>('me')
it('should dispatch', async function() {
const dispatcher = new EventDispatcher<string, string>()
this.timeout(300)
setTimeout(() => dispatcher.dispatch('hello'), 5)
@@ -18,8 +17,8 @@ describe('EventDispatcher', async () => {
expect(response).to.eq('hello')
})
it('should unsubscribe', async function () {
const dispatcher = new EventDispatcher<string, string>('me')
it('should unsubscribe', async function() {
const dispatcher = new EventDispatcher<string, string>()
this.timeout(300)
let incrementee = 0
const callback = (msg: any) => {
+1 -1
View File
@@ -78,7 +78,7 @@ export class ConnectionManager {
}
class UpdateNotifier {
public onCheckUpdateRequest = new EventDispatcher<void, UpdateNotifier>(this)
public onCheckUpdateRequest = new EventDispatcher<void, UpdateNotifier>()
constructor() {
backendEvents.subscribe(checkForUpdates, () => {
this.onCheckUpdateRequest.dispatch()
+3 -8
View File
@@ -7,20 +7,15 @@ interface CallbackStore {
export class EventDispatcher<Message, Dispatcher> {
private emitter = new EventEmitter()
private dispatcher: Dispatcher
private callbacks: Array<CallbackStore> = []
constructor(dispatcher: Dispatcher) {
this.dispatcher = dispatcher
}
public dispatch(msg: Message) {
this.emitter.emit('event', msg)
}
public subscribe(callback: (msg: Message, dispatcher: Dispatcher) => void) {
public subscribe(callback: (msg: Message) => void) {
const wrappedCallback = (msg: Message) => {
callback(msg, this.dispatcher)
callback(msg)
}
this.emitter.on('event', wrappedCallback)
@@ -30,7 +25,7 @@ export class EventDispatcher<Message, Dispatcher> {
})
}
public unsubscribe(callback: (msg: Message, dispatcher: Dispatcher) => void) {
public unsubscribe(callback: (msg: Message) => void) {
const item = this.callbacks.find(store => store.callback === callback)
if (!item) {
return
+2 -2
View File
@@ -32,7 +32,6 @@
"publish": [
"github"
],
"provisioningProfile": "res/MQTT_Explorer_Store_Distribution_Profile.provisionprofile",
"entitlements": "res/entitlements.mas.plist"
},
"linux": {
@@ -70,7 +69,7 @@
"app-builder-lib": "https://github.com/thomasnordquist/app-builder-lib.git",
"axios": "^0.18.0",
"chai": "^4.2.0",
"electron": "4",
"electron": "5",
"electron-builder": "^20.38.5",
"fs-extra": "^7.0.1",
"mime": "^2.4.0",
@@ -91,6 +90,7 @@
},
"dependencies": {
"about-window": "^1.12.1",
"dot-prop": "^5.0.0",
"electron-log": "^2.2.17",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
"electron-updater": "^4.0.6",
+28 -11
View File
@@ -1,6 +1,7 @@
import * as builder from 'electron-builder'
import * as fs from 'fs'
import * as path from 'path'
import * as dotProp from 'dot-prop'
const linuxAppImage: builder.CliOptions = {
x64: true,
@@ -88,20 +89,36 @@ type Packages = 'portable' | 'nsis' | 'appx' | 'AppImage' | 'snap' | 'dmg' | 'zi
async function buildWithOptions(options: builder.CliOptions, buildInfo: BuildInfo) {
fs.writeFileSync(path.join(options.projectDir!, 'buildOptions.json'), JSON.stringify(buildInfo))
ensureAppNameForPackage(options, buildInfo.package)
await builder.build({
...options,
[buildInfo.platform]: [buildInfo.package],
})
}
// AppX must hav a different name since the store name is already taken (but not used)
function ensureAppNameForPackage(options: builder.CliOptions, packageOption: Packages) {
const jsonLocation = path.join((options.projectDir as string), 'package.json')
const packageJsonStr = fs.readFileSync(jsonLocation).toString()
const packageJson = JSON.parse(fs.readFileSync(jsonLocation).toString())
packageJson.build.productName = packageOption === 'appx' ? 'MQTT-Explorer' : 'MQTT Explorer'
fs.writeFileSync(jsonLocation, JSON.stringify(packageJson, undefined, ' '))
// AppX must have a different name since the store name is already taken (but not used)
if (buildInfo.package === 'appx') {
dotProp.set(packageJson, 'build.productName', 'MQTT-Explorer')
}
if (buildInfo.platform === 'mac') {
console.log(buildInfo.package)
const provisioningProfile = (buildInfo.package === 'mas') ? 'res/MQTT_Explorer_Store_Distribution_Profile.provisionprofile' : 'res/MQTTExplorerdmg.provisionprofile'
dotProp.set(packageJson, 'build.mac.provisioningProfile', provisioningProfile)
}
try {
// Write modified package.json
fs.writeFileSync(jsonLocation, JSON.stringify(packageJson))
await builder.build({
...options,
[buildInfo.platform]: [buildInfo.package],
})
} catch (error) {
throw error
} finally {
// Roll back changes to package.json
fs.writeFileSync(jsonLocation, packageJsonStr)
}
}
function build() {
Binary file not shown.
+1 -1
View File
@@ -36,7 +36,7 @@ rm ./app.mp4 || echo no need to delete ./app.mp4
tmux new-session -d -s record ffmpeg -f x11grab -draw_mouse 0 -video_size $DIMENSIONS -i :$SCR -r 20 -vcodec rawvideo -pix_fmt yuv420p qrawvideorgb24.yuv
# Start tests
node dist/src/spec/webdriverio.js
node dist/src/spec/demoVideo.js
TEST_EXIT_CODE=$?
echo "Webriver exitet with $TEST_EXIT_CODE"
+4 -6
View File
@@ -93,9 +93,8 @@ const viewMenu: MenuItemConstructorOptions = {
click: () => {
const window = BrowserWindow.getFocusedWindow()
if (window) {
window.webContents.getZoomFactor((zoom) => {
window.webContents.setZoomFactor(Math.min(zoom + 0.1, 2.0))
})
const zoom = window.webContents.getZoomFactor()
window.webContents.setZoomFactor(Math.min(zoom + 0.1, 2.0))
}
},
},
@@ -105,9 +104,8 @@ const viewMenu: MenuItemConstructorOptions = {
click: () => {
const window = BrowserWindow.getFocusedWindow()
if (window) {
window.webContents.getZoomFactor((zoom) => {
window.webContents.setZoomFactor(Math.max(zoom - 0.1, 0.5))
})
const zoom = window.webContents.getZoomFactor()
window.webContents.setZoomFactor(Math.max(zoom - 0.1, 0.5))
}
},
},
+3 -3
View File
@@ -18,9 +18,9 @@ export async function waitForDevServer() {
export function loadDevTools() {
// Redux
BrowserWindow.addDevToolsExtension(
path.join(os.homedir(), '/Library/Application Support/Google/Chrome/Default/Extensions/lmhkpmbekcpmknklioeibfkpmmfibljd/2.17.0_0/')
)
// BrowserWindow.addDevToolsExtension(
// path.join(os.homedir(), '/Library/Application Support/Google/Chrome/Default/Extensions/lmhkpmbekcpmknklioeibfkpmmfibljd/2.17.0_0/')
// )
}
export function isDev() {
+4 -1
View File
@@ -14,6 +14,8 @@ if (!isDev() && !runningUiTestOnCi()) {
const electronTelemetry = electronTelemetryFactory('9b0c8ca04a361eb8160d98c5', buildOptions)
}
app.commandLine.appendSwitch('--no-sandbox')
autoUpdater.logger = log
log.info('App starting...')
@@ -42,13 +44,14 @@ async function createWindow() {
webPreferences: {
nodeIntegration: true,
devTools: true,
sandbox: false,
},
icon: iconPath,
})
mainWindow.once('ready-to-show', () => {
if (mainWindow) {
runningUiTestOnCi && mainWindow.setFullScreen(true)
runningUiTestOnCi() && mainWindow.setFullScreen(true)
mainWindow.show()
}
})
@@ -23,7 +23,7 @@ import {
createFakeMousePointer,
hideText,
showText,
sleep
sleep,
} from './util'
process.on('unhandledRejection', (error: Error) => {
@@ -56,7 +56,12 @@ async function doStuff() {
await createFakeMousePointer(browser)
// Wait for Username input to be visible
await browser.$(`//label[contains(text(), "Username")]/..//input`)
let inputField = undefined
let start = Date.now()
let maxWaitDuration = 30000
while ((!inputField || !inputField.isExisting) && ((Date.now() - start) < maxWaitDuration)) {
inputField = await browser.$(`//label[contains(text(), "Username")]/..//input`)
}
const scenes = new SceneBuilder()
await scenes.record('connect', async () => {
await connectTo('127.0.0.1', browser)
+122
View File
@@ -0,0 +1,122 @@
import * as fs from 'fs'
import * as os from 'os'
import * as webdriverio from 'webdriverio'
import mockMqtt, { stopUpdates as stopMqttUpdates } from './mock-mqtt'
import {
ClassNameMapping,
countInstancesOf,
createFakeMousePointer,
getHeapDump,
setFast,
sleep
} from './util'
import { clearSearch, searchTree } from './scenarios/searchTree'
import { connectTo } from './scenarios/connect'
import { reconnect } from './scenarios/reconnect'
process.on('unhandledRejection', (error: Error) => {
console.error('unhandledRejection', error.message, error.stack)
process.exit(1)
})
const runningUiTestOnCi = os.platform() === 'darwin' ? [] : ['--runningUiTestOnCi']
console.log(`${__dirname}/../../../node_modules/.bin/electron`)
const options = {
host: '127.0.0.1', // Use localhost as chrome driver server
port: 9515, // "9515" is the port opened by chrome driver.
capabilities: {
browserName: 'electron',
chromeOptions: {
binary: `${__dirname}/../../../node_modules/.bin/electron`,
args: [`--app=${__dirname}/../../..`, '--force-device-scale-factor=1', '--no-sandbox', '--disable-dev-shm-usage', '--disable-extensions'].concat(runningUiTestOnCi),
},
windowTypes: ['app', 'webview'],
},
}
async function doStuff() {
console.log('Waiting for MQTT Broker on port 1880 (no auth)')
await mockMqtt()
console.log('start webdriver')
const browser = await webdriverio.remote(options)
setFast()
await createFakeMousePointer(browser)
// Wait for Username input to be visible
await browser.$('//label[contains(text(), "Username")]/..//input')
await connectTo('127.0.0.1', browser)
stopMqttUpdates()
await sleep(1000, true)
let heapDump = await getHeapDump(browser)
const initialTreeOccurrances = await countInstancesOf(heapDump, ClassNameMapping.Tree)
const initialNodeOccurrances = await countInstancesOf(heapDump, ClassNameMapping.TreeNode)
console.log(initialTreeOccurrances, initialNodeOccurrances)
await doX(3, async () => {
await reconnect(browser)
})
await sleep(1000, true)
await doX(15, async () => {
await searchTree('temp', browser)
await reconnect(browser)
})
await searchTree('ab', browser)
await clearSearch(browser)
await searchTree('temp', browser)
await clearSearch(browser)
await sleep(1000, true)
await waitForGarbageCollectorToDetermineLeak(browser, initialTreeOccurrances, initialNodeOccurrances)
}
async function waitForGarbageCollectorToDetermineLeak(browser: any, initialTreeOccurrances: number, initialNodeOccurrances: number) {
let delta = -1
let lastTreeOccurances = -1
let lastNodeOccurances = -1
let leak = false
while (delta < 0) {
if (lastTreeOccurances !== -1) {
await sleep(10000, true)
}
const heapDump = await getHeapDump(browser)
const currentTreeOccurrances = await countInstancesOf(heapDump, ClassNameMapping.Tree)
const currentNodeOccurrances = await countInstancesOf(heapDump, ClassNameMapping.TreeNode)
// Temporary "leaks" are expected due to React Fibers memoization
if (Math.abs(initialTreeOccurrances - currentTreeOccurrances) > 1 || Math.abs(currentNodeOccurrances - initialNodeOccurrances) > 8) {
console.error('Possible leak detected', initialTreeOccurrances, currentTreeOccurrances, initialNodeOccurrances, currentNodeOccurrances)
leak = true
} else {
leak = false
}
const treeDelta = lastTreeOccurances >= 0 ? currentTreeOccurrances - lastTreeOccurances : -1
const nodeDelta = lastTreeOccurances >= 0 ? currentNodeOccurrances - lastNodeOccurances : -1
delta = treeDelta + nodeDelta
lastTreeOccurances = currentTreeOccurrances
lastNodeOccurances = currentNodeOccurrances
}
if (leak) {
console.error('leak')
process.exit(100)
}
}
async function doX(x: number, action: () => Promise<any>) {
for (let i = 0; i < x; i += 1) {
await action()
await sleep(10, true)
}
}
doStuff()
+7 -2
View File
@@ -30,16 +30,21 @@ function temperature(base = 18, sineCoefficient = 2, offset = 0) {
return String(Math.round(temp * 100) / 100)
}
export function stop() {
export function stopUpdates() {
for (const interval of intervals) {
clearInterval(interval)
}
intervals = []
}
export function stop() {
stopUpdates()
try {
client && client.end()
} catch {}
}
const intervals: any = []
let intervals: any = []
function generateData(client: mqtt.MqttClient) {
client.publish('livingroom/lamp/state', 'on', { retain: true, qos: 0 })
+1 -1
View File
@@ -9,5 +9,5 @@ export async function connectTo(host: string, browser: Browser<void>) {
await browser.saveScreenshot('screen1.png')
const connectButton = await browser.$('//button/span[contains(text(),"Connect")]')
clickOn(connectButton, browser)
await clickOn(connectButton, browser)
}
+2 -2
View File
@@ -2,6 +2,6 @@ import { clickOn } from '../util'
import { Browser } from 'webdriverio'
export async function disconnect(browser: Browser<void>) {
const connectButton = await browser.$('//button/span[contains(text(),"Disconnect")]')
clickOn(connectButton, browser)
const disconnectButton = await browser.$('//button/span[contains(text(),"Disconnect")]')
await clickOn(disconnectButton, browser)
}
+9
View File
@@ -0,0 +1,9 @@
import { clickOn } from '../util'
import { Browser } from 'webdriverio'
export async function reconnect(browser: Browser<void>) {
const disconnectButton = await browser.$('//button/span[contains(text(),"Disconnect")]')
await clickOn(disconnectButton, browser)
const connectButton = await browser.$('//button/span[contains(text(),"Connect")]')
await clickOn(connectButton, browser)
}
+41 -6
View File
@@ -1,17 +1,27 @@
import * as fs from 'fs'
import { Browser, Element } from 'webdriverio'
export { expandTopic } from './expandTopic'
let fast = false
export function setFast() {
fast = true
}
export function sleep(ms: number, required = false) {
return new Promise((resolve) => {
if (required) {
setTimeout(resolve, ms)
} else {
setTimeout(resolve, ms)
setTimeout(resolve, fast ? 0 : ms)
}
})
}
export async function writeText(text: string, browser: Browser<void>, delay = 0) {
if (fast) {
return browser.keys(text.split(''))
}
for (const c of text.split('')) {
await browser.keys([c])
await sleep(delay)
@@ -42,11 +52,12 @@ export async function moveToCenterOfElement(element: Element<void>, browser: Bro
const targetX = x + width / 2
const targetY = y + height / 2
const duration = 500
const duration = fast ? 1 : 500
const js = `window.demo.moveMouse(${targetX}, ${targetY}, ${duration});`
await browser.execute(js)
await sleep(duration + 500, true)
await sleep(duration)
await sleep(250, true)
await element.moveTo()
}
@@ -73,17 +84,41 @@ export async function createFakeMousePointer(browser: Browser<void>) {
export async function showText(text: string, duration: number = 0, browser: Browser<void>, location: 'top' | 'bottom' | 'middle' = 'bottom', keys = []) {
const js = `window.demo.showMessage('${text}', '${location}', ${duration});`
browser.execute(js)
await browser.execute(js)
}
type HeapDump = any
export async function getHeapDump(browser: Browser<void>): Promise<HeapDump> {
const filename = 'heapdump.json'
const js = `window.demo.writeHeapdump('${filename}');`
await browser.execute(js)
const buffer = fs.readFileSync(filename)
fs.unlinkSync(filename)
return JSON.parse(buffer.toString())
}
export enum ClassNameMapping {
TreeNode = 'TreeNode_TreeNode',
TreeNodeComponent = 'TreeNode_TreeNodeComponent',
Tree = 'Tree_Tree',
}
export async function countInstancesOf(heapDump: HeapDump, className: ClassNameMapping): Promise<number> {
return heapDump.nodes
.map((idx: number) => heapDump.strings[idx])
.filter((s: string) => s === className).length
}
export async function showKeys(text: string, duration: number = 0, browser: Browser<void>, location: 'top' | 'bottom' | 'middle' = 'bottom', keys: string[] = []) {
const js = `window.demo.showMessage('${text}', '${location}', ${duration}, ${JSON.stringify(keys)});`
browser.execute(js)
await browser.execute(js)
}
export async function hideText(browser: Browser<void>) {
const js = 'window.demo.hideMessage();'
browser.execute(js)
await browser.execute(js)
await sleep(600)
}
+1 -1
View File
@@ -11,5 +11,5 @@
"lib": ["es2017", "dom"],
"sourceMap": true
},
"include": ["src/electron.ts", "src/spec/electron.ts", "src/spec/webdriverio.ts", "scripts/*.ts"]
"include": ["src/electron.ts", "src/spec/electron.ts", "src/spec/demoVideo.ts", "src/spec/leakTest.ts", "scripts/*.ts"]
}
+11 -4
View File
@@ -1131,6 +1131,13 @@ dot-prop@^4.1.0:
dependencies:
is-obj "^1.0.0"
dot-prop@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.0.0.tgz#64b7968af349c3a9f966aa12658dbd5829f6b953"
integrity sha512-RTmaF2jx3nOBO2GvtFqjnDLycjFUMqt+2pwRx7JVYa81lDauoj9aNkyrJI2ikR58FbBIchiIlRiGG+muLJ4oHQ==
dependencies:
is-obj "^1.0.0"
dotenv-expand@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-4.2.0.tgz#def1f1ca5d6059d24a766e587942c21106ce1275"
@@ -1287,10 +1294,10 @@ electron-updater@^4.0.6:
semver "^5.6.0"
source-map-support "^0.5.9"
electron@4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/electron/-/electron-4.1.4.tgz#41ba9e041f38c25c62a7db806884410654df058f"
integrity sha512-MelOjntJvd33izEjR6H4N/Uii7y535z/b2BuYXJGLNSHL6o1IlyhUQmfiT87kWABayERgeuYERgvsyf956OOFw==
electron@5:
version "5.0.0"
resolved "https://registry.yarnpkg.com/electron/-/electron-5.0.0.tgz#d8352e2c9625b3be0112ce0c1bf5c9b6f692557e"
integrity sha512-++emIe4vLihiYiAVL+E8DT5vSNVFEIuQCRxA+VfpDRVBcog85UB28vi4ogRmMOK3UffzKdWV6e1jqp3T0KpBoA==
dependencies:
"@types/node" "^10.12.18"
electron-download "^4.1.0"