Skip to content

Commit

Permalink
Enable Artemis to hook into the React render timeline for more reliab…
Browse files Browse the repository at this point in the history
…le tests (#296)
  • Loading branch information
pal03377 authored Sep 2, 2023
1 parent a9c5d19 commit 88a68f2
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 186 deletions.
101 changes: 58 additions & 43 deletions src/main/apollon-editor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'pepjs';
import { createElement, createRef, RefObject } from 'react';
import { createElement } from 'react';
import { createRoot, Root } from 'react-dom/client';
import { DeepPartial, Store } from 'redux';
import { ModelState, PartialModelState } from './components/store/model-state';
Expand All @@ -21,31 +21,35 @@ import { ErrorBoundary } from './components/controls/error-boundary/ErrorBoundar
import { replaceColorVariables } from './utils/replace-color-variables';

export class ApollonEditor {
private ensureInitialized() {
if (!this.store) {
// tslint:disable-next-line:no-console
console.error(
'The application state of Apollon could not be retrieved. The editor may already be destroyed or you might need to `await apollonEditor.nextRender`.',
);
throw new Error(
'The application state of Apollon could not be retrieved. The editor may already be destroyed or you might need to `await apollonEditor.nextRender`.',
);
}
}

/**
* Returns the current model of the Apollon Editor
*/
get model(): Apollon.UMLModel {
if (!this.store) {
// tslint:disable-next-line:no-console
console.error('The application state of Apollon could not be retrieved. The editor may already be destroyed.');
throw new Error('The application state of Apollon could not be retrieved. The editor may already be destroyed.');
}
return ModelState.toModel(this.store.getState());
this.ensureInitialized();
return ModelState.toModel(this.store!.getState());
}

/**
* Sets a model as the current model of the Apollon Editor
* @param model valid Apollon Editor Model
*/
set model(model: Apollon.UMLModel) {
if (!this.store) {
// tslint:disable-next-line:no-console
console.error('The application state of Apollon could not be retrieved. The editor may already be destroyed.');
throw new Error('The application state of Apollon could not be retrieved. The editor may already be destroyed.');
}
this.ensureInitialized();
const state: PartialModelState = {
...ModelState.fromModel(model),
editor: { ...this.store.getState().editor },
editor: { ...this.store!.getState().editor },
};
this.recreateEditor(state);
}
Expand All @@ -55,13 +59,9 @@ export class ApollonEditor {
* @param diagramType the new diagram type
*/
set type(diagramType: UMLDiagramType) {
if (!this.store) {
// tslint:disable-next-line:no-console
console.error('The application state of Apollon could not be retrieved. The editor may already be destroyed.');
throw new Error('The application state of Apollon could not be retrieved. The editor may already be destroyed.');
}
this.ensureInitialized();
const state: PartialModelState = {
...this.store.getState(),
...this.store!.getState(),
diagram: new UMLDiagram({
type: diagramType,
}),
Expand All @@ -75,12 +75,8 @@ export class ApollonEditor {
* @param locale supported locale
*/
set locale(locale: Locale) {
if (!this.store) {
// tslint:disable-next-line:no-console
console.error('The application state of Apollon could not be retrieved. The editor may already be destroyed.');
throw new Error('The application state of Apollon could not be retrieved. The editor may already be destroyed.');
}
const state = this.store.getState();
this.ensureInitialized();
const state = this.store!.getState();
this.options.locale = locale;
this.recreateEditor(state);
}
Expand All @@ -101,7 +97,7 @@ export class ApollonEditor {
const element = createElement(Svg, { model, options, styles: theme });
const svg = new Svg({ model, options, styles: theme });
root.render(element);
await delay(0);
await delay(50);

return {
svg: replaceColorVariables(container.querySelector('svg')!.outerHTML),
Expand All @@ -113,12 +109,13 @@ export class ApollonEditor {
private root?: Root;
private currentModelState?: ModelState;
private assessments: Apollon.Assessment[] = [];
private application: RefObject<Application> = createRef();
private application: Application | null = null;
private selectionSubscribers: { [key: number]: (selection: Apollon.Selection) => void } = {};
private assessmentSubscribers: { [key: number]: (assessments: Apollon.Assessment[]) => void } = {};
private modelSubscribers: { [key: number]: (model: Apollon.UMLModel) => void } = {};
private discreteModelSubscribers: { [key: number]: (model: Apollon.UMLModel) => void } = {};
private errorSubscribers: { [key: number]: (error: Error) => void } = {};
private nextRenderPromise: Promise<void>;

constructor(
private container: HTMLElement,
Expand Down Expand Up @@ -154,8 +151,19 @@ export class ApollonEditor {
},
};

let nextRenderResolve: () => void;
this.nextRenderPromise = new Promise((resolve) => {
nextRenderResolve = resolve;
});

const element = createElement(Application, {
ref: this.application,
ref: async (app) => {
if (app == null) return;
this.application = app;
await app.initialized;
this.store!.subscribe(this.onDispatch);
nextRenderResolve();
},
state,
styles: options.theme,
locale: options.locale,
Expand Down Expand Up @@ -309,14 +317,6 @@ export class ApollonEditor {

private componentDidMount = () => {
this.container.setAttribute('touch-action', 'none');

setTimeout(() => {
if (this.store) {
this.store.subscribe(this.onDispatch);
} else {
setTimeout(this.componentDidMount, 100);
}
});
};

/**
Expand Down Expand Up @@ -401,8 +401,19 @@ export class ApollonEditor {
private recreateEditor(state: PartialModelState) {
this.destroy();

let nextRenderResolve: () => void;
this.nextRenderPromise = new Promise((resolve) => {
nextRenderResolve = resolve;
});

const element = createElement(Application, {
ref: this.application,
ref: async (app) => {
if (app == null) return;
this.application = app;
await app.initialized;
this.store!.subscribe(this.onDispatch);
nextRenderResolve();
},
state,
styles: this.options.theme,
locale: this.options.locale,
Expand Down Expand Up @@ -434,11 +445,15 @@ export class ApollonEditor {
}
}

private get store(): Store<ModelState, Actions> | null {
return (
this.application.current &&
this.application.current.store.current &&
this.application.current.store.current.state.store
);
private get store(): Store<ModelState, Actions> | undefined {
return this.application?.store?.state.store;
}

/**
* Returns a Promise that resolves when the current React render cycle is finished.
* => this.store is be available and there should be no errors when trying to access some methods like this.model
*/
get nextRender(): Promise<void> {
return this.nextRenderPromise;
}
}
21 changes: 18 additions & 3 deletions src/main/scenes/application.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { createRef, RefObject } from 'react';
import React, { RefObject } from 'react';
import { DeepPartial } from 'redux';
import { Canvas, CanvasComponent } from '../components/canvas/canvas';
import { CanvasContext, CanvasProvider } from '../components/canvas/canvas-context';
Expand Down Expand Up @@ -33,7 +33,12 @@ type State = typeof initialState;
export class Application extends React.Component<Props, State> {
state = initialState;

store: RefObject<ModelStore> = createRef();
store?: ModelStore;

private resolveInitialized: () => void = () => undefined;
private initializedPromise: Promise<void> = new Promise((resolve) => {
this.resolveInitialized = resolve;
});

setCanvas = (ref: CanvasComponent) => {
if (ref && ref.layer.current) {
Expand All @@ -54,7 +59,13 @@ export class Application extends React.Component<Props, State> {
return (
<CanvasProvider value={canvasContext}>
<RootProvider value={rootContext}>
<StoreProvider ref={this.store} initialState={this.props.state}>
<StoreProvider
initialState={this.props.state}
ref={(ref) => {
this.store ??= ref as ModelStore;
this.resolveInitialized();
}}
>
<I18nProvider locale={this.props.locale}>
<Theme styles={this.props.styles} scale={this.props.state?.editor?.scale}>
<Layout className="apollon-editor" ref={this.setLayout}>
Expand All @@ -80,4 +91,8 @@ export class Application extends React.Component<Props, State> {
</CanvasProvider>
);
}

get initialized(): Promise<void> {
return this.initializedPromise;
}
}
Loading

0 comments on commit 88a68f2

Please sign in to comment.