-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
dce8cf9
commit c060939
Showing
6 changed files
with
225 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
react-native/components/createMigratorManagerComponent/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import * as React from 'react' | ||
import type { Json } from '../../types/Json' | ||
import type { MigratorInterface } from '../../types/MigratorInterface' | ||
import type { MigratableState } from '../../types/MigratableState' | ||
|
||
/** | ||
* Creates a React component which automatically manages a migrator, displaying | ||
* a loading screen and executing it if appropriate. | ||
* @template T The type of state to migrate. | ||
* @param migrator The migrator. | ||
* @returns A React component which automatically manages the migrator, | ||
* displaying a loading screen and executing it if appropriate. | ||
*/ | ||
export const createMigratorManagerComponent = <T extends Readonly<Record<string | number, Json>>>( | ||
migrator: MigratorInterface<T> | ||
): React.FunctionComponent<{ | ||
/** | ||
* The state to migrate. | ||
*/ | ||
readonly state: MigratableState<T> | ||
|
||
/** | ||
* Called once migration completes. | ||
* @param to The resulting state.. | ||
*/ | ||
readonly setState: (to: MigratableState<T>) => void | ||
|
||
/** | ||
* The JSX to display while the state is migrated. | ||
*/ | ||
readonly migrating: JSX.Element | ||
|
||
/** | ||
* The JSX to display once the state is migrated. | ||
*/ | ||
readonly ready: JSX.Element | ||
}> => { | ||
return ({ state, setState, migrating, ready }) => { | ||
const executionRequired = migrator.executionRequired(state) | ||
|
||
React.useEffect(() => { | ||
if (executionRequired) { | ||
setState(migrator.execute(state)) | ||
} | ||
}, [executionRequired]) | ||
|
||
return executionRequired ? migrating : ready | ||
} | ||
} |
49 changes: 49 additions & 0 deletions
49
react-native/components/createMigratorManagerComponent/readme.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# `react-native-app-helpers/createMigratorManagerComponent` | ||
|
||
Creates a React component which automatically manages a migrator, displaying a | ||
loading screen and executing it if appropriate. | ||
|
||
## Usage | ||
|
||
```tsx | ||
import { | ||
Migrator, | ||
MigratableState, | ||
createMigratorManagerComponent, | ||
} from "react-native-app-helpers"; | ||
|
||
type State = { readonly items: ReadonlyArray<number> } | ||
|
||
const migrator = new Migrator<State>([ | ||
['b4dac8cd-af18-4e7d-a723-27f61d368228', (previous) => ({ | ||
...previous, | ||
items: [...previous.items, 1], | ||
})], | ||
['1b69c28f-454e-4511-aa05-596fe5ae23a8', (previous) => ({ | ||
...previous, | ||
items: [...previous.items, 2], | ||
})], | ||
['b07bc75d-1ba2-4bf7-b510-51a93d554a56', (previous) => ({ | ||
...previous, | ||
items: [...previous.items, 3], | ||
})], | ||
]); | ||
|
||
const MigratorManager = createMigratorManagerComponent(migrator); | ||
|
||
export default () => { | ||
const [state, setState] = React.useState<MigratableState<State>>({ | ||
executedMigrationUuids: ['1b69c28f-454e-4511-aa05-596fe5ae23a8'], | ||
items: [], | ||
}); | ||
|
||
return ( | ||
<MigratorManager | ||
state={state} | ||
setState={setState} | ||
migrating={<Text>Migrations are in progress...</Text>} | ||
ready={<Text>All migrations have completed.</Text>} | ||
/> | ||
); | ||
}; | ||
``` |
123 changes: 123 additions & 0 deletions
123
react-native/components/createMigratorManagerComponent/unit.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import * as React from 'react' | ||
import { Text } from 'react-native' | ||
import * as TestRenderer from 'react-test-renderer' | ||
import { createMigratorManagerComponent, type MigratorInterface } from '../../..' | ||
|
||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions | ||
type State = { readonly items: readonly number[] } | ||
|
||
test('displays the migrating screen when no migrations are required', async () => { | ||
const migrator: MigratorInterface<State> = { | ||
executionRequired: jest.fn().mockReturnValue(true), | ||
execute: jest.fn() | ||
} | ||
const MigratorManager = createMigratorManagerComponent(migrator) | ||
const setState = jest.fn() | ||
|
||
const renderer = TestRenderer.create( | ||
<MigratorManager | ||
state={{ items: [1, 2, 3] }} | ||
setState={setState} | ||
migrating={<Text>Migrating</Text>} | ||
ready={<Text>Ready</Text>} | ||
/> | ||
) | ||
|
||
expect(renderer.toTree()?.rendered).toEqual( | ||
expect.objectContaining({ | ||
props: expect.objectContaining({ | ||
children: 'Migrating' | ||
}) | ||
}) | ||
) | ||
|
||
expect(setState).not.toHaveBeenCalled() | ||
expect(migrator.executionRequired).toHaveBeenCalledTimes(1) | ||
expect(migrator.executionRequired).toHaveBeenCalledWith({ items: [1, 2, 3] }) | ||
expect(migrator.execute).not.toHaveBeenCalled() | ||
|
||
renderer.unmount() | ||
|
||
await TestRenderer.act(async () => { | ||
await new Promise((resolve) => setTimeout(resolve, 250)) | ||
}) | ||
}) | ||
|
||
test('executes migrations when required', async () => { | ||
const migrator: MigratorInterface<State> = { | ||
executionRequired: jest.fn().mockReturnValue(true), | ||
execute: jest.fn().mockReturnValue({ items: [4, 5, 6] }) | ||
} | ||
const MigratorManager = createMigratorManagerComponent(migrator) | ||
const setState = jest.fn() | ||
|
||
const renderer = TestRenderer.create( | ||
<MigratorManager | ||
state={{ items: [1, 2, 3] }} | ||
setState={setState} | ||
migrating={<Text>Migrating</Text>} | ||
ready={<Text>Ready</Text>} | ||
/> | ||
) | ||
|
||
expect(renderer.toTree()?.rendered).toEqual( | ||
expect.objectContaining({ | ||
props: expect.objectContaining({ | ||
children: 'Migrating' | ||
}) | ||
}) | ||
) | ||
|
||
await TestRenderer.act(async () => { | ||
await new Promise((resolve) => setTimeout(resolve, 250)) | ||
}) | ||
|
||
expect(migrator.executionRequired).toHaveBeenCalledTimes(1) | ||
expect(migrator.execute).toHaveBeenCalledTimes(1) | ||
expect(migrator.execute).toHaveBeenCalledWith({ items: [1, 2, 3] }) | ||
expect(setState).toHaveBeenCalledTimes(1) | ||
expect(setState).toHaveBeenCalledWith({ items: [4, 5, 6] }) | ||
|
||
renderer.unmount() | ||
|
||
await TestRenderer.act(async () => { | ||
await new Promise((resolve) => setTimeout(resolve, 250)) | ||
}) | ||
}) | ||
|
||
test('displays the ready screen when no migrations are required', async () => { | ||
const migrator: MigratorInterface<State> = { | ||
executionRequired: jest.fn().mockReturnValue(false), | ||
execute: jest.fn() | ||
} | ||
const MigratorManager = createMigratorManagerComponent(migrator) | ||
const setState = jest.fn() | ||
|
||
const renderer = TestRenderer.create( | ||
<MigratorManager | ||
state={{ items: [1, 2, 3] }} | ||
setState={setState} | ||
migrating={<Text>Migrating</Text>} | ||
ready={<Text>Ready</Text>} | ||
/> | ||
) | ||
|
||
expect(renderer.toTree()?.rendered).toEqual( | ||
expect.objectContaining({ | ||
props: expect.objectContaining({ | ||
children: 'Ready' | ||
}) | ||
}) | ||
) | ||
|
||
renderer.unmount() | ||
|
||
await TestRenderer.act(async () => { | ||
await new Promise((resolve) => setTimeout(resolve, 250)) | ||
}) | ||
|
||
expect(setState).not.toHaveBeenCalled() | ||
expect(migrator.executionRequired).toHaveBeenCalledTimes(1) | ||
expect(migrator.executionRequired).toHaveBeenCalledWith({ items: [1, 2, 3] }) | ||
expect(migrator.execute).not.toHaveBeenCalled() | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters