Skip to content

Commit

Permalink
feat(all): add done callback (#549)
Browse files Browse the repository at this point in the history
fixes #545
  • Loading branch information
unstubbable authored Dec 17, 2019
1 parent d4c8feb commit dd9f6db
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 18 deletions.
24 changes: 24 additions & 0 deletions packages/core/src/__tests__/feature-app-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,30 @@ describe('FeatureAppManager', () => {
});
});

describe('with a done callback', () => {
it('passes the done callback as part of the env to the Feature App', () => {
const featureAppId = 'testId';

featureAppManager = new FeatureAppManager(mockFeatureServiceRegistry, {
logger
});

const mockDone = jest.fn();

featureAppManager.createFeatureAppScope(
featureAppId,
mockFeatureAppDefinition,
{done: mockDone}
);

const {featureServices} = mockFeatureServicesBinding;

expect(mockFeatureAppCreate.mock.calls).toEqual([
[{featureServices, featureAppId, done: mockDone}]
]);
});
});

describe('without an ExternalsValidator provided to the FeatureAppManager', () => {
describe('with a Feature App definition that is declaring external dependencies', () => {
beforeEach(() => {
Expand Down
20 changes: 18 additions & 2 deletions packages/core/src/feature-app-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ export interface FeatureAppEnvironment<
* The absolute or relative base URL of the Feature App's assets and/or BFF.
*/
readonly baseUrl: string | undefined;

/**
* If this callback is defined, a short-lived Feature App can call this
* function when it has completed its task. The Integrator (or parent Feature
* App) can then decide to e.g. unmount the Feature App.
*/
readonly done?: () => void;
}

export interface FeatureAppDefinition<
Expand Down Expand Up @@ -81,6 +88,14 @@ export interface FeatureAppScopeOptions<
readonly beforeCreate?: (
env: FeatureAppEnvironment<TFeatureServices, TConfig>
) => void;

/**
* A callback that is passed to the Feature App's `create` method. A
* short-lived Feature App can call this function when it has completed its
* task. The Integrator (or parent Feature App) can then decide to e.g.
* unmount the Feature App.
*/
readonly done?: () => void;
}

export interface FeatureAppManagerOptions {
Expand Down Expand Up @@ -347,7 +362,7 @@ export class FeatureAppManager {
): FeatureAppRetainer<TFeatureApp> {
this.validateExternals(featureAppDefinition);

const {baseUrl, beforeCreate, config} = options;
const {baseUrl, beforeCreate, config, done} = options;

const binding = this.featureServiceRegistry.bindFeatureServices(
featureAppDefinition,
Expand All @@ -358,7 +373,8 @@ export class FeatureAppManager {
baseUrl,
config,
featureAppId,
featureServices: binding.featureServices as TFeatureServices
featureServices: binding.featureServices as TFeatureServices,
done
};

if (beforeCreate) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Text} from '@blueprintjs/core';
import {Button} from '@blueprintjs/core';
import {FeatureAppDefinition} from '@feature-hub/core';
import {ReactFeatureApp} from '@feature-hub/react';
import * as React from 'react';
Expand All @@ -11,8 +11,13 @@ const featureAppDefinition: FeatureAppDefinition<ReactFeatureApp> = {
}
},

create: () => ({
render: () => <Text>Hello, World!</Text>
create: ({done}) => ({
render: () => (
<>
<p>Hello, World!</p>
<Button text="I'm done" onClick={() => done && done()}></Button>
</>
)
})
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ import {FeatureAppContainer, ReactFeatureApp} from '@feature-hub/react';
import * as React from 'react';
import innerFeatureAppDefinition from './feature-app-inner';

interface OuterFeatureAppProps {
readonly featureAppId: string;
}

function OuterFeatureApp({featureAppId}: OuterFeatureAppProps): JSX.Element {
const [done, setDone] = React.useState(false);

return (
<Card style={{margin: '20px'}}>
{done ? (
'Bye!'
) : (
<FeatureAppContainer
featureAppDefinition={innerFeatureAppDefinition}
featureAppId={`${featureAppId}:test:hello-world-inner`}
done={() => setDone(true)}
/>
)}
</Card>
);
}

const featureAppDefinition: FeatureAppDefinition<ReactFeatureApp> = {
dependencies: {
externals: {
Expand All @@ -13,14 +35,7 @@ const featureAppDefinition: FeatureAppDefinition<ReactFeatureApp> = {
},

create: ({featureAppId}) => ({
render: () => (
<Card style={{margin: '20px'}}>
<FeatureAppContainer
featureAppDefinition={innerFeatureAppDefinition}
featureAppId={`${featureAppId}:test:hello-world-inner`}
/>
</Card>
)
render: () => <OuterFeatureApp featureAppId={featureAppId} />
})
};

Expand Down
9 changes: 9 additions & 0 deletions packages/demos/src/feature-app-in-feature-app/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,13 @@ describe('integration test: "Feature App in Feature App"', () => {
it('renders a Feature App with another nested Feature App', async () => {
await expect(page).toMatch('Hello, World!');
});

it("unmounts the nested Feature App when it has signaled that it's done", async () => {
// tslint:disable-next-line:no-non-null-assertion
const button = (await page.$('button'))!;

await button.click();

await expect(page).toMatch('Bye!');
});
});
7 changes: 5 additions & 2 deletions packages/react/src/__tests__/feature-app-container.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ describe('FeatureAppContainer', () => {
testRendererOptions
);

it('calls the Feature App manager with the given featureAppDefinition, featureAppId, config, baseUrl, and beforeCreate callback', () => {
it('calls the Feature App manager with the given featureAppDefinition, featureAppId, config, baseUrl, beforeCreate callback, and done callback', () => {
const mockBeforeCreate = jest.fn();
const mockDone = jest.fn();

renderWithFeatureHubContext(
<FeatureAppContainer
Expand All @@ -109,13 +110,15 @@ describe('FeatureAppContainer', () => {
config="testConfig"
baseUrl="/base"
beforeCreate={mockBeforeCreate}
done={mockDone}
/>
);

const expectedOptions: FeatureAppScopeOptions<{}, string> = {
baseUrl: '/base',
config: 'testConfig',
beforeCreate: mockBeforeCreate
beforeCreate: mockBeforeCreate,
done: mockDone
};

expect(mockCreateFeatureAppScope.mock.calls).toEqual([
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/__tests__/feature-app-loader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -289,13 +289,15 @@ describe('FeatureAppLoader', () => {
const onError = jest.fn();
const renderError = jest.fn();
const beforeCreate = jest.fn();
const done = jest.fn();

const testRenderer = renderWithFeatureHubContext(
<FeatureAppLoader
src="example.js"
featureAppId="testId"
config="testConfig"
beforeCreate={beforeCreate}
done={done}
onError={onError}
renderError={renderError}
baseUrl="/base"
Expand All @@ -307,6 +309,7 @@ describe('FeatureAppLoader', () => {
const expectedProps = {
baseUrl: '/base',
beforeCreate,
done,
config: 'testConfig',
featureAppDefinition: mockFeatureAppDefinition,
featureAppId: 'testId',
Expand Down
13 changes: 11 additions & 2 deletions packages/react/src/feature-app-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ export interface FeatureAppContainerProps<
env: FeatureAppEnvironment<TFeatureServices, TConfig>
) => void;

/**
* A callback that is passed to the Feature App's `create` method. A
* short-lived Feature App can call this function when it has completed its
* task. The Integrator (or parent Feature App) can then decide to e.g.
* unmount the Feature App.
*/
readonly done?: () => void;

readonly onError?: (error: Error) => void;

readonly renderError?: (error: Error) => React.ReactNode;
Expand Down Expand Up @@ -129,14 +137,15 @@ class InternalFeatureAppContainer<
config,
featureAppDefinition,
featureAppId,
featureAppManager
featureAppManager,
done
} = props;

try {
this.featureAppScope = featureAppManager.createFeatureAppScope(
featureAppId,
featureAppDefinition,
{baseUrl, config, beforeCreate}
{baseUrl, config, beforeCreate, done}
);

if (!isFeatureApp(this.featureAppScope.featureApp)) {
Expand Down
12 changes: 11 additions & 1 deletion packages/react/src/feature-app-loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ export interface FeatureAppLoaderProps<TConfig = unknown> {
env: FeatureAppEnvironment<FeatureServices, TConfig>
) => void;

/**
* A callback that is passed to the Feature App's `create` method. A
* short-lived Feature App can call this function when it has completed its
* task. The Integrator (or parent Feature App) can then decide to e.g.
* unmount the Feature App.
*/
readonly done?: () => void;

readonly onError?: (error: Error) => void;

readonly renderError?: (error: Error) => React.ReactNode;
Expand Down Expand Up @@ -175,7 +183,8 @@ class InternalFeatureAppLoader<TConfig = unknown> extends React.PureComponent<
config,
featureAppId,
onError,
renderError
renderError,
done
} = this.props;

const {error, failedToHandleAsyncError, featureAppDefinition} = this.state;
Expand Down Expand Up @@ -204,6 +213,7 @@ class InternalFeatureAppLoader<TConfig = unknown> extends React.PureComponent<
}
onError={onError}
renderError={renderError}
done={done}
/>
);
}
Expand Down

0 comments on commit dd9f6db

Please sign in to comment.