Repository: mobxjs/mobx-state-tree
Branch: master
Commit: 24037f14c6b8
Files: 206
Total size: 1.4 MB
Directory structure:
gitextract_v8l79haa/
├── .circleci/
│ └── config.yml
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ └── bug-report.md
│ ├── lock.yml
│ ├── pull_request_template.md
│ └── stale.yml
├── .gitignore
├── .husky/
│ └── pre-commit
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── ISSUE_TEMPLATE.md
├── LICENSE
├── README.md
├── SECURITY.md
├── __tests__/
│ ├── core/
│ │ ├── 1525.test.ts
│ │ ├── 1664.test.ts
│ │ ├── 2230.test.ts
│ │ ├── __snapshots__/
│ │ │ ├── async.test.ts.snap
│ │ │ ├── custom-type.test.ts.snap
│ │ │ └── reference-custom.test.ts.snap
│ │ ├── action.test.ts
│ │ ├── actionTrackingMiddleware2.test.ts
│ │ ├── api.test.ts
│ │ ├── array.test.ts
│ │ ├── async.test.ts
│ │ ├── bigint.test.ts
│ │ ├── boolean.test.ts
│ │ ├── boxes-store.test.ts
│ │ ├── circular1.test.ts
│ │ ├── circular2.test.ts
│ │ ├── custom-type.test.ts
│ │ ├── date.test.ts
│ │ ├── deprecated.test.ts
│ │ ├── enum.test.ts
│ │ ├── env.test.ts
│ │ ├── frozen.test.ts
│ │ ├── hooks.test.ts
│ │ ├── identifier.test.ts
│ │ ├── jsonpatch.test.ts
│ │ ├── late.test.ts
│ │ ├── lazy.test.ts
│ │ ├── literal.test.ts
│ │ ├── map.test.ts
│ │ ├── model.test.ts
│ │ ├── name.test.ts
│ │ ├── node.test.ts
│ │ ├── number.test.ts
│ │ ├── object-node.test.ts
│ │ ├── object.test.ts
│ │ ├── optimizations.test.ts
│ │ ├── optional-extension.test.ts
│ │ ├── optional.test.ts
│ │ ├── parent-properties.test.ts
│ │ ├── pointer.test.ts
│ │ ├── primitives.test.ts
│ │ ├── protect.test.ts
│ │ ├── recordPatches.test.ts
│ │ ├── reference-custom.test.ts
│ │ ├── reference-onInvalidated.test.ts
│ │ ├── reference.test.ts
│ │ ├── refinement.test.ts
│ │ ├── reflection.test.ts
│ │ ├── snapshotProcessor.test.ts
│ │ ├── string.test.ts
│ │ ├── this.test.ts
│ │ ├── type-system.test.ts
│ │ ├── union.test.ts
│ │ └── volatile.test.ts
│ ├── perf/
│ │ ├── fixture-data.skip.ts
│ │ ├── fixture-models.skip.ts
│ │ ├── fixtures/
│ │ │ ├── fixture-data.ts
│ │ │ └── fixture-models.ts
│ │ ├── perf.skip.ts
│ │ ├── report.ts
│ │ ├── scenarios.ts
│ │ └── timer.ts
│ ├── setup.ts
│ ├── tsconfig.json
│ └── utils.test.ts
├── bun.lockb
├── bunfig.toml
├── changelog.md
├── docker-compose.yml
├── docs/
│ ├── .gitattributes
│ ├── API/
│ │ ├── index.md
│ │ └── interfaces/
│ │ ├── customtypeoptions.md
│ │ ├── functionwithflag.md
│ │ ├── iactioncontext.md
│ │ ├── iactionrecorder.md
│ │ ├── iactiontrackingmiddleware2call.md
│ │ ├── iactiontrackingmiddleware2hooks.md
│ │ ├── iactiontrackingmiddlewarehooks.md
│ │ ├── ianycomplextype.md
│ │ ├── ianymodeltype.md
│ │ ├── ianytype.md
│ │ ├── ihooks.md
│ │ ├── ijsonpatch.md
│ │ ├── imiddlewareevent.md
│ │ ├── imodelreflectiondata.md
│ │ ├── imodelreflectionpropertiesdata.md
│ │ ├── imodeltype.md
│ │ ├── ipatchrecorder.md
│ │ ├── ireversiblejsonpatch.md
│ │ ├── iserializedactioncall.md
│ │ ├── isimpletype.md
│ │ ├── isnapshotprocessor.md
│ │ ├── isnapshotprocessors.md
│ │ ├── itype.md
│ │ ├── ivalidationcontextentry.md
│ │ ├── ivalidationerror.md
│ │ ├── referenceoptionsgetset.md
│ │ ├── referenceoptionsoninvalidated.md
│ │ └── unionoptions.md
│ ├── API_header.md
│ ├── compare/
│ │ └── context-reducer-vs-mobx-state-tree.md
│ ├── concepts/
│ │ ├── actions.md
│ │ ├── async-actions.md
│ │ ├── dependency-injection.md
│ │ ├── listeners.md
│ │ ├── middleware.md
│ │ ├── patches.md
│ │ ├── react.md
│ │ ├── reconciliation.md
│ │ ├── references.md
│ │ ├── snapshots.md
│ │ ├── trees.md
│ │ ├── views.md
│ │ └── volatiles.md
│ ├── intro/
│ │ ├── examples.md
│ │ ├── getting-started.md
│ │ ├── installation.md
│ │ ├── philosophy.md
│ │ └── welcome.md
│ ├── overview/
│ │ ├── hooks.md
│ │ ├── types.md
│ │ └── utilties.md
│ ├── recipes/
│ │ ├── auto-generated-property-setter-actions.md
│ │ ├── mst-query.md
│ │ └── pre-built-form-types-with-mst-form-type.md
│ └── tips/
│ ├── circular-deps.md
│ ├── faq.md
│ ├── inheritance.md
│ ├── more-tips.md
│ ├── resources.md
│ ├── snapshots-as-values.md
│ └── typescript.md
├── jest.config.js
├── package.json
├── rollup.config.js
├── scripts/
│ ├── fix-docs-source-links.js
│ ├── generate-compose-type.js
│ └── generate-shared.js
├── src/
│ ├── core/
│ │ ├── action.ts
│ │ ├── actionContext.ts
│ │ ├── flow.ts
│ │ ├── json-patch.ts
│ │ ├── mst-operations.ts
│ │ ├── node/
│ │ │ ├── BaseNode.ts
│ │ │ ├── Hook.ts
│ │ │ ├── create-node.ts
│ │ │ ├── identifier-cache.ts
│ │ │ ├── livelinessChecking.ts
│ │ │ ├── node-utils.ts
│ │ │ ├── object-node.ts
│ │ │ └── scalar-node.ts
│ │ ├── process.ts
│ │ └── type/
│ │ ├── type-checker.ts
│ │ └── type.ts
│ ├── index.ts
│ ├── internal.ts
│ ├── middlewares/
│ │ ├── create-action-tracking-middleware.ts
│ │ ├── createActionTrackingMiddleware2.ts
│ │ └── on-action.ts
│ ├── types/
│ │ ├── complex-types/
│ │ │ ├── array.ts
│ │ │ ├── map.ts
│ │ │ └── model.ts
│ │ ├── index.ts
│ │ ├── primitives.ts
│ │ └── utility-types/
│ │ ├── custom.ts
│ │ ├── enumeration.ts
│ │ ├── frozen.ts
│ │ ├── identifier.ts
│ │ ├── late.ts
│ │ ├── lazy.ts
│ │ ├── literal.ts
│ │ ├── maybe.ts
│ │ ├── optional.ts
│ │ ├── reference.ts
│ │ ├── refinement.ts
│ │ ├── snapshotProcessor.ts
│ │ └── union.ts
│ └── utils.ts
├── test-results/
│ └── .gitkeep
├── tsconfig.json
├── tslint.json
├── typedocconfig.js
└── website/
├── bun.lockb
├── core/
│ └── Footer.js
├── i18n/
│ └── en.json
├── package.json
├── sidebars.json
├── siteConfig.js
└── static/
├── css/
│ └── custom.css
└── index.html
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/config.yml
================================================
version: 2.1
executors:
my-executor:
docker:
- image: cimg/node:14.18.1
environment:
CI: true
orbs:
node: circleci/node@4.7.0
jobs:
# mobx-state-tree build
build:
executor: my-executor
steps:
- checkout
- run:
name: Install the latest version of bun
command: curl -fsSL https://bun.sh/install | bash
- run:
name: Link bun
command: sudo ln -s ~/.bun/bin/bun /usr/local/bin/
- run:
name: Install dependencies
command: bun install
- run:
name: Build MST
command: bun run build
- persist_to_workspace:
root: .
paths:
- ./*
# Add new prettier check job
check-prettier:
executor: my-executor
steps:
- attach_workspace:
at: .
- run:
name: Install the latest version of bun
command: curl -fsSL https://bun.sh/install | bash
- run:
name: Link bun
command: sudo ln -s ~/.bun/bin/bun /usr/local/bin/
- run:
name: Check code formatting
command: bun run prettier:check
# mobx-state-tree tests
test-mst-dev:
executor: my-executor
steps:
- attach_workspace:
at: .
- run:
name: Install the latest version of bun
command: curl -fsSL https://bun.sh/install | bash
- run:
name: Link bun
command: sudo ln -s ~/.bun/bin/bun /usr/local/bin/
- run: bun test:all
test-mst-prod:
executor: my-executor
steps:
- attach_workspace:
at: .
- run:
name: Install the latest version of bun
command: curl -fsSL https://bun.sh/install | bash
- run:
name: Link bun
command: sudo ln -s ~/.bun/bin/bun /usr/local/bin/
- run: bun test:prod
test-size:
executor: my-executor
steps:
- attach_workspace:
at: .
- run:
name: Install the latest version of bun
command: curl -fsSL https://bun.sh/install | bash
- run:
name: Link bun
command: sudo ln -s ~/.bun/bin/bun /usr/local/bin/
- run: bun run size
workflows:
version: 2
build-and-test:
jobs:
- build
# Add prettier check to workflow
- check-prettier:
requires:
- build
- test-mst-dev:
requires:
- build
- test-mst-prod:
requires:
- build
# Temporarily disabled while we work to implement Bun testing
# - test-size:
# requires:
# - build
================================================
FILE: .dockerignore
================================================
*/node_modules
*.log
================================================
FILE: .gitattributes
================================================
* text=auto
*.ts text eol=lf
*.json text eol=lf
*.md text eol=lf
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.md
================================================
---
name: Bug report
about: I think something is not working as it should
---
**_Bug report_**
- [ ] I've checked documentation and searched for existing issues [and discussions](https://github.com/mobxjs/mobx-state-tree/discussions)
- [ ] I've made sure my project is based on the latest MST version
- [ ] Fork [this](https://codesandbox.io/p/sandbox/mobx-state-tree-todolist-forked-pj732k) code sandbox or another minimal reproduction.
**Sandbox link or minimal reproduction code**
**Describe the expected behavior**
**Describe the observed behavior**
================================================
FILE: .github/lock.yml
================================================
# Configuration for Lock Threads - https://github.com/dessant/lock-threads
# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 60
# Skip issues and pull requests created before a given timestamp. Timestamp must
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
skipCreatedBefore: 2019-01-01
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
exemptLabels: []
# Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: false
# Comment to post before locking. Set to `false` to disable
lockComment: >
This thread has been automatically locked since there has not been
any recent activity after it was closed. Please open a new issue for
related bugs or questions.
# Assign `resolved` as the reason for locking. Set to `false` to disable
setLockReason: true
# Limit to only `issues` or `pulls`
only: issues
================================================
FILE: .github/pull_request_template.md
================================================
## What does this PR do and why?
## Steps to validate locally
================================================
FILE: .github/stale.yml
================================================
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 10
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 4
# Issues with these labels will never be considered stale
exemptLabels:
- brainstorming/wild idea
- breaking change
- bug
- docs or examples
- enhancement
- has PR
- help/PR welcome
- require('@mweststrate')
- never-stale
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity in the last 10 days. It will be closed in 4 days if no further
activity occurs. Thank you for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
# Limit to only `issues` or `pulls`
only: issues
# Comment to post when removing the stale label.
unmarkComment: >
This issue has been automatically unmarked as stale. Please disregard previous warnings.
================================================
FILE: .gitignore
================================================
node_modules
*.log
lib
dist
coverage
.nyc_output
.idea
package-lock.json
.prettierignore
.vscode
.editorconfig
/test-results/**/*.xml
/website/build
.DS_Store
junit.xml
================================================
FILE: .husky/pre-commit
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
bun run lint-staged
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to MobX-State-Tree
Welcome to MobX-State-Tree! We're stoked that you want to contribute to our open-source project. Our community is essential in maintaining and improving the stability, test coverage, and documentation of MST. We really appreciate your time and interest in pitching in. Overall, we want to build useful software and have fun doing it - we hope you'll be able to join us!
## Table of Contents
1. [Getting Started](#getting-started)
2. [Contributing Guidelines](#contributing-guidelines)
3. [Reporting Bugs](#reporting-bugs)
4. [Code of Conduct](#code-of-conduct)
## Getting Started
Before you start contributing, please make sure you have:
- [Bun](https://bun.sh/) installed on your local machine.
- A [GitHub](https://github.com/) account, as you'll need it to create issues and submit pull requests.
Most of the documentation and community assumes some amount of familiarity with:
1. JavaScript
2. TypeScript
3. git
4. Using the command line
5. Experience with state management libraries (typically on the frontend, although there are plenty of applications using MST in other contexts).
If you don't feel comfortable with these concepts, we'd be happy to help you get started, but you may want to consider brushing up on them before digging into the codebase. Reach out in the [discussions section of our GitHub repository](https://github.com/mobxjs/mobx-state-tree/discussions) if you'd like pointers about where to start.
## Contributing Guidelines
### Prioritizing Stability Over New Features
The existing API for MobX-State-Tree is [already quite extensive](https://mobx-state-tree.js.org/intro/welcome). As such, issues and PRs about new features may take lower priority than bug fixes, improving existing features, and performance/TypeScript improvements. If you're looking to augment MST with new functionality, we encourage you to consider building your own third-party library around our project, or perhaps [contributing to the mst-middlewares package](https://github.com/coolsoftwaretyler/mst-middlewares). We'd be happy to help faciliate that work, especially if it keeps our API from expanding much further.
### Tests
To maintain our library's stability, we are always striving to improve test coverage, even where more tests might be redundant. Every PR is _required to add at least one test that directly exercises your code change_, even if that test may be duplicative of existing tests. If you do not include tests with your PR, we will ask you to do so before reviewing. If you open a PR and are unwilling to write tests, we may either write tests on your behalf, or close the PR. If you are uncomfortable writing tests or working with Jest (our current testing library), we would be happy to guide you. This requirement is not intended as a barrier to entry, but as a way for us to enforce and improve the stability of our long-lived library here. It also serves as a way to externally communicate how your change will (or perhaps how it will not) modify the behavior of MST. There's no documentation quite as good as a comprehensive test suite!
### Documentation Changes
Good documentation is crucial for our users. If your contribution involves changes to the library's behavior, please update the documentation accordingly. Not every PR is necessarily going to require documentation updates, but we encourage you to consider touching up documentation related to your code changes. If you're unsure about where to make changes, feel free to reach out to us, and we'll be happy to guide you.
When changing the API, commit the code changes first, then run `npm run build-docs` and commit the documentation separately.
### Submitting a Pull Request
1. Fork the MobX-State-Tree repository on GitHub to your own GitHub account.
2. Clone your fork to your local machine.
3. Create a new branch for your changes: `git checkout -b my-feature`.
4. Before starting, it's not a bad idea to `bun install && bun run build && bun run test:all` to check that your machine can run the full test suite to start.
5. Make your changes and ensure that all tests pass (including any new tests you have added).
6. Update the documentation if necessary.
7. Commit your changes: `git commit -m "Add my feature"`. Please consider [following conventional commit formatting](https://www.conventionalcommits.org/en/v1.0.0/).
8. Push your changes to your fork: `git push origin my-feature`.
9. Create a pull request on the MobX-State-Tree repository. We have a pull request template. If you fill that out and include examples of your changes, links to any issue(s) you're working on, and a good description of your PR, that would help us out a lot. If you skip those steps, we may ask you for clarification before reviewing your work.
Our team will review your pull request as soon as possible and provide feedback. Please be patient, as it may take some time to review and merge your contribution.
## Reporting Bugs
If you encounter a bug while using MobX-State-Tree, please help us by reporting it. To report a bug:
1. Check if the bug has already been reported by searching our [issue tracker](https://github.com/mobxjs/mobx-state-tree/issues).
2. If not, create a new issue, including as much detail as possible about the bug and steps to reproduce it. We have issue templates that will ask specific questions for you to help us understand the problem.
## Bigger PRs
If you want to contribute a large or significant change to MST, we'd love to connect with you ahead of time to make sure it fits in with our overall road map, meets our stability requirements, and make sure you are set up for success. Please consider [asking around in the discussion forum](https://github.com/mobxjs/mobx-state-tree/discussions) if you have a big idea you want to implement, or if you want to work on some existing big ideas out therein the communit.
## Code of Conduct
We strive to maintain a friendly and welcoming community. Please read and adhere to our [Code of Conduct](CODE_OF_CONDUCT.md) in all interactions within the MobX-State-Tree project.
Thank you for considering contributing to MobX-State-Tree. Your contributions help us make our library better for everyone. We appreciate your support and look forward to collaborating with you!
================================================
FILE: Dockerfile
================================================
FROM node:8.11.4
WORKDIR /app/website
EXPOSE 3000 35729
COPY ./docs /app/docs
COPY ./website /app/website
RUN yarn install
CMD ["yarn", "start"]
================================================
FILE: ISSUE_TEMPLATE.md
================================================
**If your issue isn't a bug report, please consider using discussion threads instead of opening an issue: https://github.com/mobxjs/mobx-state-tree/discussions**
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2016 Michel Weststrate
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# mobx-state-tree
[](https://badge.fury.io/js/mobx-state-tree)
[](https://circleci.com/gh/mobxjs/mobx-state-tree)
[](https://github.com/mobxjs/mobx-state-tree/discussions)
## What is mobx-state-tree?
Technically speaking, mobx-state-tree (also known as MST) is a state container system built on [MobX](https://github.com/mobxjs/mobx), a functional reactive state library.
This may not mean much to you, and that’s okay. I’ll explain it like this: **MobX is a state management "engine", and MobX-State-Tree gives it structure and common tools you need for your app.** MST is valuable in a large team but also useful in smaller applications when you expect your code to scale rapidly. And if we compare it to Redux, MST offers better performance and much less boilerplate code than Redux!
MobX is [one of the most popular Redux alternatives](https://2019.stateofjs.com/data-layer/mobx/) and is used (along with MobX-State-Tree) by companies worldwide. MST plays very well with TypeScript, React, and React Native, especially when paired with [mobx-react-lite](https://github.com/mobxjs/mobx/tree/main/packages/mobx-react-lite). It supports multiple stores, async actions and side effects, enables extremely targeted re-renders for React apps, and much more -- all in a package with _zero dependencies_ other than MobX itself.
_Note: you don't need to know how to use MobX in order to use MST._
# Getting started
See the [Getting started](https://mobx-state-tree.js.org/intro/getting-started) tutorial or follow the free [egghead.io course](https://egghead.io/courses/manage-application-state-with-mobx-state-tree).
👉 Official docs can be found at [http://mobx-state-tree.js.org/](http://mobx-state-tree.js.org/)
## Quick Code Example
There's nothing quite like looking at some code to get a feel for a library. Check out this small example of an author and list of tweets by that author.
```js
import { types } from "mobx-state-tree" // alternatively: import { t } from "mobx-state-tree"
// Define a couple models
const Author = types.model({
id: types.identifier,
firstName: types.string,
lastName: types.string
})
const Tweet = types.model({
id: types.identifier,
author: types.reference(Author), // stores just the `id` reference!
body: types.string,
timestamp: types.number
})
// Define a store just like a model
const RootStore = types.model({
authors: types.array(Author),
tweets: types.array(Tweet)
})
// Instantiate a couple model instances
const jamon = Author.create({
id: "jamon",
firstName: "Jamon",
lastName: "Holmgren"
})
const tweet = Tweet.create({
id: "1",
author: jamon.id, // just the ID needed here
body: "Hello world!",
timestamp: Date.now()
})
// Now instantiate the store!
const rootStore = RootStore.create({
authors: [jamon],
tweets: [tweet]
})
// Ready to use in a React component, if that's your target.
import { observer } from "mobx-react-lite"
const MyComponent = observer((props) => {
return
Hello, {rootStore.authors[0].firstName}!
})
// Note: since this component is "observed", any changes to rootStore.authors[0].firstName
// will result in a re-render! If you're not using React, you can also "listen" to changes
// using `onSnapshot`: https://mobx-state-tree.js.org/concepts/snapshots
```
## Thanks!
- [Michel Weststrate](https://twitter.com/mweststrate) for creating MobX, MobX-State-Tree, and MobX-React.
- [Infinite Red](https://infinite.red) for supporting ongoing maintenance on MST.
- [Mendix](https://mendix.com) for sponsoring and providing the opportunity to work on exploratory projects like MST.
- [Dan Abramov](https://twitter.com/dan_abramov)'s work on [Redux](http://redux.js.org) has strongly influenced the idea of snapshots and transactional actions in MST.
- [Giulio Canti](https://twitter.com/GiulioCanti)'s work on [tcomb](http://github.com/gcanti/tcomb) and type systems in general has strongly influenced the type system of MST.
- All the early adopters encouraging to pursue this whole idea and proving it is something feasible.
================================================
FILE: SECURITY.md
================================================
## Security contact information
To report a security vulnerability, please use the
[Tidelift security contact](https://tidelift.com/security).
Tidelift will coordinate the fix and disclosure.
================================================
FILE: __tests__/core/1525.test.ts
================================================
import { types, Instance } from "../../src/index"
import { describe, it } from "bun:test"
describe("1525. Model instance maybe fields becoming TypeScript optional fields when included in a types.union", () => {
it("does not throw a typescript error", () => {
const Model = types.model("myModel", {
foo: types.string,
bar: types.maybe(types.integer)
})
const Store = types.model("store", {
itemWithoutIssue: Model,
itemWithIssue: types.union(types.literal("anotherValue"), Model)
})
interface IModel extends Instance {}
interface FunctionArgs {
model1: IModel
model2: IModel
}
const store = Store.create({
itemWithoutIssue: { foo: "works" },
itemWithIssue: { foo: "has ts error in a regression" }
})
const f = (props: FunctionArgs) => {}
const itemWithoutIssueModel = store.itemWithoutIssue
const itemWithIssueModel =
store.itemWithIssue === "anotherValue" ? null : store.itemWithIssue
itemWithIssueModel && f({ model1: itemWithoutIssueModel, model2: itemWithIssueModel })
})
})
================================================
FILE: __tests__/core/1664.test.ts
================================================
import { types as t } from "../../src/index"
import { describe, test } from "bun:test"
describe("1664. Array and model types are not inferred correctly when broken down into their components", () => {
test("should not throw a typescript error", () => {
// Simple concrete type with a creation type different than its instance type
const date = t.custom({
name: "Date",
fromSnapshot: snapshot => new Date(snapshot),
toSnapshot: dt => dt.toISOString(),
isTargetType: (val: unknown) => val instanceof Date,
getValidationMessage: (snapshot: unknown) =>
typeof snapshot !== "string" || isNaN(Date.parse(snapshot))
? `${snapshot} is not a valid Date string`
: ""
})
//Wrap the date type in an array type. IArrayType is a sub-interface of IType.
const DateArray = t.array(date)
//Pass the array type to t.union, which infers the component types as
const LoadableDateArray = t.union(t.literal("loading"), DateArray)
//Instantiate the type
const lda = LoadableDateArray.create([])
//Try to use the array type as an instance
if (lda !== "loading") {
//Error: type of lda is essentially `(string | Date)[] | undefined`
//The creation type has been mixed together with the instance type
const dateArray: Date[] = lda
}
})
})
================================================
FILE: __tests__/core/2230.test.ts
================================================
//github.com/mobxjs/mobx-state-tree/issues/2230
import { describe, test } from "bun:test"
import { types, Instance } from "../../src/index"
describe("2230 - type instantiation is excessively deep and possibly infinite", () => {
test("does not happen", () => {
const ModelProps = types
.model({
prop01: "",
prop02: "",
prop03: ""
})
.props({
prop11: "",
prop12: "",
prop13: ""
})
.props({
prop21: "",
prop22: "",
prop23: ""
})
.props({
prop31: "",
prop32: "",
prop33: ""
})
.props({
prop41: "",
prop42: "",
prop43: ""
})
.props({
prop51: "",
prop52: "",
prop53: ""
})
.props({
prop61: "",
prop62: "",
prop63: ""
})
.props({
prop71: "",
prop72: "",
prop73: ""
})
.props({
prop81: "",
prop82: "",
prop83: ""
})
.props({
prop91: "",
prop92: "",
prop93: ""
})
interface IModelProps extends Instance {}
const ModelVolatile = ModelProps.volatile(() => ({
vol01: null,
vol02: null,
vol03: null
}))
.volatile(() => ({
vol11: null,
vol12: null,
vol13: null
}))
.volatile(() => ({
vol21: null,
vol22: null,
vol23: null
}))
.volatile(() => ({
vol31: null,
vol32: null,
vol33: null
}))
.volatile(() => ({
vol41: null,
vol42: null,
vol43: null
}))
.volatile(() => ({
vol51: null,
vol52: null,
vol53: null
}))
.volatile(() => ({
vol61: null,
vol62: null,
vol63: null
}))
.volatile(() => ({
vol71: null,
vol72: null,
vol73: null
}))
.volatile(() => ({
vol81: null,
vol82: null,
vol83: null
}))
.volatile(() => ({
vol91: null,
vol92: null,
vol93: null
}))
interface IModelVolatile extends Instance {}
const ModelViews = ModelVolatile.views((self: IModelVolatile) => ({
get vol01Var() {
return self.vol01
}
}))
interface IModelViews extends Instance {}
const Action1 = ModelViews.actions((self: IModelViews) => ({
getProp01(): string {
return self.prop01
}
}))
interface IAction1 extends Instance {}
const Action2 = Action1.actions((self: IAction1) => ({
getProp11(): string {
return self.prop11
}
}))
interface IAction2 extends Instance {}
const Action3 = Action2.actions((self: IAction2) => ({
getProp21(): string {
return self.prop21
}
}))
interface IAction3 extends Instance {}
const Action4 = Action3.actions((self: IAction3) => ({
getProp31(): string {
return self.prop31
}
}))
interface IAction4 extends Instance {}
const Action5 = Action4.actions((self: IAction4) => ({
getProp41(): string {
return self.prop41
}
}))
.actions((self: IAction4) => ({
getProp51(): string {
return self.prop51
}
}))
.actions((self: IAction4) => ({
getProp61(): string {
return self.prop61
}
}))
.actions((self: IAction4) => ({
getProp71(): string {
return self.prop71
}
}))
.actions((self: IAction4) => ({
getProp81(): string {
return self.prop81
}
}))
.actions((self: IAction4) => ({
getProp91(): string {
return self.prop91
}
}))
interface IAction5 extends Instance {}
})
})
================================================
FILE: __tests__/core/__snapshots__/async.test.ts.snap
================================================
// Bun Snapshot v1, https://goo.gl/fbAQLP
exports[`can handle erroring actions 1`] = `
[
{
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
{
"allParentIds": [
1,
],
"args": [
"black",
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_spawn",
},
{
"allParentIds": [
1,
],
"args": [
undefined,
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_resume",
},
{
"allParentIds": [
1,
],
"args": [
"black",
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_throw",
},
]
`;
exports[`empty sequence works 1`] = `
[
{
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
{
"allParentIds": [
1,
],
"args": [
"black",
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_spawn",
},
{
"allParentIds": [
1,
],
"args": [
undefined,
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_resume",
},
{
"allParentIds": [
1,
],
"args": [
undefined,
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_return",
},
]
`;
exports[`typings 1`] = `
[
{
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
{
"allParentIds": [
1,
],
"args": [
"black",
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_spawn",
},
{
"allParentIds": [
1,
],
"args": [
undefined,
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_resume",
},
{
"allParentIds": [
1,
],
"args": [
"tea",
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_resume_error",
},
{
"allParentIds": [
1,
],
"args": [
"biscuit",
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_return",
},
]
`;
exports[`typings 2`] = `
[
{
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
{
"allParentIds": [
1,
],
"args": [
"black",
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_spawn",
},
{
"allParentIds": [
1,
],
"args": [
undefined,
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_resume",
},
{
"allParentIds": [
1,
],
"args": [
"x",
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_resume_error",
},
{
"allParentIds": [
1,
],
"args": [
"x",
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_throw",
},
]
`;
exports[`can handle nested async actions when using decorate 1`] = `
[
{
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
{
"allParentIds": [
1,
],
"args": [
"black",
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_spawn",
},
{
"allParentIds": [
1,
],
"args": [
undefined,
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_resume",
},
{
"allParentIds": [
1,
],
"args": [
"drinking coffee",
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_resume",
},
{
"allParentIds": [
1,
],
"args": [
"awake",
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_return",
},
]
`;
exports[`can handle nested async actions when using decorate 2`] = `
[
{
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
{
"allParentIds": [
1,
],
"args": [
"black",
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_spawn",
},
{
"allParentIds": [
1,
],
"args": [
undefined,
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_resume",
},
{
"allParentIds": [
1,
2,
],
"args": [
"drinking black",
],
"id": 3,
"name": "uppercase",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [
1,
],
"args": [
undefined,
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_resume",
},
"parentId": 2,
"rootId": 1,
"type": "flow_spawn",
},
{
"allParentIds": [
1,
2,
],
"args": [
undefined,
],
"id": 3,
"name": "uppercase",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [
1,
],
"args": [
undefined,
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_resume",
},
"parentId": 2,
"rootId": 1,
"type": "flow_resume",
},
{
"allParentIds": [
1,
2,
],
"args": [
"DRINKING BLACK",
],
"id": 3,
"name": "uppercase",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [
1,
],
"args": [
undefined,
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_resume",
},
"parentId": 2,
"rootId": 1,
"type": "flow_resume",
},
{
"allParentIds": [
1,
2,
],
"args": [
"DRINKING BLACK",
],
"id": 3,
"name": "uppercase",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [
1,
],
"args": [
undefined,
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_resume",
},
"parentId": 2,
"rootId": 1,
"type": "flow_return",
},
{
"allParentIds": [
1,
],
"args": [
"DRINKING BLACK",
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_resume",
},
{
"allParentIds": [
1,
],
"args": [
"DRINKING BLACK",
],
"id": 2,
"name": "fetchData",
"parentActionEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentEvent": {
"allParentIds": [],
"args": [
"black",
],
"id": 1,
"name": "startFetch",
"parentActionEvent": undefined,
"parentEvent": undefined,
"parentId": 0,
"rootId": 1,
"type": "action",
},
"parentId": 1,
"rootId": 1,
"type": "flow_return",
},
]
`;
================================================
FILE: __tests__/core/__snapshots__/custom-type.test.ts.snap
================================================
// Bun Snapshot v1, https://goo.gl/fbAQLP
exports[`reassignments will work 1`] = `
[
{
"balance": "2.5",
"lastTransaction": null,
},
{
"balance": "3.5",
"lastTransaction": null,
},
{
"balance": "4.5",
"lastTransaction": null,
},
{
"balance": "4.5",
"lastTransaction": "2.5",
},
{
"balance": "4.5",
"lastTransaction": null,
},
]
`;
exports[`reassignments will work 2`] = `
[
{
"op": "replace",
"path": "/balance",
"value": "2.5",
},
{
"op": "replace",
"path": "/balance",
"value": "3.5",
},
{
"op": "replace",
"path": "/balance",
"value": "4.5",
},
{
"op": "replace",
"path": "/lastTransaction",
"value": "2.5",
},
{
"op": "replace",
"path": "/lastTransaction",
"value": null,
},
]
`;
exports[`complex reassignments will work 1`] = `
[
{
"balance": [
2,
5,
],
},
{
"balance": [
2,
5,
],
},
{
"balance": [
3,
5,
],
},
{
"balance": [
4,
5,
],
},
]
`;
exports[`complex reassignments will work 2`] = `
[
{
"op": "replace",
"path": "/balance",
"value": [
2,
5,
],
},
{
"op": "replace",
"path": "/balance",
"value": [
2,
5,
],
},
{
"op": "replace",
"path": "/balance",
"value": [
3,
5,
],
},
{
"op": "replace",
"path": "/balance",
"value": [
4,
5,
],
},
]
`;
================================================
FILE: __tests__/core/__snapshots__/reference-custom.test.ts.snap
================================================
// Bun Snapshot v1, https://goo.gl/fbAQLP
exports[`it should support custom references - adv 1`] = `
[
"1",
"2",
"1",
null,
"3",
]
`;
exports[`it should support custom references - adv 2`] = `
[
{
"selection": "Michel",
"users": {
"1": {
"id": "1",
"name": "Michel",
},
"2": {
"id": "2",
"name": "Mattia",
},
},
},
{
"selection": "Mattia",
"users": {
"1": {
"id": "1",
"name": "Michel",
},
"2": {
"id": "2",
"name": "Mattia",
},
},
},
{
"selection": "Michel",
"users": {
"1": {
"id": "1",
"name": "Michel",
},
"2": {
"id": "2",
"name": "Mattia",
},
},
},
{
"selection": "Michel",
"users": {
"2": {
"id": "2",
"name": "Mattia",
},
},
},
{
"selection": "Michel",
"users": {
"2": {
"id": "2",
"name": "Mattia",
},
"3": {
"id": "3",
"name": "Michel",
},
},
},
]
`;
exports[`it should support custom references - adv 3`] = `
[
{
"op": "replace",
"path": "/selection",
"value": "Michel",
},
{
"op": "replace",
"path": "/selection",
"value": "Mattia",
},
{
"op": "replace",
"path": "/selection",
"value": "Michel",
},
{
"op": "remove",
"path": "/users/1",
},
{
"op": "add",
"path": "/users/3",
"value": {
"id": "3",
"name": "Michel",
},
},
]
`;
exports[`it should support custom references - adv 4`] = `
[
{
"op": "replace",
"path": "/selection",
"value": "Mattia",
},
{
"op": "replace",
"path": "/selection",
"value": "Michel",
},
{
"op": "replace",
"path": "/selection",
"value": "Mattia",
},
{
"op": "add",
"path": "/users/1",
"value": {
"id": "1",
"name": "Michel",
},
},
{
"op": "remove",
"path": "/users/3",
},
]
`;
================================================
FILE: __tests__/core/action.test.ts
================================================
import { configure } from "mobx"
import {
recordActions,
types,
getSnapshot,
onAction,
applyPatch,
applySnapshot,
addMiddleware,
getRoot,
cast,
IMiddlewareEvent,
ISerializedActionCall,
Instance
} from "../../src"
import { expect, test } from "bun:test"
/// Simple action replay and invocation
const Task = types
.model({
done: false
})
.actions(self => {
function toggle() {
self.done = !self.done
return self.done
}
return {
toggle
}
})
test("it should be possible to invoke a simple action", () => {
const t1 = Task.create()
expect(t1.done).toBe(false)
expect(t1.toggle()).toBe(true)
expect(t1.done).toBe(true)
})
test("it should be possible to record & replay a simple action", () => {
const t1 = Task.create()
const t2 = Task.create()
expect(t1.done).toBe(false)
expect(t2.done).toBe(false)
const recorder = recordActions(t1)
t1.toggle()
t1.toggle()
t1.toggle()
expect(recorder.actions).toEqual([
{ name: "toggle", path: "", args: [] },
{ name: "toggle", path: "", args: [] },
{ name: "toggle", path: "", args: [] }
])
recorder.replay(t2)
expect(t2.done).toBe(true)
})
test("applying patches should be recordable and replayable", () => {
const t1 = Task.create()
const t2 = Task.create()
const recorder = recordActions(t1)
expect(t1.done).toBe(false)
applyPatch(t1, { op: "replace", path: "/done", value: true })
expect(t1.done).toBe(true)
expect(recorder.actions).toEqual([
{
name: "@APPLY_PATCHES",
path: "",
args: [[{ op: "replace", path: "/done", value: true }]]
}
])
recorder.replay(t2)
expect(t2.done).toBe(true)
})
test("applying patches should be replacing the root store", () => {
const t1 = Task.create()
const recorder = recordActions(t1)
expect(t1.done).toBe(false)
applyPatch(t1, { op: "replace", path: "", value: { done: true } })
expect(t1.done).toBe(true)
expect(recorder.actions).toEqual([
{
name: "@APPLY_PATCHES",
path: "",
args: [[{ op: "replace", path: "", value: { done: true } }]]
}
])
})
test("applying snapshots should be recordable and replayable", () => {
const t1 = Task.create()
const t2 = Task.create()
const recorder = recordActions(t1)
expect(t1.done).toBe(false)
applySnapshot(t1, { done: true })
expect(t1.done).toBe(true)
expect(recorder.actions).toEqual([
{
name: "@APPLY_SNAPSHOT",
path: "",
args: [{ done: true }]
}
])
recorder.replay(t2)
expect(t2.done).toBe(true)
})
// Complex actions
const Customer = types.model("Customer", {
id: types.identifierNumber,
name: types.string
})
const Order = types
.model("Order", {
customer: types.maybeNull(types.reference(Customer))
})
.actions(self => {
function setCustomer(customer: Instance) {
self.customer = customer
}
function noopSetCustomer(_: Instance) {
// noop
}
return {
setCustomer,
noopSetCustomer
}
})
const OrderStore = types.model("OrderStore", {
customers: types.array(Customer),
orders: types.array(Order)
})
function createTestStore() {
const store = OrderStore.create({
customers: [{ id: 1, name: "Mattia" }],
orders: [
{
customer: null
}
]
})
onAction(store, () => {})
return store
}
test("it should not be possible to pass a complex object", () => {
const store = createTestStore()
const recorder = recordActions(store)
expect(store.customers[0].name).toBe("Mattia")
store.orders[0].setCustomer(store.customers[0])
expect(store.orders[0].customer!.name).toBe("Mattia")
expect(store.orders[0].customer).toBe(store.customers[0])
expect(getSnapshot(store)).toEqual({
customers: [
{
id: 1,
name: "Mattia"
}
],
orders: [
{
customer: 1
}
]
})
expect(recorder.actions).toEqual([
{
name: "setCustomer",
path: "/orders/0",
args: [{ $MST_UNSERIALIZABLE: true, type: "[MSTNode: Customer]" }]
}
])
})
if (process.env.NODE_ENV !== "production") {
test("it should not be possible to set the wrong type", () => {
const store = createTestStore()
expect(() => {
store.orders[0].setCustomer(store.orders[0] as any)
}).toThrow(
"Error while converting to `(reference(Customer) | null)`:\n\n " +
"value of type Order: is not assignable to type: `(reference(Customer) | null)`, expected an instance of `(reference(Customer) | null)` or a snapshot like `(reference(Customer) | null?)` instead."
) // wrong type!
})
}
test("it should not be possible to pass the element of another tree", () => {
const store1 = createTestStore()
const store2 = createTestStore()
const recorder = recordActions(store2)
store2.orders[0].setCustomer(store1.customers[0])
expect(recorder.actions).toEqual([
{
name: "setCustomer",
path: "/orders/0",
args: [
{
$MST_UNSERIALIZABLE: true,
type: "[MSTNode: Customer]"
}
]
}
])
})
test("it should not be possible to pass an unserializable object", () => {
const store = createTestStore()
const circular = { a: null as any }
circular.a = circular
const recorder = recordActions(store)
store.orders[0].noopSetCustomer(circular as any)
store.orders[0].noopSetCustomer(Buffer.from("bla") as any)
expect(recorder.actions).toEqual([
{
args: [
{
$MST_UNSERIALIZABLE: true,
type: "TypeError: JSON.stringify cannot serialize cyclic structures."
}
],
name: "noopSetCustomer",
path: "/orders/0"
},
{
args: [
{
$MST_UNSERIALIZABLE: true,
type: "[object Buffer]"
}
],
name: "noopSetCustomer",
path: "/orders/0"
}
])
})
test("it should be possible to pass a complex plain object", () => {
const t1 = Task.create()
const t2 = Task.create()
const recorder = recordActions(t1)
;(t1 as any).toggle({ bla: ["nuff", ["said"]] }) // nonsense, but serializable!
expect(recorder.actions).toEqual([
{ name: "toggle", path: "", args: [{ bla: ["nuff", ["said"]] }] }
])
recorder.replay(t2)
expect(t2.done).toBe(true)
})
test("action should be bound", () => {
const task = Task.create()
const f = task.toggle
expect(f()).toBe(true)
expect(task.done).toBe(true)
})
test("snapshot should be available and updated during an action", () => {
const Model = types
.model({
x: types.number
})
.actions(self => {
function inc() {
self.x += 1
const res = getSnapshot(self).x
self.x += 1
return res
}
return {
inc
}
})
const a = Model.create({ x: 2 })
expect(a.inc()).toBe(3)
expect(a.x).toBe(4)
expect(getSnapshot(a).x).toBe(4)
})
test("indirectly called private functions should be able to modify state", () => {
const Model = types
.model({
x: 3
})
.actions(self => {
function incrementBy(delta: number) {
self.x += delta
}
return {
inc() {
incrementBy(1)
},
dec() {
incrementBy(-1)
}
}
})
const cnt = Model.create()
expect(cnt.x).toBe(3)
cnt.dec()
expect(cnt.x).toBe(2)
expect((cnt as any).incrementBy).toBe(undefined)
})
test("volatile state survives reonciliation", () => {
const Model = types.model({ x: 3 }).actions(self => {
let incrementor = 1
return {
setIncrementor(value: number) {
incrementor = value
},
inc() {
self.x += incrementor
}
}
})
const Store = types.model({
cnt: types.optional(Model, {})
})
const store = Store.create()
store.cnt.inc()
expect(store.cnt.x).toBe(4)
store.cnt.setIncrementor(3)
store.cnt.inc()
expect(store.cnt.x).toBe(7)
applySnapshot(store, { cnt: { x: 2 } })
expect(store.cnt.x).toBe(2)
store.cnt.inc()
expect(store.cnt.x).toBe(5) // incrementor was not lost
})
test("middleware events are correct", () => {
configure({
useProxies: "never"
})
const A = types.model({}).actions(self => ({
a(x: number) {
return this.b(x * 2)
},
b(y: number) {
return y + 1
}
}))
const a = A.create()
const events: IMiddlewareEvent[] = []
addMiddleware(a, function (call, next) {
events.push(call)
return next(call)
})
a.a(7)
const event1 = {
args: [7],
context: {},
id: 1,
name: "a",
parentId: 0,
rootId: 1,
allParentIds: [],
tree: {},
type: "action",
parentEvent: undefined,
parentActionEvent: undefined
} as IMiddlewareEvent
const event2 = {
args: [14],
context: {},
id: 2,
name: "b",
parentId: 1,
rootId: 1,
allParentIds: [1],
tree: {},
type: "action",
parentEvent: event1,
parentActionEvent: event1
} as IMiddlewareEvent
expect(events).toEqual([event1, event2])
})
test("actions are mockable", () => {
configure({
useProxies: "never"
})
const M = types
.model()
.actions(self => ({
method(): number {
return 3
}
}))
.views(self => ({
view(): number {
return 3
}
}))
const m = M.create()
if (process.env.NODE_ENV === "production") {
expect(() => {
m.method = function () {
return 3
}
}).toThrow(TypeError)
expect(() => {
m.view = function () {
return 3
}
}).toThrow(TypeError)
} else {
m.method = function () {
return 4
}
expect(m.method()).toBe(4)
m.view = function () {
return 4
}
expect(m.view()).toBe(4)
}
})
test("after attach action should work correctly", () => {
const Todo = types
.model({
title: "test"
})
.actions(self => ({
remove() {
getRoot(self).remove(cast(self))
}
}))
const S = types
.model({
todos: types.array(Todo)
})
.actions(self => ({
remove(todo: Instance) {
self.todos.remove(todo)
}
}))
const s = S.create({
todos: [{ title: "todo" }]
})
const events: ISerializedActionCall[] = []
onAction(
s,
call => {
events.push(call)
},
true
)
s.todos[0].remove()
expect(events).toEqual([
{
args: [],
name: "remove",
path: "/todos/0"
}
])
})
================================================
FILE: __tests__/core/actionTrackingMiddleware2.test.ts
================================================
import {
addMiddleware,
createActionTrackingMiddleware2,
types,
flow,
IActionTrackingMiddleware2Call
} from "../../src"
import { expect, it, test } from "bun:test"
function createTestMiddleware(m: any, actionName: string, value: number, calls: string[]) {
function checkCall(call: IActionTrackingMiddleware2Call) {
expect(call.name).toBe(actionName)
expect(call.args).toEqual([value])
expect(call.context).toBe(m)
expect(call.env).toBe(call.id)
}
const mware = createActionTrackingMiddleware2({
filter(call) {
return call.name === actionName
},
onStart(call) {
call.env = call.id // just to check env is copied properly down
calls.push(`${call.name} (${call.id}) - onStart`)
checkCall(call)
},
onFinish(call, error) {
calls.push(`${call.name} (${call.id}) - onFinish (error: ${!!error})`)
checkCall(call)
}
})
addMiddleware(m, mware, false)
}
async function doTest(m: any, mode: "success" | "fail") {
const calls: string[] = []
createTestMiddleware(m, "setX", 10, calls)
createTestMiddleware(m, "setY", 9, calls)
try {
await m.setZ(8) // -> setY(9) -> setX(10)
if (mode === "fail") {
expect().fail("should have failed")
}
} catch (e) {
if (mode === "fail") {
expect(e).toBe("error")
} else {
throw e
// fail("should have succeeded")
}
}
return calls
}
async function syncTest(mode: "success" | "fail") {
const M = types
.model({
x: 1,
y: 2,
z: 3
})
.actions(self => ({
setX(v: number) {
self.x = v
if (mode === "fail") {
throw "error"
}
},
setY(v: number) {
self.y = v
this.setX(v + 1)
},
setZ(v: number) {
self.z = v
this.setY(v + 1)
}
}))
const m = M.create()
const calls = await doTest(m, mode)
if (mode === "success") {
expect(calls).toEqual([
"setY (2) - onStart",
"setX (3) - onStart",
"setX (3) - onFinish (error: false)",
"setY (2) - onFinish (error: false)"
])
} else {
expect(calls).toEqual([
"setY (5) - onStart",
"setX (6) - onStart",
"setX (6) - onFinish (error: true)",
"setY (5) - onFinish (error: true)"
])
}
}
/**
* This test checks that the middleware is called and
*/
test("sync action", async () => {
await syncTest("success")
await syncTest("fail")
})
async function flowTest(mode: "success" | "fail") {
const _subFlow = flow(function* subFlow() {
yield Promise.resolve()
})
const M = types
.model({
x: 1,
y: 2,
z: 3
})
.actions(self => ({
setX: flow(function* flowSetX(v: number) {
yield Promise.resolve()
yield _subFlow()
self.x = v
if (mode === "fail") {
throw "error"
}
}),
setY: flow(function* flowSetY(v: number) {
self.y = v
yield (self as any).setX(v + 1)
}),
setZ: flow(function* flowSetZ(v: number) {
self.z = v
yield (self as any).setY(v + 1)
})
}))
const m = M.create()
const calls = await doTest(m, mode)
if (mode === "success") {
expect(calls).toEqual([
"setY (3) - onStart",
"setX (5) - onStart",
"setX (5) - onFinish (error: false)",
"setY (3) - onFinish (error: false)"
])
} else {
expect(calls).toEqual([
"setY (10) - onStart",
"setX (12) - onStart",
"setX (12) - onFinish (error: true)",
"setY (10) - onFinish (error: true)"
])
}
}
test("flow action", async () => {
await flowTest("success")
await flowTest("fail")
})
test("#1250", async () => {
const M = types
.model({
x: 0,
y: 0
})
.actions(self => ({
setX: flow(function* () {
self.x = 10
yield new Promise(resolve => setTimeout(resolve, 10))
}),
setY() {
self.y = 10
}
}))
const calls: string[] = []
const mware = createActionTrackingMiddleware2({
filter(call) {
calls.push(
`${call.name} (${call.id}) <- (${call.parentCall && call.parentCall.id}) - filter`
)
return true
},
onStart(call) {
calls.push(
`${call.name} (${call.id}) <- (${call.parentCall && call.parentCall.id}) - onStart`
)
},
onFinish(call, error) {
calls.push(
`${call.name} (${call.id}) <- (${
call.parentCall && call.parentCall.id
}) - onFinish (error: ${!!error})`
)
}
})
const model = M.create({})
addMiddleware(model, mware, false)
expect(model.x).toBe(0)
expect(model.y).toBe(0)
expect(calls).toEqual([])
const p = model.setX()
expect(model.x).toBe(10)
expect(model.y).toBe(0)
expect(calls).toEqual(["setX (1) <- (undefined) - filter", "setX (1) <- (undefined) - onStart"])
calls.length = 0
await new Promise(r =>
setTimeout(() => {
model.setY()
r()
}, 5)
)
expect(model.x).toBe(10)
expect(model.y).toBe(10)
expect(calls).toEqual([
"setY (3) <- (undefined) - filter",
"setY (3) <- (undefined) - onStart",
"setY (3) <- (undefined) - onFinish (error: false)"
])
calls.length = 0
await p
expect(model.x).toBe(10)
expect(model.y).toBe(10)
expect(calls).toEqual(["setX (1) <- (undefined) - onFinish (error: false)"])
calls.length = 0
})
/**
* Test that when createActionTrackingMiddleware2 is called with valid hooks and a synchronous action, it runs onStart and onFinish hooks.
*/
test("successful execution", () => {
const M = types.model({}).actions(self => ({
test() {}
}))
const calls: string[] = []
const mware = createActionTrackingMiddleware2({
filter(call) {
calls.push(`${call.name} - filter`)
return true
},
onStart(call) {
calls.push(`${call.name} - onStart`)
},
onFinish(call, error) {
calls.push(`${call.name} - onFinish (error: ${!!error})`)
}
})
const model = M.create({})
addMiddleware(model, mware, false)
model.test()
expect(calls).toEqual(["test - filter", "test - onStart", "test - onFinish (error: false)"])
})
/**
* Test that when createActionTrackingMiddleware2 is called with valid hooks and an asynchronous action, it runs onStart and onFinish hooks.
*/
test("successful execution with async action", async () => {
const M = types.model({}).actions(self => ({
async test() {}
}))
const calls: string[] = []
const mware = createActionTrackingMiddleware2({
filter(call) {
calls.push(`${call.name} - filter`)
return true
},
onStart(call) {
calls.push(`${call.name} - onStart`)
},
onFinish(call, error) {
calls.push(`${call.name} - onFinish (error: ${!!error})`)
}
})
const model = M.create({})
addMiddleware(model, mware, false)
await model.test()
expect(calls).toEqual(["test - filter", "test - onStart", "test - onFinish (error: false)"])
})
/**
* Test that when the filter returns true, the action is tracked. We check
* this by checking that the onStart and onFinish hooks are called for `runThisOne`,
* which is the name provided to the `filter` function.
*/
it("calls onStart and onFinish hooks for actions that pass the filter", () => {
const M2 = types.model({}).actions(self => ({
trackThisOne() {},
doNotTrackThisOne() {}
}))
const calls: string[] = []
const mware2 = createActionTrackingMiddleware2({
filter(call) {
return call.name === "trackThisOne"
},
onStart(call) {
calls.push(`${call.name} - onStart`)
},
onFinish(call, error) {
calls.push(`${call.name} - onFinish (error: ${!!error})`)
}
})
const model2 = M2.create({})
addMiddleware(model2, mware2, false)
model2.trackThisOne()
// We call this action to prove that it is not tracked since it fails - there's also a test for this below.
model2.doNotTrackThisOne()
expect(calls).toEqual(["trackThisOne - onStart", "trackThisOne - onFinish (error: false)"])
})
/**
* Test that when the filter returns false, the action is not tracked. We check
* this by checking that the onStart and onFinish hooks are not called for `doNotTrackThisOne`,
*/
it("does not call onStart and onFinish hooks for actions that do not pass the filter", () => {
const M = types.model({}).actions(self => ({
trackThisOne() {},
doNotTrackThisOne() {}
}))
const calls: string[] = []
const mware = createActionTrackingMiddleware2({
filter(call) {
return call.name === "trackThisOne"
},
onStart(call) {
calls.push(`${call.name} - onStart`)
},
onFinish(call, error) {
calls.push(`${call.name} - onFinish (error: ${!!error})`)
}
})
const model = M.create({})
addMiddleware(model, mware, false)
model.doNotTrackThisOne()
expect(calls).toEqual([])
})
/**
* Test that parent actions and child actions have the expected order of operations -
* if we had an action `a` that called an action `b1`, then `b2` inside `a`, the flow would be:
*
* - `filter(a)`
* - `onStart(a)`
* - `filter(b1)`
* - `onStart(b1)`
* - `onFinish(b1)`
* - `filter(b2)`
* - `onStart(b2)`
* - `onFinish(b2)`
* - `onFinish(a)`
*
* See https://mobx-state-tree.js.org/API/#createactiontrackingmiddleware2
*/
test("complete in the expected recursive order", () => {
const M = types
.model({})
.actions(self => ({
childAction1() {},
childAction2() {}
}))
.actions(self => ({
parentAction() {
self.childAction1()
self.childAction2()
}
}))
const calls: string[] = []
const mware = createActionTrackingMiddleware2({
filter(call) {
calls.push(`${call.name} - filter`)
return true
},
onStart(call) {
calls.push(`${call.name} - onStart`)
},
onFinish(call, error) {
calls.push(`${call.name} - onFinish (error: ${!!error})`)
}
})
const model = M.create({})
addMiddleware(model, mware, false)
model.parentAction()
expect(calls).toEqual([
"parentAction - filter",
"parentAction - onStart",
"childAction1 - filter",
"childAction1 - onStart",
"childAction1 - onFinish (error: false)",
"childAction2 - filter",
"childAction2 - onStart",
"childAction2 - onFinish (error: false)",
"parentAction - onFinish (error: false)"
])
})
================================================
FILE: __tests__/core/api.test.ts
================================================
import { expect, test } from "bun:test"
import { readFileSync } from "fs"
import * as mst from "../../src"
function stringToArray(s: string): string[] {
return s.split(",").map(str => str.trim())
}
const METHODS_AND_INTERNAL_TYPES = stringToArray(`
typecheck,
escapeJsonPath,
unescapeJsonPath,
joinJsonPath,
splitJsonPath,
decorate,
addMiddleware,
isStateTreeNode,
flow,
castFlowReturn,
applyAction,
onAction,
recordActions,
createActionTrackingMiddleware,
createActionTrackingMiddleware2,
setLivelinessChecking,
getLivelinessChecking,
getType,
getChildType,
onPatch,
onSnapshot,
applyPatch,
recordPatches,
protect,
unprotect,
isProtected,
applySnapshot,
getSnapshot,
hasParent,
getParent,
hasParentOfType,
getParentOfType,
getRoot,
getPath,
getPathParts,
isRoot,
resolvePath,
resolveIdentifier,
getIdentifier,
tryResolve,
getRelativePath,
clone,
detach,
destroy,
isAlive,
addDisposer,
getEnv,
hasEnv,
walk,
getMembers,
getPropertyMembers,
cast,
castToSnapshot,
castToReferenceSnapshot,
isType,
isArrayType,
isFrozenType,
isIdentifierType,
isLateType,
isLiteralType,
isMapType,
isModelType,
isOptionalType,
isPrimitiveType,
isReferenceType,
isRefinementType,
isUnionType,
isValidReference,
tryReference,
types,
t,
getNodeId,
getRunningActionContext,
isActionContextChildOf,
isActionContextThisOrChildOf,
toGeneratorFunction,
toGenerator
`)
const DEPRECATED_METHODS_AND_INTERNAL_TYPES = stringToArray(`
setLivelynessChecking,
process
`)
const METHODS = METHODS_AND_INTERNAL_TYPES.filter(s => s[0].toLowerCase() === s[0])
const INTERNAL_TYPES = METHODS_AND_INTERNAL_TYPES.filter(s => s[0].toUpperCase() === s[0])
const TYPES = stringToArray(`
enumeration,
model,
compose,
custom,
reference,
safeReference,
union,
optional,
literal,
maybe,
maybeNull,
refinement,
string,
boolean,
number,
integer,
float,
finite,
bigint,
Date,
map,
array,
frozen,
identifier,
identifierNumber,
late,
lazy,
undefined,
null,
snapshotProcessor
`)
test("correct api exposed", () => {
expect(
Object.keys(mst)
.sort()
.filter(key => (mst as any)[key] !== undefined) // filter out interfaces
.filter(s => !DEPRECATED_METHODS_AND_INTERNAL_TYPES.includes(s))
).toEqual([...METHODS, ...INTERNAL_TYPES].sort())
})
test("correct types exposed", () => {
expect(Object.keys(mst.types).sort()).toEqual(TYPES.sort())
})
test("types also exposed on t module", () => {
expect(Object.keys(mst.t).sort()).toEqual(TYPES.sort())
})
test("all methods mentioned in API docs", () => {
const apimd = readFileSync(__dirname + "/../../docs/API/index.md", "utf8")
const missing = TYPES.map(type => "types." + type).filter(
identifier => apimd.indexOf(identifier) === -1
)
missing.push(
...METHODS.filter(identifier => apimd.indexOf("#" + identifier.toLowerCase()) === -1)
)
expect(missing).toEqual(["types.lazy", "types"])
})
test("only accepted dependencies", () => {
const validDeps: string[] = ["ts-essentials"]
const deps =
JSON.parse(readFileSync(__dirname + "/../../package.json", "utf8")).dependencies || {}
const depNames = Object.keys(deps) || []
expect(depNames.sort()).toEqual(validDeps.sort())
})
================================================
FILE: __tests__/core/array.test.ts
================================================
import {
unprotect,
onSnapshot,
onPatch,
clone,
isAlive,
applyPatch,
getPath,
applySnapshot,
getSnapshot,
types,
IJsonPatch,
setLivelinessChecking,
detach,
cast
} from "../../src"
import { observable, autorun, configure } from "mobx"
import { expect, test } from "bun:test"
const createTestFactories = () => {
const ItemFactory = types.optional(
types.model({
to: "world"
}),
{}
)
const Factory = types.array(ItemFactory)
return { Factory, ItemFactory }
}
// === FACTORY TESTS ===
test("it should create a factory", () => {
const { Factory } = createTestFactories()
expect(getSnapshot(Factory.create())).toEqual([])
})
test("it should succeed if not optional and no default provided", () => {
const Factory = types.array(types.string)
expect(getSnapshot(Factory.create())).toEqual([])
})
test("it should restore the state from the snapshot", () => {
configure({
useProxies: "never"
})
const { Factory } = createTestFactories()
const instance = Factory.create([{ to: "universe" }])
expect(getSnapshot(instance)).toEqual([{ to: "universe" }])
expect("" + instance).toBe("AnonymousModel@/0") // just the normal to string
})
// === SNAPSHOT TESTS ===
test("it should emit snapshots", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
let snapshots: (typeof Factory.SnapshotType)[] = []
onSnapshot(doc, snapshot => snapshots.push(snapshot))
doc.push(ItemFactory.create())
expect(snapshots).toEqual([[{ to: "world" }]])
})
test("it should apply snapshots", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
applySnapshot(doc, [{ to: "universe" }])
expect(getSnapshot(doc)).toEqual([{ to: "universe" }])
})
test("it should return a snapshot", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
doc.push(ItemFactory.create())
expect(getSnapshot(doc)).toEqual([{ to: "world" }])
})
// === PATCHES TESTS ===
test("it should emit add patches", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
let patches: IJsonPatch[] = []
onPatch(doc, patch => patches.push(patch))
doc.push(ItemFactory.create({ to: "universe" }))
expect(patches).toEqual([{ op: "add", path: "/0", value: { to: "universe" } }])
})
test("it should apply an add patch", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
applyPatch(doc, { op: "add", path: "/0", value: { to: "universe" } })
expect(getSnapshot(doc)).toEqual([{ to: "universe" }])
})
test("it should emit update patches", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
doc.push(ItemFactory.create())
let patches: IJsonPatch[] = []
onPatch(doc, patch => patches.push(patch))
doc[0] = ItemFactory.create({ to: "universe" })
expect(patches).toEqual([{ op: "replace", path: "/0", value: { to: "universe" } }])
})
test("it should apply an update patch", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
applyPatch(doc, { op: "replace", path: "/0", value: { to: "universe" } })
expect(getSnapshot(doc)).toEqual([{ to: "universe" }])
})
test("it should emit remove patches", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
doc.push(ItemFactory.create())
let patches: IJsonPatch[] = []
onPatch(doc, patch => patches.push(patch))
doc.splice(0)
expect(patches).toEqual([{ op: "replace", path: "", value: [] }])
})
test("it should apply a remove patch", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
doc.push(ItemFactory.create())
doc.push(ItemFactory.create({ to: "universe" }))
applyPatch(doc, { op: "remove", path: "/0" })
expect(getSnapshot(doc)).toEqual([{ to: "universe" }])
})
test("it should apply patches", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
applyPatch(doc, [
{ op: "add", path: "/0", value: { to: "mars" } },
{ op: "replace", path: "/0", value: { to: "universe" } }
])
expect(getSnapshot(doc)).toEqual([{ to: "universe" }])
})
// === TYPE CHECKS ===
test("it should check the type correctly", () => {
const { Factory } = createTestFactories()
const doc = Factory.create()
expect(Factory.is(doc)).toEqual(true)
expect(Factory.is([])).toEqual(true)
expect(Factory.is({})).toEqual(false)
expect(Factory.is([{ to: "mars" }])).toEqual(true)
expect(Factory.is([{ wrongKey: true }])).toEqual(true)
expect(Factory.is([{ to: true }])).toEqual(false)
})
test("paths shoud remain correct when splicing", () => {
const Task = types.model("Task", {
done: false
})
const store = types
.model({
todos: types.array(Task)
})
.create({
todos: [{}]
})
unprotect(store)
expect(store.todos.map(getPath)).toEqual(["/todos/0"])
store.todos.push({})
expect(store.todos.map(getPath)).toEqual(["/todos/0", "/todos/1"])
store.todos.unshift({})
expect(store.todos.map(getPath)).toEqual(["/todos/0", "/todos/1", "/todos/2"])
store.todos.splice(0, 2)
expect(store.todos.map(getPath)).toEqual(["/todos/0"])
store.todos.splice(0, 1, {}, {}, {})
expect(store.todos.map(getPath)).toEqual(["/todos/0", "/todos/1", "/todos/2"])
store.todos.remove(store.todos[1])
expect(store.todos.map(getPath)).toEqual(["/todos/0", "/todos/1"])
})
test("items should be reconciled correctly when splicing - 1", () => {
configure({
useProxies: "never"
})
const Task = types.model("Task", {
x: types.string
})
const a = Task.create({ x: "a" }),
b = Task.create({ x: "b" }),
c = Task.create({ x: "c" }),
d = Task.create({ x: "d" })
const store = types
.model({
todos: types.array(Task)
})
.create({
todos: [a]
})
unprotect(store)
expect(store.todos.slice()).toEqual([a])
expect(isAlive(a)).toBe(true)
store.todos.push(b)
expect(store.todos.slice()).toEqual([a, b])
store.todos.unshift(c)
expect(store.todos.slice()).toEqual([c, a, b])
store.todos.splice(0, 2)
expect(store.todos.slice()).toEqual([b])
expect(isAlive(a)).toBe(false)
expect(isAlive(b)).toBe(true)
expect(isAlive(c)).toBe(false)
setLivelinessChecking("error")
expect(() => store.todos.splice(0, 1, a, c, d)).toThrow(
"You are trying to read or write to an object that is no longer part of a state tree. (Object type: 'Task', Path upon death: '/todos/1', Subpath: '', Action: ''). Either detach nodes first, or don't use objects after removing / replacing them in the tree."
)
store.todos.splice(0, 1, clone(a), clone(c), clone(d))
expect(store.todos.map(_ => _.x)).toEqual(["a", "c", "d"])
})
test("items should be reconciled correctly when splicing - 2", () => {
const Task = types.model("Task", {
x: types.string
})
const a = Task.create({ x: "a" }),
b = Task.create({ x: "b" }),
c = Task.create({ x: "c" }),
d = Task.create({ x: "d" })
const store = types
.model({
todos: types.array(Task)
})
.create({
todos: [a, b, c, d]
})
unprotect(store)
store.todos.splice(2, 1, { x: "e" }, { x: "f" })
// becomes, a, b, e, f, d
expect(store.todos.length).toBe(5)
expect(store.todos[0] === a).toBe(true)
expect(store.todos[1] === b).toBe(true)
expect(store.todos[2] !== c).toBe(true)
expect(store.todos[2].x).toBe("e")
expect(store.todos[3] !== d).toBe(true)
expect(store.todos[3].x).toBe("f")
expect(store.todos[4] === d).toBe(true) // preserved and moved
expect(store.todos[4].x).toBe("d")
expect(store.todos.map(getPath)).toEqual([
"/todos/0",
"/todos/1",
"/todos/2",
"/todos/3",
"/todos/4"
])
store.todos.splice(1, 3, { x: "g" })
// becomes a, g, d
expect(store.todos.length).toBe(3)
expect(store.todos[0] === a).toBe(true)
expect(store.todos[1].x).toBe("g")
expect(store.todos[2].x).toBe("d")
expect(store.todos[1] !== b).toBe(true)
expect(store.todos[2] === d).toBe(true) // still original d
expect(store.todos.map(getPath)).toEqual(["/todos/0", "/todos/1", "/todos/2"])
})
test("it should reconciliate keyed instances correctly", () => {
const Store = types.model({
todos: types.optional(
types.array(
types.model("Task", {
id: types.identifier,
task: "",
done: false
})
),
[]
)
})
const store = Store.create({
todos: [
{ id: "1", task: "coffee", done: false },
{ id: "2", task: "tea", done: false },
{ id: "3", task: "biscuit", done: false }
]
})
expect(store.todos.map(todo => todo.task)).toEqual(["coffee", "tea", "biscuit"])
expect(store.todos.map(todo => todo.done)).toEqual([false, false, false])
expect(store.todos.map(todo => todo.id)).toEqual(["1", "2", "3"])
const coffee = store.todos[0]
const tea = store.todos[1]
const biscuit = store.todos[2]
applySnapshot(store, {
todos: [
{ id: "2", task: "Tee", done: true },
{ id: "1", task: "coffee", done: true },
{ id: "4", task: "biscuit", done: false },
{ id: "5", task: "stuffz", done: false }
]
})
expect(store.todos.map(todo => todo.task)).toEqual(["Tee", "coffee", "biscuit", "stuffz"])
expect(store.todos.map(todo => todo.done)).toEqual([true, true, false, false])
expect(store.todos.map(todo => todo.id)).toEqual(["2", "1", "4", "5"])
expect(store.todos[0] === tea).toBe(true)
expect(store.todos[1] === coffee).toBe(true)
expect(store.todos[2] === biscuit).toBe(false)
})
test("it correctly reconciliate when swapping", () => {
const Task = types.model("Task", {})
const Store = types.model({
todos: types.optional(types.array(Task), [])
})
const s = Store.create()
unprotect(s)
const a = Task.create()
const b = Task.create()
s.todos.push(a, b)
s.todos.replace([b, a])
expect(s.todos[0] === b).toBe(true)
expect(s.todos[1] === a).toBe(true)
expect(s.todos.map(getPath)).toEqual(["/todos/0", "/todos/1"])
})
test("it correctly reconciliate when swapping using snapshots", () => {
const Task = types.model("Task", {})
const Store = types.model({
todos: types.array(Task)
})
const s = Store.create()
unprotect(s)
const a = Task.create()
const b = Task.create()
s.todos.push(a, b)
s.todos.replace([getSnapshot(b), getSnapshot(a)])
expect(s.todos[0] === b).toBe(true)
expect(s.todos[1] === a).toBe(true)
expect(s.todos.map(getPath)).toEqual(["/todos/0", "/todos/1"])
s.todos.push({})
expect(s.todos[0] === b).toBe(true)
expect(s.todos[1] === a).toBe(true)
expect(s.todos.map(getPath)).toEqual(["/todos/0", "/todos/1", "/todos/2"])
})
test("it should not be allowed to add the same item twice to the same store", () => {
const Task = types.model("Task", {})
const Store = types.model({
todos: types.optional(types.array(Task), [])
})
const s = Store.create()
unprotect(s)
const a = Task.create()
s.todos.push(a)
expect(() => {
s.todos.push(a)
}).toThrow(
"Cannot add an object to a state tree if it is already part of the same or another state tree. Tried to assign an object to '/todos/1', but it lives already at '/todos/0'"
)
const b = Task.create()
expect(() => {
s.todos.push(b, b)
}).toThrow(
"Cannot add an object to a state tree if it is already part of the same or another state tree. Tried to assign an object to '/todos/2', but it lives already at '/todos/1'"
)
})
test("it should support observable arrays", () => {
const TestArray = types.array(types.number)
const testArray = TestArray.create(observable([1, 2]))
expect(testArray[0] === 1).toBe(true)
expect(testArray.length === 2).toBe(true)
expect(Array.isArray(testArray.slice())).toBe(true)
})
test("it should support observable arrays, array should be real when useProxies eq 'always'", () => {
const TestArray = types.array(types.number)
const testArray = TestArray.create(observable([1, 2]))
expect(testArray[0] === 1).toBe(true)
expect(testArray.length === 2).toBe(true)
expect(Array.isArray(testArray)).toBe(true)
})
test("it should support observable arrays, array should be not real when useProxies eq 'never'", () => {
configure({
useProxies: "never"
})
const TestArray = types.array(types.number)
const testArray = TestArray.create(observable([1, 2]))
expect(testArray[0] === 1).toBe(true)
expect(testArray.length === 2).toBe(true)
expect(Array.isArray(testArray.slice())).toBe(true)
expect(Array.isArray(testArray)).toBe(false)
})
test("it should correctly handle re-adding of the same objects", () => {
const Store = types
.model("Task", {
objects: types.array(types.maybe(types.frozen()))
})
.actions(self => ({
setObjects(objects: {}[]) {
self.objects.replace(objects)
}
}))
const store = Store.create({
objects: []
})
expect(store.objects.slice()).toEqual([])
const someObject = {}
store.setObjects([someObject])
expect(store.objects.slice()).toEqual([someObject])
store.setObjects([someObject])
expect(store.objects.slice()).toEqual([someObject])
})
test("it should work correctly for splicing primitive array", () => {
const store = types.array(types.number).create([1, 2, 3])
unprotect(store)
store.splice(0, 1)
expect(store.slice()).toEqual([2, 3])
store.unshift(1)
expect(store.slice()).toEqual([1, 2, 3])
store.replace([4, 5])
expect(store.slice()).toEqual([4, 5])
store.clear()
expect(store.slice()).toEqual([])
})
test("it should keep unchanged for structrual equalled snapshot", () => {
const Store = types.model({
todos: types.array(
types.model("Task", {
id: types.identifier,
task: "",
done: false
})
),
numbers: types.array(types.number)
})
const store = Store.create({
todos: [
{ id: "1", task: "coffee", done: false },
{ id: "2", task: "tea", done: false },
{ id: "3", task: "biscuit", done: false }
],
numbers: [1, 2, 3]
})
const values: boolean[][] = []
autorun(() => {
values.push(store.todos.map(todo => todo.done))
})
applySnapshot(store.todos, [
{ id: "1", task: "coffee", done: false },
{ id: "2", task: "tea", done: false },
{ id: "3", task: "biscuit", done: true }
])
applySnapshot(store.todos, [
{ id: "1", task: "coffee", done: false },
{ id: "2", task: "tea", done: false },
{ id: "3", task: "biscuit", done: true }
])
expect(values).toEqual([
[false, false, false],
[false, false, true]
])
const values1: number[][] = []
autorun(() => {
values1.push(store.numbers.slice())
})
applySnapshot(store.numbers, [1, 2, 4])
applySnapshot(store.numbers, [1, 2, 4])
expect(values1).toEqual([
[1, 2, 3],
[1, 2, 4]
])
})
// === OPERATIONS TESTS ===
test("#1105 - it should return pop/shift'ed values for scalar arrays", () => {
const ScalarArray = types
.model({
array: types.array(types.number)
})
.actions(self => {
return {
shift() {
return self.array.shift()
}
}
})
const test = ScalarArray.create({ array: [3, 5] })
expect(test.shift()).toEqual(3)
expect(test.shift()).toEqual(5)
})
test("it should return pop/shift'ed values for object arrays", () => {
const TestObject = types.model({ id: types.string })
const ObjectArray = types
.model({
array: types.array(TestObject)
})
.actions(self => {
return {
shift() {
return self.array.shift()
},
pop() {
return self.array.pop()
}
}
})
const test = ObjectArray.create({
array: [{ id: "foo" }, { id: "mid" }, { id: "bar" }]
})
const foo = test.shift()!
expect(isAlive(foo)).toBe(false)
const bar = test.pop()!
expect(isAlive(bar)).toBe(false)
// we have to use clone or getSnapshot to access dead nodes data
expect(clone(foo)).toEqual({ id: "foo" })
expect(getSnapshot(bar)).toEqual({ id: "bar" })
})
test("#1173 - detaching an array should not eliminate its children", () => {
const M = types.model({})
const AM = types.array(M)
const Store = types.model({ items: AM })
const s = Store.create({ items: [{}, {}, {}] })
const n0 = s.items[0]
unprotect(s)
const detachedItems = detach(s.items)
expect(s.items).not.toBe(detachedItems)
expect(s.items.length).toBe(0)
expect(detachedItems.length).toBe(3)
expect(detachedItems[0]).toBe(n0)
})
test("initializing an array instance from another array instance should end up in the same instance", () => {
const A = types.array(types.number)
const a1 = A.create([1, 2, 3])
const a2 = A.create(a1)
expect(a1).toBe(a2)
expect(getSnapshot(a1)).toEqual([1, 2, 3])
})
test("assigning filtered instances works", () => {
const Task = types.model("Task", {
done: false
})
const store = types
.model({
todos: types.array(Task)
})
.actions(self => ({
clearFinishedTodos() {
self.todos = cast(self.todos.filter(todo => !todo.done))
}
}))
.create({
todos: [{ done: true }, { done: false }, { done: true }]
})
expect(store.todos.length).toBe(3)
const done = store.todos.filter(t => t.done)
const notDone = store.todos.filter(t => !t.done)
expect(store.todos.every(t => isAlive(t)))
store.clearFinishedTodos()
expect(store.todos.length).toBe(1)
expect(store.todos[0]).toBe(notDone[0])
expect(done.every(t => !isAlive(t))).toBe(true)
expect(notDone.every(t => isAlive(t))).toBe(true)
})
test("#1676 - should accept read-only arrays", () => {
const ArrayType = types.array(types.string)
const data = ["foo", "bar"] as const
const instance = ArrayType.create(data)
expect(getSnapshot(instance)).toEqual(["foo", "bar"])
})
================================================
FILE: __tests__/core/async.test.ts
================================================
import { configure, reaction } from "mobx"
import {
addMiddleware,
decorate,
destroy,
flow,
IMiddlewareEvent,
IMiddlewareEventType,
IMiddlewareHandler,
recordActions,
toGenerator,
// TODO: export IRawActionCall
toGeneratorFunction,
types
} from "../../src"
import { expect, test } from "bun:test"
import type { Writable } from "ts-essentials"
function delay(time: number, value: TV, shouldThrow = false): Promise {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldThrow) reject(value)
else resolve(value)
}, time)
})
}
async function testCoffeeTodo(
generator: (
self: any
) => (str: string) => Generator, string | void | undefined, undefined>,
shouldError: boolean,
resultValue: string | undefined,
producedCoffees: any[]
) {
const Todo = types
.model({
title: "get coffee"
})
.actions(self => ({
startFetch: flow(generator(self))
}))
const events: IMiddlewareEvent[] = []
const t1 = Todo.create({})
addMiddleware(t1, (c, next) => {
events.push(c)
return next(c)
})
const coffees: any[] = []
reaction(
() => t1.title,
coffee => coffees.push(coffee)
)
try {
configure({ enforceActions: "observed" })
const result = await t1.startFetch("black")
expect(shouldError).toBe(false)
expect(result).toBe(resultValue)
} catch (error) {
expect(shouldError).toBe(true)
} finally {
configure({ enforceActions: "never" })
}
expect(coffees).toEqual(producedCoffees)
const filtered = filterRelevantStuff(events)
expect(filtered).toMatchSnapshot()
}
test("flow happens in single ticks", async () => {
const X = types
.model({
y: 1
})
.actions(self => ({
p: flow(function* () {
self.y++
self.y++
yield delay(1, true, false)
self.y++
self.y++
})
}))
const x = X.create()
const values: number[] = []
reaction(
() => x.y,
v => values.push(v)
)
await x.p()
expect(x.y).toBe(5)
expect(values).toEqual([3, 5])
})
test("can handle async actions", () => {
testCoffeeTodo(
self =>
function* fetchData(kind: string) {
self.title = "getting coffee " + kind
self.title = yield delay(100, "drinking coffee")
return "awake"
},
false,
"awake",
["getting coffee black", "drinking coffee"]
)
})
test("can handle erroring actions", () => {
testCoffeeTodo(
self =>
function* fetchData(kind: string) {
throw kind
},
true,
"black",
[]
)
})
test("can handle try catch", () => {
testCoffeeTodo(
self =>
function* fetchData(kind: string) {
try {
yield delay(10, "tea", true)
return undefined
} catch (e) {
self.title = e
return "biscuit"
}
},
false,
"biscuit",
["tea"]
)
})
test("empty sequence works", () => {
testCoffeeTodo(() => function* fetchData(kind: string) {}, false, undefined, [])
})
test("can handle throw from yielded promise works", () => {
testCoffeeTodo(
() =>
function* fetchData(kind: string) {
yield delay(10, "x", true)
},
true,
"x",
[]
)
})
test("typings", async () => {
const M = types.model({ title: types.string }).actions(self => {
function* a(x: string) {
yield delay(10, "x", false)
self.title = "7"
return 23
}
// tslint:disable-next-line:no-shadowed-variable
const b = flow(function* b(x: string) {
yield delay(10, "x", false)
self.title = "7"
return 24
})
return { a: flow(a), b }
})
const m1 = M.create({ title: "test " })
const resA = m1.a("z")
const resB = m1.b("z")
const [x1, x2] = await Promise.all([resA, resB])
expect(x1).toBe(23)
expect(x2).toBe(24)
})
test("typings", async () => {
const M = types.model({ title: types.string }).actions(self => {
function* a(x: string) {
yield delay(10, "x", false)
self.title = "7"
return 23
}
// tslint:disable-next-line:no-shadowed-variable
const b = flow(function* b(x: string) {
yield delay(10, "x", false)
self.title = "7"
return 24
})
return { a: flow(a), b }
})
const m1 = M.create({ title: "test " })
const resA = m1.a("z")
const resB = m1.b("z")
const [x1, x2] = await Promise.all([resA, resB])
expect(x1).toBe(23)
expect(x2).toBe(24)
})
test("recordActions should only emit invocation", async () => {
let calls = 0
const M = types
.model({
title: types.string
})
.actions(self => {
function* a(x: string) {
yield delay(10, "x", false)
calls++
return 23
}
return {
a: flow(a)
}
})
const m1 = M.create({ title: "test " })
const recorder = recordActions(m1)
await m1.a("x")
recorder.stop()
expect(recorder.actions).toEqual([
{
args: ["x"],
name: "a",
path: ""
}
])
expect(calls).toBe(1)
recorder.replay(m1)
await new Promise(resolve => setTimeout(resolve, 50))
expect(calls).toBe(2)
})
test("can handle nested async actions", () => {
// tslint:disable-next-line:no-shadowed-variable
const uppercase = flow(function* uppercase(value: string) {
const res = yield delay(20, value.toUpperCase())
return res
})
testCoffeeTodo(
self =>
function* fetchData(kind: string) {
self.title = yield uppercase("drinking " + kind)
return self.title
},
false,
"DRINKING BLACK",
["DRINKING BLACK"]
)
})
test("can handle nested async actions when using decorate", async () => {
const events: [IMiddlewareEventType, string][] = []
const middleware: IMiddlewareHandler = (call, next) => {
events.push([call.type, call.name])
return next(call)
}
// tslint:disable-next-line:no-shadowed-variable
const uppercase = flow(function* uppercase(value: string) {
const res = yield delay(20, value.toUpperCase())
return res
})
const Todo = types.model({}).actions(self => {
// tslint:disable-next-line:no-shadowed-variable
const act = flow(function* act(value: string) {
return yield uppercase(value)
})
return { act: decorate(middleware, act) }
})
const res = await Todo.create().act("x")
expect(res).toBe("X")
expect(events).toEqual([
["action", "act"],
["flow_spawn", "act"],
["flow_resume", "act"],
["flow_resume", "act"],
["flow_return", "act"]
])
})
test("flow gain back control when node become not alive during yield", async () => {
expect.assertions(2)
const rejectError = new Error("Reject Error")
const MyModel = types.model({}).actions(() => {
return {
doAction() {
return flow(function* () {
try {
yield delay(20, "").then(() => Promise.reject(rejectError))
} catch (e) {
expect(e).toEqual(rejectError)
throw e
}
})()
}
}
})
const m = MyModel.create({})
const p = m.doAction()
destroy(m)
try {
await p
} catch (e) {
expect(e).toEqual(rejectError)
}
})
function filterRelevantStuff(stuff: Partial>[]) {
return stuff.map(x => {
delete x.context
delete x.tree
return x
})
}
test("flow typings", async () => {
const promise = Promise.resolve()
const M = types.model({ x: 5 }).actions(self => ({
// should be () => Promise
voidToVoid: flow(function* () {
yield promise
}), // should be (val: number) => Promise
numberToNumber: flow(function* (val: number) {
yield promise
return val
}), // should be () => Promise
voidToNumber: flow(function* () {
yield promise
return Promise.resolve(2)
})
}))
const m = M.create()
// these should compile
const a: void = await m.voidToVoid()
expect(a).toBe(undefined)
const b: number = await m.numberToNumber(4)
expect(b).toBe(4)
const c: number = await m.voidToNumber()
expect(c).toBe(2)
await m.voidToNumber().then(d => {
const _d: number = d
expect(_d).toBe(2)
})
})
/**
* Detect explicit `any` type.
* https://stackoverflow.com/a/55541672/4289902
*/
type IfAny = 0 extends 1 & T ? Y : N
/**
* Ensure that the type of the passed value is of the expected type, and is NOT the TypeScript `any` type
*/
function ensureNotAnyType(value: IfAny) {}
test("yield* typings for toGeneratorFunction", async () => {
const voidPromise = () => Promise.resolve()
const numberPromise = () => Promise.resolve(7)
const stringWithArgsPromise = (input1: string, input2: boolean) =>
Promise.resolve("test-result")
const voidGen = toGeneratorFunction(voidPromise)
const numberGen = toGeneratorFunction(numberPromise)
const stringWithArgsGen = toGeneratorFunction(stringWithArgsPromise)
const M = types.model({ x: 5 }).actions(self => {
function* testAction() {
const voidResult = yield* voidGen()
ensureNotAnyType(voidResult)
const numberResult = yield* numberGen()
ensureNotAnyType(numberResult)
const stringResult = yield* stringWithArgsGen("input", true)
ensureNotAnyType(stringResult)
return stringResult
}
return {
testAction: flow(testAction)
}
})
const m = M.create()
const result = await m.testAction()
ensureNotAnyType(result)
expect(result).toBe("test-result")
})
test("yield* typings for toGenerator", async () => {
const voidPromise = () => Promise.resolve()
const numberPromise = () => Promise.resolve(7)
const stringWithArgsPromise = (input1: string, input2: boolean) =>
Promise.resolve("test-result")
const M = types.model({ x: 5 }).actions(self => {
function* testAction() {
const voidResult = yield* toGenerator(voidPromise())
ensureNotAnyType(voidResult)
const numberResult = yield* toGenerator(numberPromise())
ensureNotAnyType(numberResult)
const stringResult = yield* toGenerator(stringWithArgsPromise("input", true))
ensureNotAnyType(stringResult)
return stringResult
}
return {
testAction: flow(testAction)
}
})
const m = M.create()
const result = await m.testAction()
ensureNotAnyType(result)
expect(result).toBe("test-result")
})
================================================
FILE: __tests__/core/bigint.test.ts
================================================
import { t } from "../../src"
import { Hook, NodeLifeCycle } from "../../src/internal"
import { describe, it, expect, test } from "bun:test"
describe("types.bigint", () => {
describe("methods", () => {
describe("create", () => {
describe("with no arguments", () => {
if (process.env.NODE_ENV !== "production") {
it("should throw an error in development", () => {
expect(() => {
t.bigint.create()
}).toThrow()
})
}
})
describe("with a bigint argument", () => {
it("should return a bigint", () => {
const n = t.bigint.create(BigInt(1))
expect(typeof n).toBe("bigint")
})
})
describe("with a number argument", () => {
it("should return a bigint", () => {
const n = t.bigint.create(1)
expect(typeof n).toBe("bigint")
expect(n).toBe(BigInt(1))
})
})
describe("with a string argument", () => {
it("should return a bigint", () => {
const n = t.bigint.create("2")
expect(typeof n).toBe("bigint")
expect(n).toBe(BigInt(2))
})
})
describe("with argument of different types", () => {
const testCases = [
null,
undefined,
true,
[],
function () {},
new Date(),
/a/,
new Map(),
new Set(),
Symbol(),
new Error()
]
if (process.env.NODE_ENV !== "production") {
testCases.forEach(testCase => {
it(`should throw an error when passed ${JSON.stringify(testCase)}`, () => {
expect(() => {
t.bigint.create(testCase as any)
}).toThrow()
})
})
}
})
})
describe("describe", () => {
it("should return the value 'bigint'", () => {
const description = t.bigint.describe()
expect(description).toBe("bigint")
})
})
describe("getSnapshot", () => {
it("should return the value as string (JSON-safe)", () => {
const n = t.bigint.instantiate(null, "", {}, BigInt(1))
const snapshot = t.bigint.getSnapshot(n)
expect(snapshot).toBe("1")
expect(typeof snapshot).toBe("string")
})
})
describe("getSubtype", () => {
it("should return null", () => {
const subtype = t.bigint.getSubTypes()
expect(subtype).toBe(null)
})
})
describe("instantiate", () => {
if (process.env.NODE_ENV !== "production") {
describe("with invalid arguments", () => {
it("should throw when passed undefined", () => {
expect(() => {
t.bigint.instantiate(null, "", {}, undefined as any)
}).toThrow()
})
})
}
describe("with a bigint argument", () => {
it("should return an object", () => {
const n = t.bigint.instantiate(null, "", {}, BigInt(1))
expect(typeof n).toBe("object")
})
})
})
describe("is", () => {
describe("with a bigint argument", () => {
it("should return true", () => {
const result = t.bigint.is(BigInt(1))
expect(result).toBe(true)
})
})
describe("with argument of different types", () => {
const testCases = [
null,
undefined,
true,
[],
function () {},
new Date(),
/a/,
new Map(),
new Set(),
Symbol(),
new Error()
]
testCases.forEach(testCase => {
it(`should return false when passed ${JSON.stringify(testCase)}`, () => {
const result = t.bigint.is(testCase as any)
expect(result).toBe(false)
})
})
})
describe("with a string argument", () => {
it("should return true (string is valid snapshot input)", () => {
expect(t.bigint.is("1")).toBe(true)
})
})
describe("with a number argument", () => {
it("should return true (number is valid snapshot input)", () => {
expect(t.bigint.is(1)).toBe(true)
})
})
})
describe("isAssignableFrom", () => {
describe("with a bigint argument", () => {
it("should return true", () => {
const result = t.bigint.isAssignableFrom(t.bigint)
expect(result).toBe(true)
})
})
describe("with argument of different types", () => {
const testCases = [
t.Date,
t.boolean,
t.finite,
t.float,
t.identifier,
t.identifierNumber,
t.integer,
t.null,
t.string,
t.undefined
]
testCases.forEach(testCase => {
it(`should return false when passed ${JSON.stringify(testCase)}`, () => {
const result = t.bigint.isAssignableFrom(testCase as any)
expect(result).toBe(false)
})
})
})
})
describe("validate", () => {
describe("with a bigint, string or number argument", () => {
it("should return with no validation errors for bigint", () => {
const result = t.bigint.validate(BigInt(1), [])
expect(result).toEqual([])
})
it("should return with no validation errors for string", () => {
const result = t.bigint.validate("1", [])
expect(result).toEqual([])
})
it("should return with no validation errors for number", () => {
const result = t.bigint.validate(1, [])
expect(result).toEqual([])
})
})
describe("with argument of different types", () => {
const testCases = [
null,
undefined,
true,
[],
function () {},
new Date(),
/a/,
new Map(),
new Set(),
Symbol(),
new Error()
]
testCases.forEach(testCase => {
it(`should return with a validation error when passed ${JSON.stringify(
testCase
)}`, () => {
const result = t.bigint.validate(testCase as any, [])
expect(result).toEqual([
{
context: [],
message: "Value is not a bigint",
value: testCase
}
])
})
})
})
})
})
describe("properties", () => {
describe("flags", () => {
test("return the correct value", () => {
const flags = t.bigint.flags
expect(flags).toBe(1 << 23)
})
})
describe("identifierAttribute", () => {
test("returns undefined", () => {
const identifierAttribute = t.bigint.identifierAttribute
expect(identifierAttribute).toBeUndefined()
})
})
describe("isType", () => {
test("returns true", () => {
const isType = t.bigint.isType
expect(isType).toBe(true)
})
})
describe("name", () => {
test('returns "bigint"', () => {
const name = t.bigint.name
expect(name).toBe("bigint")
})
})
})
describe("instance", () => {
describe("methods", () => {
describe("aboutToDie", () => {
it("calls the beforeDetach hook", () => {
const n = t.bigint.instantiate(null, "", {}, BigInt(1))
let called = false
n.registerHook(Hook.beforeDestroy, () => {
called = true
})
n.aboutToDie()
expect(called).toBe(true)
})
})
describe("die", () => {
it("kills the node", () => {
const n = t.bigint.instantiate(null, "", {}, BigInt(1))
n.die()
expect(n.isAlive).toBe(false)
})
it("should mark the node as dead", () => {
const n = t.bigint.instantiate(null, "", {}, BigInt(1))
n.die()
expect(n.state).toBe(NodeLifeCycle.DEAD)
})
})
describe("finalizeCreation", () => {
it("should mark the node as finalized", () => {
const n = t.bigint.instantiate(null, "", {}, BigInt(1))
n.finalizeCreation()
expect(n.state).toBe(NodeLifeCycle.FINALIZED)
})
})
describe("finalizeDeath", () => {
it("should mark the node as dead", () => {
const n = t.bigint.instantiate(null, "", {}, BigInt(1))
n.finalizeDeath()
expect(n.state).toBe(NodeLifeCycle.DEAD)
})
})
describe("getReconciliationType", () => {
it("should return the correct type", () => {
const n = t.bigint.instantiate(null, "", {}, BigInt(1))
const type = n.getReconciliationType()
expect(type).toBe(t.bigint)
})
})
describe("getSnapshot", () => {
it("should return the value as string (JSON-safe)", () => {
const n = t.bigint.instantiate(null, "", {}, BigInt(1))
const snapshot = n.getSnapshot()
expect(snapshot).toBe("1")
expect(typeof snapshot).toBe("string")
})
})
describe("registerHook", () => {
it("should register a hook and call it", () => {
const n = t.bigint.instantiate(null, "", {}, BigInt(1))
let called = false
n.registerHook(Hook.beforeDestroy, () => {
called = true
})
n.die()
expect(called).toBe(true)
})
})
describe("setParent", () => {
if (process.env.NODE_ENV !== "production") {
describe("with null", () => {
it("should throw an error", () => {
const n = t.bigint.instantiate(null, "", {}, BigInt(1))
expect(() => {
n.setParent(null, "foo")
}).toThrow()
})
})
describe("with a parent object", () => {
it("should throw an error", () => {
const Parent = t.model({
child: t.bigint
})
const parent = Parent.create({ child: BigInt(1) })
const n = t.bigint.instantiate(null, "", {}, BigInt(1))
expect(() => {
// @ts-ignore
n.setParent(parent, "bar")
}).toThrow(
"[mobx-state-tree] assertion failed: scalar nodes cannot change their parent"
)
})
})
}
})
})
})
})
================================================
FILE: __tests__/core/boolean.test.ts
================================================
import { t } from "../../src"
import { Hook, NodeLifeCycle } from "../../src/internal"
import { describe, expect, it, test } from "bun:test"
describe("types.boolean", () => {
describe("methods", () => {
describe("create", () => {
describe("with no arguments", () => {
if (process.env.NODE_ENV !== "production") {
it("should throw an error in development", () => {
expect(() => {
t.boolean.create()
}).toThrow()
})
}
})
describe("with a boolean argument", () => {
it("should return a boolean", () => {
const n = t.boolean.create(true)
expect(typeof n).toBe("boolean")
})
})
describe("with argument of different types", () => {
const testCases = [
null,
undefined,
"string",
1,
[],
function () {},
new Date(),
/a/,
new Map(),
new Set(),
Symbol(),
new Error(),
Infinity,
NaN
]
if (process.env.NODE_ENV !== "production") {
testCases.forEach(testCase => {
it(`should throw an error when passed ${JSON.stringify(testCase)}`, () => {
expect(() => {
t.boolean.create(testCase as any)
}).toThrow()
})
})
}
})
})
describe("describe", () => {
it("should return the value 'boolean'", () => {
const description = t.boolean.describe()
expect(description).toBe("boolean")
})
})
describe("getSnapshot", () => {
it("should return the value passed in", () => {
const b = t.boolean.instantiate(null, "", {}, true)
const snapshot = t.boolean.getSnapshot(b)
expect(snapshot).toBe(true)
})
})
describe("getSubtype", () => {
it("should return null", () => {
const subtype = t.boolean.getSubTypes()
expect(subtype).toBe(null)
})
})
describe("instantiate", () => {
if (process.env.NODE_ENV !== "production") {
describe("with invalid arguments", () => {
it("should not throw an error", () => {
expect(() => {
// @ts-ignore
t.boolean.instantiate()
}).not.toThrow()
})
})
}
describe("with a boolean argument", () => {
it("should return an object", () => {
const b = t.boolean.instantiate(null, "", {}, true)
expect(typeof b).toBe("object")
})
})
})
describe("is", () => {
describe("with a boolean argument", () => {
it("should return true", () => {
const result = t.boolean.is(true)
expect(result).toBe(true)
})
})
describe("with argument of different types", () => {
const testCases = [
null,
undefined,
"string",
1,
[],
function () {},
new Date(),
/a/,
new Map(),
new Set(),
Symbol(),
new Error(),
Infinity,
NaN
]
testCases.forEach(testCase => {
it(`should return false when passed ${JSON.stringify(testCase)}`, () => {
const result = t.boolean.is(testCase as any)
expect(result).toBe(false)
})
})
})
})
describe("isAssignableFrom", () => {
describe("with a boolean argument", () => {
it("should return true", () => {
const result = t.boolean.isAssignableFrom(t.boolean)
expect(result).toBe(true)
})
})
describe("with argument of different types", () => {
const testCases = [
t.Date,
t.number,
t.finite,
t.float,
t.identifier,
t.identifierNumber,
t.integer,
t.null,
t.string,
t.undefined
]
testCases.forEach(testCase => {
it(`should return false when passed ${JSON.stringify(testCase)}`, () => {
const result = t.boolean.isAssignableFrom(testCase as any)
expect(result).toBe(false)
})
})
})
})
// TODO: we need to test this, but to be honest I'm not sure what the expected behavior is on single boolean nodes.
describe.skip("reconcile", () => {})
describe("validate", () => {
describe("with a boolean argument", () => {
it("should return with no validation errors", () => {
const result = t.boolean.validate(true, [])
expect(result).toEqual([])
})
})
describe("with argument of different types", () => {
const testCases = [
null,
undefined,
"string",
1,
[],
function () {},
new Date(),
/a/,
new Map(),
new Set(),
Symbol(),
new Error(),
Infinity,
NaN
]
testCases.forEach(testCase => {
it(`should return with a validation error when passed ${JSON.stringify(
testCase
)}`, () => {
const result = t.boolean.validate(testCase as any, [])
expect(result).toEqual([
{
context: [],
message: "Value is not a boolean",
value: testCase
}
])
})
})
})
})
})
describe("properties", () => {
describe("flags", () => {
test("return the correct value", () => {
const flags = t.boolean.flags
expect(flags).toBe(4)
})
})
describe("identifierAttribute", () => {
// We don't have a way to set the identifierAttribute on a primitive type, so this should return undefined.
test("returns undefined", () => {
const identifierAttribute = t.boolean.identifierAttribute
expect(identifierAttribute).toBeUndefined()
})
})
describe("isType", () => {
test("returns true", () => {
const isType = t.boolean.isType
expect(isType).toBe(true)
})
})
describe("name", () => {
test('returns "boolean"', () => {
const name = t.boolean.name
expect(name).toBe("boolean")
})
})
})
describe("instance", () => {
describe("methods", () => {
describe("aboutToDie", () => {
it("calls the beforeDetach hook", () => {
const b = t.boolean.instantiate(null, "", {}, true)
let called = false
b.registerHook(Hook.beforeDestroy, () => {
called = true
})
b.aboutToDie()
expect(called).toBe(true)
})
})
describe("die", () => {
it("kills the node", () => {
const b = t.boolean.instantiate(null, "", {}, true)
b.die()
expect(b.isAlive).toBe(false)
})
it("should mark the node as dead", () => {
const b = t.boolean.instantiate(null, "", {}, true)
b.die()
expect(b.state).toBe(NodeLifeCycle.DEAD)
})
})
describe("finalizeCreation", () => {
it("should mark the node as finalized", () => {
const b = t.boolean.instantiate(null, "", {}, true)
b.finalizeCreation()
expect(b.state).toBe(NodeLifeCycle.FINALIZED)
})
})
describe("finalizeDeath", () => {
it("should mark the node as dead", () => {
const b = t.boolean.instantiate(null, "", {}, true)
b.finalizeDeath()
expect(b.state).toBe(NodeLifeCycle.DEAD)
})
})
describe("getReconciliationType", () => {
it("should return the correct type", () => {
const b = t.boolean.instantiate(null, "", {}, true)
const type = b.getReconciliationType()
expect(type).toBe(t.boolean)
})
})
describe("getSnapshot", () => {
it("should return the value passed in", () => {
const b = t.boolean.instantiate(null, "", {}, true)
const snapshot = b.getSnapshot()
expect(snapshot).toBe(true)
})
})
describe("registerHook", () => {
it("should register a hook and call it", () => {
const b = t.boolean.instantiate(null, "", {}, true)
let called = false
b.registerHook(Hook.beforeDestroy, () => {
called = true
})
b.die()
expect(called).toBe(true)
})
})
describe("setParent", () => {
if (process.env.NODE_ENV !== "production") {
describe("with null", () => {
it("should throw an error", () => {
const b = t.boolean.instantiate(null, "", {}, true)
expect(() => {
b.setParent(null, "foo")
}).toThrow()
})
})
describe("with a parent object", () => {
it("should throw an error", () => {
const Parent = t.model({
child: t.boolean
})
const parent = Parent.create({ child: true })
const b = t.boolean.instantiate(null, "", {}, true)
expect(() => {
// @ts-ignore
b.setParent(parent, "bar")
}).toThrow(
"[mobx-state-tree] assertion failed: scalar nodes cannot change their parent"
)
})
})
}
})
})
})
})
================================================
FILE: __tests__/core/boxes-store.test.ts
================================================
import { values } from "mobx"
import {
types,
getParent,
hasParent,
recordPatches,
unprotect,
getSnapshot,
Instance
} from "../../src"
import { expect, test } from "bun:test"
export const Box = types
.model("Box", {
id: types.identifier,
name: "",
x: 0,
y: 0
})
.views(self => ({
get width() {
return self.name.length * 15
},
get isSelected(): boolean {
if (!hasParent(self)) return false
return getParent(getParent(self)).selection === self
}
}))
.actions(self => {
function move(dx: number, dy: number) {
self.x += dx
self.y += dy
}
function setName(newName: string) {
self.name = newName
}
return {
move,
setName
}
})
export const Arrow = types.model("Arrow", {
id: types.identifier,
from: types.reference(Box),
to: types.reference(Box)
})
export const Store = types
.model("Store", {
boxes: types.map(Box),
arrows: types.array(Arrow),
selection: types.reference(Box)
})
.actions(self => {
function afterCreate() {
unprotect(self)
}
function addBox(id: string, name: string, x: number, y: number) {
const box = Box.create({ name, x, y, id })
self.boxes.put(box)
return box
}
function addArrow(id: string, from: string, to: string) {
self.arrows.push(Arrow.create({ id, from, to }))
}
function setSelection(selection: Instance) {
self.selection = selection
}
function createBox(
id: string,
name: string,
x: number,
y: number,
source: Instance | null | undefined,
arrowId: string | null
) {
const box = addBox(id, name, x, y)
setSelection(box)
if (source) addArrow(arrowId!, source.id, box.id)
}
return {
afterCreate,
addBox,
addArrow,
setSelection,
createBox
}
})
function createStore() {
return Store.create({
boxes: {
cc: { id: "cc", name: "Rotterdam", x: 100, y: 100 },
aa: { id: "aa", name: "Bratislava", x: 650, y: 300 }
},
arrows: [{ id: "dd", from: "cc", to: "aa" }],
selection: "aa"
})
}
test("store is deserialized correctly", () => {
const s = createStore()
expect(s.boxes.size).toBe(2)
expect(s.arrows.length).toBe(1)
expect(s.selection === s.boxes.get("aa")).toBe(true)
expect(s.arrows[0].from.name).toBe("Rotterdam")
expect(s.arrows[0].to.name).toBe("Bratislava")
expect(values(s.boxes).map(b => b.isSelected)).toEqual([false, true])
})
test("store emits correct patch paths", () => {
const s = createStore()
const recorder1 = recordPatches(s)
const recorder2 = recordPatches(s.boxes)
const recorder3 = recordPatches(s.boxes.get("cc")!)
s.arrows[0].from.x += 117
expect(recorder1.patches).toEqual([{ op: "replace", path: "/boxes/cc/x", value: 217 }])
expect(recorder2.patches).toEqual([{ op: "replace", path: "/cc/x", value: 217 }])
expect(recorder3.patches).toEqual([{ op: "replace", path: "/x", value: 217 }])
})
test("box operations works correctly", () => {
const s = createStore()
s.createBox("a", "A", 0, 0, null, null)
s.createBox("b", "B", 100, 100, s.boxes.get("aa"), "aa2b")
expect(getSnapshot(s)).toEqual({
boxes: {
cc: { id: "cc", name: "Rotterdam", x: 100, y: 100 },
aa: { id: "aa", name: "Bratislava", x: 650, y: 300 },
a: { id: "a", name: "A", x: 0, y: 0 },
b: { id: "b", name: "B", x: 100, y: 100 }
},
arrows: [
{ id: "dd", from: "cc", to: "aa" },
{ id: "aa2b", from: "aa", to: "b" }
],
selection: "b"
})
s.boxes.get("a")!.setName("I'm groot")
expect(getSnapshot(s)).toEqual({
boxes: {
cc: { id: "cc", name: "Rotterdam", x: 100, y: 100 },
aa: { id: "aa", name: "Bratislava", x: 650, y: 300 },
a: { id: "a", name: "I'm groot", x: 0, y: 0 },
b: { id: "b", name: "B", x: 100, y: 100 }
},
arrows: [
{ id: "dd", from: "cc", to: "aa" },
{ id: "aa2b", from: "aa", to: "b" }
],
selection: "b"
})
expect(JSON.stringify(s)).toEqual(JSON.stringify(getSnapshot(s)))
s.boxes.get("a")!.move(50, 50)
expect(getSnapshot(s)).toEqual({
boxes: {
cc: { id: "cc", name: "Rotterdam", x: 100, y: 100 },
aa: { id: "aa", name: "Bratislava", x: 650, y: 300 },
a: { id: "a", name: "I'm groot", x: 50, y: 50 },
b: { id: "b", name: "B", x: 100, y: 100 }
},
arrows: [
{ id: "dd", from: "cc", to: "aa" },
{ id: "aa2b", from: "aa", to: "b" }
],
selection: "b"
})
expect(s.boxes.get("b")!.width).toBe(15)
expect(Box.create({ id: "hello" }).isSelected).toBe(false)
})
================================================
FILE: __tests__/core/circular1.test.ts
================================================
import { types } from "../../src"
import { LateTodo2, LateStore2 } from "./circular2.test"
import { expect, test } from "bun:test"
// combine function hosting with types.late to support circular refs between files!
export function LateStore1() {
return types.model({
todo: types.late(LateTodo2)
})
}
export function LateTodo1() {
return types.model({
done: types.boolean
})
}
test("circular test 1 should work", () => {
const Store1 = types.late(LateStore1)
const Store2 = types.late(LateStore2)
expect(Store1.is({})).toBe(false)
expect(Store1.is({ todo: { done: true } })).toBe(true)
const s1 = Store1.create({ todo: { done: true } })
expect(s1.todo.done).toBe(true)
expect(Store2.is({})).toBe(false)
expect(Store2.is({ todo: { done: true } })).toBe(true)
const s2 = Store2.create({ todo: { done: true } })
expect(s2.todo.done).toBe(true)
})
================================================
FILE: __tests__/core/circular2.test.ts
================================================
import { types } from "../../src"
import { LateTodo1, LateStore1 } from "./circular1.test"
import { expect, test } from "bun:test"
// combine function hosting with types.late to support circular refs between files!
export function LateTodo2() {
return types.model({
done: types.boolean
})
}
export function LateStore2() {
return types.model({
todo: types.late(LateTodo1)
})
}
test("circular test 2 should work", () => {
const Store1 = types.late(LateStore1)
const Store2 = types.late(LateStore2)
expect(Store1.is({})).toBe(false)
expect(Store1.is({ todo: { done: true } })).toBe(true)
const s1 = Store1.create({ todo: { done: true } })
expect(s1.todo.done).toBe(true)
expect(Store2.is({})).toBe(false)
expect(Store2.is({ todo: { done: true } })).toBe(true)
const s2 = Store2.create({ todo: { done: true } })
expect(s2.todo.done).toBe(true)
})
================================================
FILE: __tests__/core/custom-type.test.ts
================================================
import {
types,
recordPatches,
onSnapshot,
unprotect,
applySnapshot,
applyPatch,
SnapshotOut
} from "../../src"
import { expect, jest, test } from "bun:test"
class Decimal {
public number: number
public fraction: number
constructor(value: string) {
const parts = value.split(".")
this.number = Number(parts[0])
this.fraction = Number(parts[1])
}
toNumber() {
return this.number + Number("0." + this.fraction)
}
toString() {
return `${this.number}.${this.fraction}`
}
}
{
const DecimalPrimitive = types.custom({
name: "Decimal",
fromSnapshot(value: string, env: any) {
if (env && env.test) env.test(value)
return new Decimal(value)
},
toSnapshot(value: Decimal) {
return value.toString()
},
isTargetType(value: string | Decimal): value is Decimal {
return value instanceof Decimal
},
getValidationMessage(value: string): string {
if (/^-?\d+\.\d+$/.test(value)) return "" // OK
return `'${value}' doesn't look like a valid decimal number`
}
})
const Wallet = types.model({
balance: DecimalPrimitive,
lastTransaction: types.maybeNull(DecimalPrimitive)
})
test("it should allow for custom primitive types", () => {
const w1 = Wallet.create({
balance: new Decimal("2.5")
})
expect(w1.balance.number).toBe(2)
expect(w1.balance.fraction).toBe(5)
const w2 = Wallet.create({ balance: "3.5" })
expect(w2.balance.number).toBe(3)
expect(w2.balance.fraction).toBe(5)
if (process.env.NODE_ENV !== "production")
expect(() => Wallet.create({ balance: "two point one" })).toThrow(
"(Invalid value for type 'Decimal': 'two point one' doesn't look like a valid decimal number)"
)
})
// test reassignment / reconcilation / conversion works
test("reassignments will work", () => {
const w1 = Wallet.create({ balance: "2.5" })
unprotect(w1)
const p = recordPatches(w1)
const snapshots: SnapshotOut[] = []
onSnapshot(w1, s => {
snapshots.push(s)
})
const b1 = w1.balance
expect(b1).toBeInstanceOf(Decimal)
w1.balance = "2.5" as any // TODO: make cast work with custom types
expect(b1).toBeInstanceOf(Decimal)
expect(w1.balance).toBe(b1) // reconciled
w1.balance = new Decimal("2.5") // not reconciling! // TODO: introduce custom hook for that?
expect(b1).toBeInstanceOf(Decimal)
w1.balance = new Decimal("3.5")
expect(b1).toBeInstanceOf(Decimal)
w1.balance = "4.5" as any
expect(b1).toBeInstanceOf(Decimal)
w1.lastTransaction = b1
expect(w1.lastTransaction).toBe(b1)
w1.lastTransaction = null
expect(w1.lastTransaction).toBe(null)
// patches & snapshots
expect(snapshots).toMatchSnapshot()
p.stop()
expect(p.patches).toMatchSnapshot()
})
test("passes environment to fromSnapshot", () => {
const env = { test: jest.fn() }
Wallet.create({ balance: "3.0" }, env)
expect(env.test).toHaveBeenCalledWith("3.0")
})
}
{
test("complex representation", () => {})
const DecimalTuple = types.custom<[number, number], Decimal>({
name: "DecimalTuple",
fromSnapshot(value: [number, number]) {
return new Decimal(value[0] + "." + value[1])
},
toSnapshot(value: Decimal) {
return [value.number, value.fraction]
},
isTargetType(value: [number, number] | Decimal): value is Decimal {
return value instanceof Decimal
},
getValidationMessage(value: [number, number]): string {
if (Array.isArray(value) && value.length === 2) return "" // OK
return `'${JSON.stringify(value)}' doesn't look like a valid decimal number`
}
})
const Wallet = types.model({ balance: DecimalTuple })
test("it should allow for complex custom primitive types", () => {
const w1 = Wallet.create({
balance: new Decimal("2.5")
})
expect(w1.balance.number).toBe(2)
expect(w1.balance.fraction).toBe(5)
const w2 = Wallet.create({ balance: [3, 5] })
expect(w2.balance.number).toBe(3)
expect(w2.balance.fraction).toBe(5)
if (process.env.NODE_ENV !== "production")
expect(() => Wallet.create({ balance: "two point one" } as any)).toThrow(
"(Invalid value for type 'DecimalTuple': '\"two point one\"' doesn't look like a valid decimal number)"
)
})
// test reassignment / reconcilation / conversion works
test("complex reassignments will work", () => {
const w1 = Wallet.create({ balance: [2, 5] })
unprotect(w1)
const p = recordPatches(w1)
const snapshots: SnapshotOut[] = []
onSnapshot(w1, s => {
snapshots.push(s)
})
const b1 = w1.balance
expect(b1).toBeInstanceOf(Decimal)
w1.balance = [2, 5] as any
expect(b1).toBeInstanceOf(Decimal)
expect(w1.balance).not.toBe(b1) // not reconciled, balance is not deep equaled (TODO: future feature?)
w1.balance = new Decimal("2.5") // not reconciling!
expect(b1).toBeInstanceOf(Decimal)
w1.balance = new Decimal("3.5")
expect(b1).toBeInstanceOf(Decimal)
w1.balance = [4, 5] as any
expect(b1).toBeInstanceOf(Decimal)
// patches & snapshots
expect(snapshots).toMatchSnapshot()
p.stop()
expect(p.patches).toMatchSnapshot()
})
test("can apply snapshot and patch", () => {
const w1 = Wallet.create({ balance: [3, 0] })
applySnapshot(w1, { balance: [4, 5] })
expect(w1.balance).toBeInstanceOf(Decimal)
expect(w1.balance.toString()).toBe("4.5")
applyPatch(w1, {
op: "replace",
path: "/balance",
value: [5, 0]
})
expect(w1.balance.toString()).toBe("5.0")
})
}
================================================
FILE: __tests__/core/date.test.ts
================================================
import { t } from "../../src"
import { Hook, NodeLifeCycle } from "../../src/internal"
import { describe, expect, it, test } from "bun:test"
describe("types.date", () => {
describe("methods", () => {
describe("create", () => {
describe("with no arguments", () => {
if (process.env.NODE_ENV !== "production") {
it("should throw an error in development", () => {
expect(() => {
t.Date.create()
}).toThrow()
})
}
})
describe("with a number argument", () => {
it("should return a Date object", () => {
const d = t.Date.create(1701369873059)
expect(d instanceof Date).toBe(true)
})
})
describe("with a Date argument", () => {
it("should return a Date object", () => {
const input = new Date()
const d = t.Date.create(input)
expect(d instanceof Date).toBe(true)
})
})
})
describe("with argument of different types", () => {
const testCases = [
null,
undefined,
true,
[],
function () {},
"2022-01-01T00:00:00.000Z",
/a/,
new Map(),
new Set(),
Symbol(),
new Error()
]
if (process.env.NODE_ENV !== "production") {
testCases.forEach(testCase => {
it(`should throw an error when passed ${JSON.stringify(testCase)}`, () => {
expect(() => {
t.Date.create(testCase as any)
}).toThrow()
})
})
}
})
})
describe("describe", () => {
it("should return the value 'Date'", () => {
const description = t.Date.describe()
expect(description).toBe("Date")
})
})
describe("getSnapshot", () => {
it("should return a number from a date", () => {
const date = new Date("2022-01-01T00:00:00.000Z")
const d = t.Date.instantiate(null, "", {}, date)
const snapshot = t.Date.getSnapshot(d)
expect(snapshot).toBe(1640995200000)
})
it("should return a number from a number", () => {
const d = t.Date.instantiate(null, "", {}, 1701369873059)
const snapshot = t.Date.getSnapshot(d)
expect(snapshot).toBe(1701369873059)
})
})
describe("getSubtype", () => {
it("should return null", () => {
const subtype = t.Date.getSubTypes()
expect(subtype).toBe(null)
})
})
describe("instantiate", () => {
if (process.env.NODE_ENV !== "production") {
describe("with invalid arguments", () => {
it("should not throw an error", () => {
expect(() => {
// @ts-ignore
t.Date.instantiate()
}).not.toThrow()
})
})
}
describe("with a Date argument", () => {
it("should return an object", () => {
const s = t.Date.instantiate(null, "", {}, new Date())
expect(typeof s).toBe("object")
})
})
describe("with a number argument", () => {
it("should return an object", () => {
const s = t.Date.instantiate(null, "", {}, 1701369873059)
expect(typeof s).toBe("object")
})
})
})
describe("is", () => {
describe("with a Date argument", () => {
it("should return true", () => {
const result = t.Date.is(new Date())
expect(result).toBe(true)
})
})
describe("with a number argument", () => {
it("should return true", () => {
const result = t.Date.is(1701369873059)
expect(result).toBe(true)
})
})
describe("with argument of different types", () => {
const testCases = [
null,
undefined,
true,
[],
function () {},
"2022-01-01T00:00:00.000Z",
/a/,
new Map(),
new Set(),
Symbol(),
new Error()
]
testCases.forEach(testCase => {
it(`should return false when passed ${JSON.stringify(testCase)}`, () => {
const result = t.Date.is(testCase as any)
expect(result).toBe(false)
})
})
})
})
describe("isAssignableFrom", () => {
describe("with a Date argument", () => {
it("should return true", () => {
const result = t.Date.isAssignableFrom(t.Date)
expect(result).toBe(true)
})
})
describe("with argument of different types", () => {
const testCases = [
t.string,
t.boolean,
t.finite,
t.float,
t.identifier,
t.identifierNumber,
t.integer,
t.null,
t.number,
t.undefined
]
testCases.forEach(testCase => {
it(`should return false when passed ${JSON.stringify(testCase)}`, () => {
const result = t.Date.isAssignableFrom(testCase as any)
expect(result).toBe(false)
})
})
})
// TODO: we need to test this, but to be honest I'm not sure what the expected behavior is on single date nodes.
describe.skip("reconcile", () => {})
describe("validate", () => {
describe("with a Date argument", () => {
it("should return with no validation errors", () => {
const result = t.Date.validate(new Date(), [])
expect(result).toEqual([])
})
})
describe("with a number argument", () => {
it("should return with no validation errors", () => {
const result = t.Date.validate(1701369873059, [])
expect(result).toEqual([])
})
})
describe("with argument of different types", () => {
const testCases = [
null,
undefined,
"2022-01-01T00:00:00.000Z",
true,
[],
function () {},
/a/,
new Map(),
new Set(),
Symbol(),
new Error()
]
testCases.forEach(testCase => {
it(`should return with a validation error when passed ${JSON.stringify(
testCase
)}`, () => {
const result = t.Date.validate(testCase as any, [])
expect(result).toEqual([
{
context: [],
message: "Value is not a Date or a unix milliseconds timestamp",
value: testCase
}
])
})
})
})
})
})
describe("properties", () => {
describe("flags", () => {
test("return the correct value", () => {
const flags = t.Date.flags
expect(flags).toBe(8)
})
})
describe("identifierAttribute", () => {
// We don't have a way to set the identifierAttribute on a primitive type, so this should return undefined.
test("returns undefined", () => {
const identifierAttribute = t.Date.identifierAttribute
expect(identifierAttribute).toBeUndefined()
})
})
describe("isType", () => {
test("returns true", () => {
const isType = t.Date.isType
expect(isType).toBe(true)
})
})
describe("name", () => {
test('returns "Date"', () => {
const name = t.Date.name
expect(name).toBe("Date")
})
})
})
describe("instance", () => {
describe("methods", () => {
describe("aboutToDie", () => {
it("calls the beforeDetach hook", () => {
const d = t.Date.instantiate(null, "", {}, new Date())
let called = false
d.registerHook(Hook.beforeDestroy, () => {
called = true
})
d.aboutToDie()
expect(called).toBe(true)
})
})
describe("die", () => {
it("kills the node", () => {
const d = t.Date.instantiate(null, "", {}, new Date())
d.die()
expect(d.isAlive).toBe(false)
})
it("should mark the node as dead", () => {
const d = t.Date.instantiate(null, "", {}, new Date())
d.die()
expect(d.state).toBe(NodeLifeCycle.DEAD)
})
})
describe("finalizeCreation", () => {
it("should mark the node as finalized", () => {
const d = t.Date.instantiate(null, "", {}, new Date())
d.finalizeCreation()
expect(d.state).toBe(NodeLifeCycle.FINALIZED)
})
})
describe("finalizeDeath", () => {
it("should mark the node as dead", () => {
const d = t.Date.instantiate(null, "", {}, new Date())
d.finalizeDeath()
expect(d.state).toBe(NodeLifeCycle.DEAD)
})
})
describe("getReconciliationType", () => {
it("should return the correct type", () => {
const d = t.Date.instantiate(null, "", {}, new Date())
const type = d.getReconciliationType()
expect(type).toBe(t.Date)
})
})
describe("getSnapshot", () => {
it("should return the value passed in with a number", () => {
const d = t.Date.instantiate(null, "", {}, 1701373349399)
const snapshot = d.getSnapshot()
expect(snapshot).toBe(1701373349399)
})
it("should return the correct date getTime value with a date object", () => {
const date = new Date("2022-01-01T00:00:00.000Z")
const d = t.Date.instantiate(null, "", {}, date)
const snapshot = d.getSnapshot()
const expected = date.getTime()
expect(snapshot).toBe(expected)
})
})
describe("registerHook", () => {
it("should register a hook and call it", () => {
const d = t.Date.instantiate(null, "", {}, new Date())
let called = false
d.registerHook(Hook.beforeDestroy, () => {
called = true
})
d.die()
expect(called).toBe(true)
})
})
describe("setParent", () => {
if (process.env.NODE_ENV !== "production") {
describe("with null", () => {
it("should throw an error", () => {
const d = t.Date.instantiate(null, "", {}, new Date())
expect(() => {
d.setParent(null, "foo")
}).toThrow()
})
})
describe("with a parent object", () => {
it("should throw an error", () => {
const Parent = t.model({
child: t.Date
})
const parent = Parent.create({ child: new Date() })
const d = t.Date.instantiate(null, "", {}, new Date())
expect(() => {
// @ts-ignore
d.setParent(parent, "bar")
}).toThrow(
"[mobx-state-tree] assertion failed: scalar nodes cannot change their parent"
)
})
})
}
})
})
})
})
================================================
FILE: __tests__/core/deprecated.test.ts
================================================
import { deprecated } from "../../src/utils"
import { flow, createFlowSpawner } from "../../src/core/flow"
import { process as mstProcess, createProcessSpawner } from "../../src/core/process"
import { expect, jest, test } from "bun:test"
function createDeprecationListener() {
// clear previous deprecation dedupe keys
deprecated.ids = {}
// save console.warn native implementation
const originalWarn = console.warn
// create spy to track warning call
const spyWarn = (console.warn = jest.fn())
// return callback to check if warn was called properly
return function isDeprecated() {
// replace original implementation
console.warn = originalWarn
// test for correct log message, if in development
if (process.env.NODE_ENV !== "production") {
expect(spyWarn).toHaveBeenCalledTimes(1)
expect(spyWarn.mock.calls[0][0].message).toMatch(/Deprecation warning:/)
}
}
}
test("`process` should mirror `flow`", () => {
const isDeprecated = createDeprecationListener()
const generator = function* () {}
const flowResult = flow(generator)
const processResult = mstProcess(generator)
expect(processResult.name).toBe(flowResult.name)
isDeprecated()
})
test("`createProcessSpawner` should mirror `createFlowSpawner`", () => {
const isDeprecated = createDeprecationListener()
const alias = "generatorAlias"
const generator = function* (): IterableIterator {}
const flowSpawnerResult = createFlowSpawner(alias, generator)
const processSpawnerResult = createProcessSpawner(alias, generator)
expect(processSpawnerResult.name).toBe(flowSpawnerResult.name)
isDeprecated()
})
================================================
FILE: __tests__/core/enum.test.ts
================================================
import { types, unprotect } from "../../src"
import { expect, test } from "bun:test"
enum ColorEnum {
Red = "Red",
Orange = "Orange",
Green = "Green"
}
const colorEnumValues = Object.values(ColorEnum) as ColorEnum[]
test("should support enums", () => {
const TrafficLight = types.model({ color: types.enumeration("Color", colorEnumValues) })
expect(TrafficLight.is({ color: ColorEnum.Green })).toBe(true)
expect(TrafficLight.is({ color: "Blue" })).toBe(false)
expect(TrafficLight.is({ color: undefined })).toBe(false)
const l = TrafficLight.create({ color: ColorEnum.Orange })
unprotect(l)
l.color = ColorEnum.Red
expect(TrafficLight.describe()).toBe('{ color: ("Red" | "Orange" | "Green") }')
if (process.env.NODE_ENV !== "production") {
expect(() => (l.color = "Blue" as any)).toThrow(
/Error while converting `"Blue"` to `Color`/
)
}
})
test("should support anonymous enums", () => {
const TrafficLight = types.model({ color: types.enumeration(colorEnumValues) })
const l = TrafficLight.create({ color: ColorEnum.Orange })
unprotect(l)
l.color = ColorEnum.Red
expect(TrafficLight.describe()).toBe('{ color: ("Red" | "Orange" | "Green") }')
if (process.env.NODE_ENV !== "production") {
expect(() => (l.color = "Blue" as any)).toThrow(
/Error while converting `"Blue"` to `"Red" | "Orange" | "Green"`/
)
}
})
test("should support optional enums", () => {
const TrafficLight = types.optional(types.enumeration(colorEnumValues), ColorEnum.Orange)
const l = TrafficLight.create()
expect(l).toBe(ColorEnum.Orange)
})
test("should support optional enums inside a model", () => {
const TrafficLight = types.model({
color: types.optional(types.enumeration(colorEnumValues), ColorEnum.Orange)
})
const l = TrafficLight.create({})
expect(l.color).toBe(ColorEnum.Orange)
})
test("should support plain string[] arrays", () => {
const colorOptions: string[] = ["Red", "Orange", "Green"]
const TrafficLight = types.model({ color: types.enumeration(colorOptions) })
const l = TrafficLight.create({ color: "Orange" })
unprotect(l)
l.color = "Red"
expect(TrafficLight.describe()).toBe('{ color: ("Red" | "Orange" | "Green") }')
if (process.env.NODE_ENV !== "production") {
expect(() => (l.color = "Blue" as any)).toThrow(
/Error while converting `"Blue"` to `"Red" | "Orange" | "Green"`/
)
}
})
test("should support readonly enums as const", () => {
const colorOptions = ["Red", "Orange", "Green"] as const
const TrafficLight = types.model({ color: types.enumeration(colorOptions) })
const l = TrafficLight.create({ color: "Orange" })
unprotect(l)
l.color = "Red"
expect(TrafficLight.describe()).toBe('{ color: ("Red" | "Orange" | "Green") }')
if (process.env.NODE_ENV !== "production") {
expect(() => (l.color = "Blue" as any)).toThrow(
/Error while converting `"Blue"` to `"Red" | "Orange" | "Green"`/
)
}
})
================================================
FILE: __tests__/core/env.test.ts
================================================
import { configure } from "mobx"
import {
types,
hasEnv,
getEnv,
clone,
detach,
unprotect,
walk,
getPath,
castToSnapshot,
hasParent,
Instance,
destroy,
getParent,
IAnyStateTreeNode,
isStateTreeNode,
isAlive
} from "../../src"
import { expect, test } from "bun:test"
// tslint:disable: no-unused-expression
const Todo = types
.model({
title: "test"
})
.views(self => ({
get description() {
return getEnv(self).useUppercase ? self.title.toUpperCase() : self.title
}
}))
const Store = types.model({
todos: types.array(Todo)
})
function createEnvironment() {
return {
useUppercase: true
}
}
test("it should be possible to use environments", () => {
const env = createEnvironment()
const todo = Todo.create({}, env)
expect(hasEnv(todo)).toBe(true)
expect(todo.description).toBe("TEST")
env.useUppercase = false
expect(todo.description).toBe("test")
})
test("it should be possible to inherit environments", () => {
const env = createEnvironment()
const store = Store.create({ todos: [{}] }, env)
expect(hasEnv(store.todos[0])).toBe(true)
expect(store.todos[0].description).toBe("TEST")
env.useUppercase = false
expect(store.todos[0].description).toBe("test")
})
test("getEnv throws error without environment", () => {
const todo = Todo.create()
expect(hasEnv(todo)).toBe(false)
expect(() => getEnv(todo)).toThrow("Failed to find the environment of AnonymousModel@")
})
test("detach should preserve environment", () => {
const env = createEnvironment()
const store = Store.create({ todos: [{}] }, env)
unprotect(store)
const todo = detach(store.todos[0])
expect(hasEnv(todo)).toBe(true)
expect(todo.description).toBe("TEST")
env.useUppercase = false
expect(todo.description).toBe("test")
})
test("it is possible to assign instance with the same environment as the parent to a tree", () => {
const env = createEnvironment()
const store = Store.create({ todos: [] }, env)
const todo = Todo.create({}, env)
unprotect(store)
store.todos.push(todo)
expect(store.todos.length === 1).toBe(true)
expect(getEnv(store.todos) === getEnv(store.todos[0])).toBe(true)
expect(getEnv(todo) === getEnv(store.todos[0])).toBe(true)
})
test("it is not possible to assign instance with a different environment than the parent to a tree", () => {
if (process.env.NODE_ENV !== "production") {
const env1 = createEnvironment()
const env2 = createEnvironment()
const store = Store.create({ todos: [] }, env1)
const todo = Todo.create({}, env2)
unprotect(store)
expect(() => store.todos.push(todo)).toThrow(
"[mobx-state-tree] A state tree cannot be made part of another state tree as long as their environments are different."
)
}
})
test("it is possible to set a value inside a map of a map when using the same environment", () => {
const env = createEnvironment()
const EmptyModel = types.model({})
const MapOfEmptyModel = types.model({
map: types.map(EmptyModel)
})
const MapOfMapOfEmptyModel = types.model({
map: types.map(MapOfEmptyModel)
})
const mapOfMap = MapOfMapOfEmptyModel.create(
{
map: {
whatever: {
map: {}
}
}
},
env
)
unprotect(mapOfMap)
// this should not throw
mapOfMap.map.get("whatever")!.map.set("1234", EmptyModel.create({}, env))
expect(getEnv(mapOfMap) === env).toBe(true)
expect(getEnv(mapOfMap.map.get("whatever")!.map.get("1234")!) === env).toBe(true)
})
test("clone preserves environment", () => {
const env = createEnvironment()
const store = Store.create({ todos: [{}] }, env)
{
const todo = clone(store.todos[0])
expect(getEnv(todo) === env).toBe(true)
}
{
const todo = clone(store.todos[0], true)
expect(getEnv(todo) === env).toBe(true)
expect(getEnv(todo) === env).toBe(true)
}
{
const todo = clone(store.todos[0], false)
expect(hasEnv(todo)).toBe(false)
expect(() => {
getEnv(todo)
}).toThrow("Failed to find the environment of AnonymousModel@")
}
{
const env2 = createEnvironment()
const todo = clone(store.todos[0], env2)
expect(env2 === getEnv(todo)).toBe(true)
}
})
test("#1231", () => {
configure({
useProxies: "never"
})
const envObj = createEnvironment()
const logs: string[] = []
function nofParents(node: IAnyStateTreeNode) {
let parents = 0
let parent = node
while (hasParent(parent)) {
parents++
parent = getParent(parent)
}
return parents
}
function leafsFirst(root: IAnyStateTreeNode) {
const nodes: IAnyStateTreeNode[] = []
walk(root, i => {
if (isStateTreeNode(i)) {
nodes.push(i)
}
})
// sort by number of parents
nodes.sort((a, b) => {
return nofParents(b) - nofParents(a)
})
return nodes
}
function check(root: Instance, name: string, mode: "detach" | "destroy") {
function logFail(operation: string, n: any) {
logs.push(`fail: (${name}) ${operation}: ${getPath(n)}, ${n}`)
}
function log(operation: string, n: any) {
logs.push(`ok: (${name}) ${operation}: ${getPath(n)}, ${n}`)
}
// make sure all nodes are there
root.s1.arr[0].title
root.s1.m.get("one")!.title
root.s2
const nodes = leafsFirst(root)
expect(nodes.length).toBe(7)
nodes.forEach(i => {
const env = getEnv(i)
const parent = hasParent(i)
if (!parent && i !== root) {
logFail("expected a parent, but none found", i)
} else {
log("had parent or was root", i)
}
if (env !== envObj) {
logFail("expected same env as root, but was different", i)
} else {
log("same env as root", i)
}
})
unprotect(root)
nodes.forEach(i => {
const optional = optionalPaths.includes(getPath(i))
if (mode === "detach") {
log("detaching node", i)
detach(i)
} else {
log("destroying node", i)
destroy(i)
}
const env = hasEnv(i) ? getEnv(i) : undefined
const parent = hasParent(i)
const alive = isAlive(i)
if (mode === "detach") {
if (parent) {
logFail(`expected no parent after detach, but one was found`, i)
} else {
log(`no parent after detach`, i)
}
if (env !== envObj) {
logFail("expected same env as root after detach, but it was not", i)
} else {
log("env kept after detach", i)
}
if (!alive) {
logFail("expected to be alive after detach, but it was not", i)
} else {
log("alive after detach", i)
}
} else {
// destroy might or might not keep the env, but doesn't matter so we don't check
if (optional) {
// optional (undefined) nodes will be assigned undefined and reconciled, therefore they will be kept alive
if (!parent) {
logFail(
`expected a parent after destroy (since it is optional), but none was found`,
i
)
} else {
log(`had parent after destroy (since it is optional)`, i)
}
if (!alive) {
logFail(
"expected to be alive after destroy (since it is optional), but it was not",
i
)
} else {
log("alive after destroy (since it is optional)", i)
}
} else {
if (parent) {
logFail(`expected no parent after destroy, but one was found`, i)
} else {
log(`no parent after destroy`, i)
}
if (alive) {
logFail("expected to be dead after destroy, but it was not", i)
} else {
log("dead after destroy", i)
}
}
}
})
}
const T = types.model("T", { title: "some title" })
const S1Arr = types.array(T)
const S1Map = types.map(T)
const S1 = types.model("S1", {
arr: S1Arr,
m: S1Map
})
const S2 = types.model("S2", {})
const RS = types.model("RS", {
s1: types.optional(S1, {}),
s2: types.optional(S2, {})
})
const optionalPaths = ["/s1", "/s2", "/s1/m", "/s1/arr"]
const data = {
s1: castToSnapshot(
S1.create({
arr: S1Arr.create([T.create({})]),
m: castToSnapshot(S1Map.create({ one: T.create({}) }))
})
),
s2: S2.create()
}
const rsCreate = RS.create(data, envObj)
const rsCreate2 = clone(rsCreate, true)
const rsSnap = RS.create(
{
s1: {
arr: [{}],
m: { one: {} }
},
s2: {}
},
envObj
)
const rsSnap2 = clone(rsCreate, true)
check(rsCreate, "using create", "detach")
check(rsSnap, "using snapshot", "detach")
check(rsCreate2, "using create", "destroy")
check(rsSnap2, "using snapshot", "destroy")
const fails = logs.filter(l => l.startsWith("fail:"))
if (fails.length > 0) {
expect().fail(`\n${fails.join("\n")}`)
}
})
================================================
FILE: __tests__/core/frozen.test.ts
================================================
import { getSnapshot, types, unprotect } from "../../src"
import { expect, test } from "bun:test"
test("it should accept any serializable value", () => {
const Factory = types.model({
value: types.frozen<{ a: number; b: number } | undefined>()
})
const doc = Factory.create()
unprotect(doc)
doc.value = { a: 1, b: 2 }
expect(getSnapshot(doc)).toEqual({ value: { a: 1, b: 2 } })
})
if (process.env.NODE_ENV !== "production") {
test("it should throw if value is not serializable", () => {
const Factory = types.model({
value: types.frozen()
})
const doc = Factory.create()
unprotect(doc)
expect(() => {
doc.value = function IAmUnserializable() {}
}).toThrow(/Error while converting to `frozen`/)
})
}
test("it should accept any default value value", () => {
const Factory = types.model({
value: types.frozen(3)
})
const doc = Factory.create()
expect(Factory.is({})).toBeTruthy()
expect(getSnapshot(doc)).toEqual({ value: 3 })
})
test("it should type strongly", () => {
type Point = { x: number; y: number }
const Mouse = types
.model({
loc: types.frozen()
})
.actions(self => ({
moveABit() {
// self.loc.x += 1; // compile error, x is readonly!
;(self.loc as any).x += 1 // throws, frozen!
}
}))
expect(Mouse.is({})).toBeTruthy() // any value is acceptable to frozen, even undefined...
const m = Mouse.create({
// loc: 3 // type error!
loc: { x: 2, y: 3 }
})
if (process.env.NODE_ENV !== "production") {
expect(() => {
m.moveABit()
}).toThrow("Attempted to assign to readonly property.")
}
})
if (process.env.NODE_ENV !== "production") {
test("it should be capable of using another MST type", () => {
const Point = types.model("Point", { x: types.number, y: types.number })
const Mouse = types.model({
loc: types.frozen(Point)
})
expect(Mouse.is({})).toBeFalsy()
expect(Mouse.is({ loc: {} })).toBeFalsy()
expect(Mouse.is({ loc: { x: 3, y: 2 } })).toBeTruthy()
expect(() => {
;(Mouse.create as any)()
}).toThrow(
'at path "/loc" value `undefined` is not assignable to type: `frozen(Point)` (Value is not a plain object)'
)
expect(() => {
Mouse.create({ loc: { x: 4 } } as any)
}).toThrow(
'at path "/loc/y" value `undefined` is not assignable to type: `number` (Value is not a number)'
)
const m = Mouse.create({
loc: { x: 3, y: 2 }
})
const x = m.loc.x
expect(x).toBe(3)
})
}
================================================
FILE: __tests__/core/hooks.test.ts
================================================
import {
addDisposer,
destroy,
detach,
types,
unprotect,
getSnapshot,
applySnapshot,
onSnapshot,
isAlive,
hasParent,
cast,
resolvePath,
getParent
} from "../../src"
import { expect, jest, test } from "bun:test"
function createTestStore(listener: (s: string) => void) {
const Todo = types
.model("Todo", {
title: ""
})
.actions(self => {
function afterCreate() {
listener("new todo: " + self.title)
addDisposer(self, () => {
listener("custom disposer 1 for " + self.title)
})
addDisposer(self, () => {
listener("custom disposer 2 for " + self.title)
})
}
function beforeDestroy() {
listener("destroy todo: " + self.title)
}
function afterAttach() {
listener("attach todo: " + self.title)
}
function beforeDetach() {
listener("detach todo: " + self.title)
}
return {
afterCreate,
beforeDestroy,
afterAttach,
beforeDetach
}
})
const Store = types
.model("Store", {
todos: types.array(Todo)
})
.actions(self => {
function afterCreate() {
unprotect(self)
listener("new store: " + self.todos.length)
addDisposer(self, () => {
listener("custom disposer for store")
})
}
function beforeDestroy() {
listener("destroy store: " + self.todos.length)
}
return {
afterCreate,
beforeDestroy
}
})
return {
store: Store.create({
todos: [{ title: "Get coffee" }, { title: "Get biscuit" }, { title: "Give talk" }]
}),
Store,
Todo
}
}
// NOTE: as we defer creation (and thus, hooks) till first real access,
// some of original hooks do not fire at all
test("it should trigger lifecycle hooks", () => {
const events: string[] = []
// new store: 3
const { store, Todo } = createTestStore(e => events.push(e))
events.push("-")
// access (new, attach), then detach "Give Talk"
const talk = detach(store.todos[2])
expect(isAlive(talk)).toBe(true)
expect(hasParent(talk)).toBe(false)
events.push("--")
// access (new, attach), then destroy biscuit
const oldBiscuit = store.todos.pop()!
expect(isAlive(oldBiscuit)).toBe(false)
events.push("---")
// new and then attach "add sugar"
const sugar = Todo.create({
title: "add sugar"
})
store.todos.push(sugar)
events.push("----")
// destroy elements in the array ("add sugar"), then store
destroy(store)
expect(isAlive(store)).toBe(false)
events.push("-----")
// destroy "Give talk"
destroy(talk)
expect(isAlive(talk)).toBe(false)
expect(events).toEqual([
"new store: 3",
"-",
"new todo: Give talk",
"attach todo: Give talk",
"detach todo: Give talk",
"--",
"new todo: Get biscuit",
"attach todo: Get biscuit",
"destroy todo: Get biscuit",
"custom disposer 2 for Get biscuit",
"custom disposer 1 for Get biscuit",
"---",
"new todo: add sugar",
"attach todo: add sugar",
"----",
"destroy todo: add sugar",
"custom disposer 2 for add sugar",
"custom disposer 1 for add sugar",
"destroy store: 2",
"custom disposer for store",
"-----",
"destroy todo: Give talk",
"custom disposer 2 for Give talk",
"custom disposer 1 for Give talk"
])
})
test("lifecycle hooks can access their children", () => {
const events: string[] = []
function listener(e: string) {
events.push(e)
}
const Child = types
.model("Todo", {
title: ""
})
.actions(self => ({
afterCreate() {
listener("new child: " + self.title)
},
afterAttach() {
listener("parent available: " + !!getParent(self))
}
}))
const Parent = types
.model("Parent", {
child: Child
})
.actions(self => ({
afterCreate() {
// **This the key line**: it is trying to access the child
listener("new parent, child.title: " + self.child?.title)
}
}))
const Store = types.model("Store", {
parent: types.maybe(Parent)
})
const store = Store.create({
parent: {
child: { title: "Junior" }
}
})
// As expected no hooks are called.
// The `parent` is not accessed it is just loaded.
events.push("-")
// Simple access does a sensible thing
const parent = store.parent
expect(events).toEqual([
"-",
"new child: Junior",
"new parent, child.title: Junior",
"parent available: true"
])
// Clear the events and make a new store
events.length = 0
const store2 = Store.create({
parent: {
child: { title: "Junior" }
}
})
events.push("-")
// Previously resolvePath would cause problems because the parent hooks
// would be called before the child was fully created
const child = resolvePath(store2, "/parent/child")
expect(events).toEqual([
"-",
"new child: Junior",
"new parent, child.title: Junior",
"parent available: true"
])
})
type CarSnapshot = { id: string }
const Car = types
.model("Car", {
id: types.number
})
.preProcessSnapshot(snapshot =>
Object.assign({}, snapshot, { id: Number(snapshot.id) * 2 })
)
.postProcessSnapshot(snapshot =>
Object.assign({}, snapshot, { id: "" + snapshot.id / 2 })
)
const Factory = types.model("Factory", {
car: Car
})
const Motorcycle = types
.model("Motorcycle", {
id: types.string
})
.preProcessSnapshot(snapshot =>
Object.assign({}, snapshot, { id: snapshot.id.toLowerCase() })
)
.postProcessSnapshot(snapshot =>
Object.assign({}, snapshot, { id: snapshot.id.toUpperCase() })
)
const MotorcycleFactory = types.model("MotorcycleFactory", {
motorcycles: types.array(Motorcycle)
})
test("it should preprocess snapshots when creating", () => {
const car = Car.create({ id: "1" })
expect(car.id).toBe(2)
})
test("it should preprocess snapshots when updating", () => {
const car = Car.create({ id: "1" })
expect(car.id).toBe(2)
applySnapshot(car, { id: "6" })
expect(car.id).toBe(12)
})
test("it should postprocess snapshots when generating snapshot - 1", () => {
const car = Car.create({ id: "1" })
expect(car.id).toBe(2)
expect(getSnapshot(car)).toEqual({ id: "1" })
})
test("it should not apply postprocessor to snapshot on getSnapshot", () => {
const car = Car.create({ id: "1" })
let error = false
onSnapshot(car, snapshot => {
error = true
})
expect(getSnapshot(car)).toEqual({ id: "1" })
// @ts-expect-error - we are testing what happens when we explicitly do not want to apply the postprocessor, expect incorrect types
expect(getSnapshot(car, false)).toEqual({ id: 2 })
expect(error).toBeFalsy()
})
test("it should preprocess snapshots when creating as property type", () => {
const f = Factory.create({ car: { id: "1" } })
expect(f.car.id).toBe(2)
})
test("it should preprocess snapshots when updating", () => {
const f = Factory.create({ car: { id: "1" } })
expect(f.car.id).toBe(2)
applySnapshot(f, { car: { id: "6" } })
expect(f.car.id).toBe(12)
})
test("it should postprocess snapshots when generating snapshot - 2", () => {
const f = Factory.create({ car: { id: "1" } })
expect(f.car.id).toBe(2)
expect(getSnapshot(f)).toEqual({ car: { id: "1" } })
})
test("it should postprocess non-initialized children", () => {
const f = MotorcycleFactory.create({ motorcycles: [{ id: "a" }, { id: "b" }] })
expect(getSnapshot(f)).toEqual({ motorcycles: [{ id: "A" }, { id: "B" }] })
})
test("base hooks can be composed", () => {
const events: string[] = []
function listener(message: string) {
events.push(message)
}
const Todo = types
.model("Todo", { title: "" })
.actions(self => {
function afterCreate() {
listener("aftercreate1")
}
function beforeDestroy() {
listener("beforedestroy1")
}
function afterAttach() {
listener("afterattach1")
}
function beforeDetach() {
listener("beforedetach1")
}
return { afterCreate, beforeDestroy, afterAttach, beforeDetach }
})
.actions(self => {
function afterCreate() {
listener("aftercreate2")
}
function beforeDestroy() {
listener("beforedestroy2")
}
function afterAttach() {
listener("afterattach2")
}
function beforeDetach() {
listener("beforedetach2")
}
return { afterCreate, beforeDestroy, afterAttach, beforeDetach }
})
const Store = types.model("Store", { todos: types.array(Todo) })
const store = Store.create({ todos: [] })
const todo = Todo.create()
unprotect(store)
store.todos.push(todo)
detach(todo)
destroy(todo)
expect(events).toEqual([
"aftercreate1",
"aftercreate2",
"afterattach1",
"afterattach2",
"beforedetach1",
"beforedetach2",
"beforedestroy1",
"beforedestroy2"
])
})
test("snapshot processors can be composed", () => {
const X = types
.model({
x: 1
})
.preProcessSnapshot(s => ({
x: s.x! - 3
}))
.preProcessSnapshot(s => ({
x: s.x! / 5
}))
.postProcessSnapshot(s => {
return { x: s.x + 3 }
})
.postProcessSnapshot(s => {
return { x: s.x * 5 }
})
const x = X.create({ x: 25 })
expect(x.x).toBe(2)
expect(getSnapshot(x).x).toBe(25)
})
test("addDisposer must return the passed disposer", () => {
const listener = jest.fn()
const M = types.model({}).actions(self => {
expect(addDisposer(self, listener)).toBe(listener)
return {}
})
M.create()
})
test("array calls all hooks", () => {
const events: string[] = []
function listener(message: string) {
events.push(message)
}
const Item = types.model("Item", { id: types.string })
const Collection = types.array(Item).hooks(self => ({
afterCreate() {
listener("afterCreate")
},
afterAttach() {
listener("afterAttach")
},
beforeDetach() {
listener("beforeDetach")
},
beforeDestroy() {
listener("beforeDestroy")
}
}))
const Holder = types.model("Holder", { items: types.maybe(Collection) })
const collection = Collection.create([{ id: "1" }, { id: "2" }, { id: "3" }])
expect(events).toStrictEqual(["afterCreate"])
const holder = Holder.create({ items: collection })
unprotect(holder)
expect(events).toStrictEqual(["afterCreate", "afterAttach"])
detach(holder.items!)
expect(events).toStrictEqual(["afterCreate", "afterAttach", "beforeDetach"])
holder.items = collection
expect(events).toStrictEqual(["afterCreate", "afterAttach", "beforeDetach", "afterAttach"])
holder.items = undefined
expect(events).toStrictEqual([
"afterCreate",
"afterAttach",
"beforeDetach",
"afterAttach",
"beforeDestroy"
])
})
test("map calls all hooks", () => {
const events: string[] = []
function listener(message: string) {
events.push(message)
}
const Item = types.model("Item", { id: types.string })
const Collection = types.map(Item).hooks(self => ({
afterCreate() {
listener("afterCreate")
},
afterAttach() {
listener("afterAttach")
},
beforeDetach() {
listener("beforeDetach")
},
beforeDestroy() {
listener("beforeDestroy")
}
}))
const Holder = types.model("Holder", { items: types.maybe(Collection) })
const collection = Collection.create({ "1": { id: "1" }, "2": { id: "2" }, "3": { id: "3" } })
expect(events).toStrictEqual(["afterCreate"])
const holder = Holder.create({ items: cast(collection) })
unprotect(holder)
expect(events).toStrictEqual(["afterCreate", "afterAttach"])
detach(holder.items!)
expect(events).toStrictEqual(["afterCreate", "afterAttach", "beforeDetach"])
holder.items = collection
expect(events).toStrictEqual(["afterCreate", "afterAttach", "beforeDetach", "afterAttach"])
holder.items = undefined
expect(events).toStrictEqual([
"afterCreate",
"afterAttach",
"beforeDetach",
"afterAttach",
"beforeDestroy"
])
})
================================================
FILE: __tests__/core/identifier.test.ts
================================================
import {
types,
tryResolve,
resolvePath,
SnapshotOrInstance,
IAnyModelType,
IAnyComplexType,
resolveIdentifier,
getIdentifier,
detach
} from "../../src"
import { expect, test } from "bun:test"
if (process.env.NODE_ENV !== "production") {
test("#275 - Identifiers should check refinement", () => {
const Model = types
.model("Model", {
id: types.refinement(
"id",
types.string,
identifier => identifier.indexOf("Model_") === 0
)
})
.actions(self => ({
setId(id: string) {
self.id = id
}
}))
const ParentModel = types
.model("ParentModel", {
models: types.array(Model)
})
.actions(self => ({
addModel(model: SnapshotOrInstance) {
self.models.push(model)
}
}))
expect(() => {
ParentModel.create({ models: [{ id: "WrongId_1" }] })
}).toThrow()
expect(() => {
const parentStore = ParentModel.create({ models: [] })
parentStore.addModel({ id: "WrongId_2" })
}).toThrow()
expect(() => {
const model = Model.create({ id: "Model_1" })
model.setId("WrongId_3")
}).toThrow()
expect(() => {
Model.create({ id: "WrongId_4" })
}).toThrow()
})
}
test("#158 - #88 - Identifiers should accept any string character", () => {
const Todo = types.model("Todo", {
id: types.identifier,
title: types.string
})
expect(() => {
;["coffee", "cof$fee", "cof|fee", "cof/fee"].forEach(id => {
Todo.create({
id: id,
title: "Get coffee"
})
})
}).not.toThrow()
})
test("#187 - identifiers should not break late types", () => {
expect(() => {
const MstNode = types.model("MstNode", {
value: types.number,
next: types.maybe(types.late((): IAnyModelType => MstNode))
})
}).not.toThrow()
})
if (process.env.NODE_ENV !== "production") {
test("should throw if multiple identifiers provided", () => {
expect(() => {
const Model = types.model("Model", {
id: types.identifierNumber,
pk: types.identifierNumber
})
Model.create({ id: 1, pk: 2 })
}).toThrow(
`[mobx-state-tree] Cannot define property 'pk' as object identifier, property 'id' is already defined as identifier property`
)
})
test("should throw if identifier of wrong type", () => {
expect(() => {
const Model = types.model("Model", { id: types.identifier })
Model.create({ id: 1 as any as string })
}).toThrow(
'at path "/id" value `1` is not assignable to type: `identifier` (Value is not a valid identifier, expected a string).'
)
})
test("identifier should be used only on model types - no parent provided", () => {
expect(() => {
const Model = types.identifierNumber
Model.create(1)
}).toThrow(
`[mobx-state-tree] Identifier types can only be instantiated as direct child of a model type`
)
})
}
{
const Foo = types.model("Foo", {
id: types.identifier,
name: types.string
})
const Bar = types.model("Bar", {
mimi: types.string,
fooRef: types.reference(Foo)
})
const Root = types.model("Root", {
foos: types.map(Foo),
bar: Bar
})
const root = Root.create({
foos: {},
bar: {
mimi: "mimi",
fooRef: "123"
}
})
test("try resolve doesn't work #686", () => {
expect(tryResolve(root, "/bar/fooRef")).toBe(undefined)
expect(tryResolve(root, "/bar/fooRef/name")).toBe(undefined)
})
test("try resolve shouldn't fail with a complex object node #2071", () => {
const array = types.array(types.number).create([])
const model = types.model().create({})
const map = types.map(types.number).create({})
expect(tryResolve(array, "/boop")).toBeUndefined()
expect(tryResolve(model, "/boop")).toBeUndefined()
expect(tryResolve(map, "/boop")).toBeUndefined()
})
test("failing to resolve throws sane errors", () => {
expect(() => {
resolvePath(root, "/bar/mimi/oopsie")
}).toThrow(
"[mobx-state-tree] Could not resolve 'oopsie' in path '/bar/mimi' while resolving '/bar/mimi/oopsie'"
)
expect(() => {
resolvePath(root, "/zoomba/moomba")
}).toThrow(
"[mobx-state-tree] Could not resolve 'zoomba' in path '/' while resolving '/zoomba/moomba'"
)
expect(() => resolvePath(root, "/bar/fooRef")).toThrow(
"[mobx-state-tree] Failed to resolve reference '123' to type 'Foo' (from node: /bar/fooRef)"
)
expect(() => resolvePath(root, "/bar/fooRef/name")).toThrow(
"[mobx-state-tree] Failed to resolve reference '123' to type 'Foo' (from node: /bar/fooRef)"
)
})
}
test("it can resolve through references", () => {
const Folder = types.model("Folder", {
type: types.literal("folder"),
name: types.identifier,
children: types.array(types.late((): IAnyComplexType => types.union(Folder, SymLink)))
})
const SymLink = types.model({
type: types.literal("link"),
target: types.reference(Folder)
})
const root = Folder.create({
type: "folder",
name: "root",
children: [
{
type: "folder",
name: "a",
children: []
},
{
type: "folder",
name: "b",
children: [
{
type: "folder",
name: "c",
children: []
}
]
},
{
type: "link",
target: "b"
},
{
type: "link",
target: "e"
}
]
})
expect(resolvePath(root, "/children/1/children/0").name).toBe("c")
expect(resolvePath(root, "/children/2/target/children/0").name).toBe("c")
expect(resolvePath(root, "/children/2/target/children/../children/./0").name).toBe("c")
expect(() => resolvePath(root, "/children/3/target/children/0").name).toThrow(
"[mobx-state-tree] Failed to resolve reference 'e' to type 'Folder' (from node: /children/3/target)"
)
})
test("#1019", () => {
let calls = 0
function randomUuid() {
calls++
let pattern = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
return pattern.replace(/[xy]/g, function (cc) {
const r = (Math.random() * 16) | 0,
v = cc === "x" ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
const TOptionalId = types.optional(
types.refinement(types.identifier, identifier =>
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/.test(identifier)
),
randomUuid
)
const CommentModel = types.model("CommentModel", {
uid: TOptionalId
})
const CommentStore = types
.model("CommentStore", {
items: types.array(CommentModel)
})
.actions(self => ({
test1() {
expect(calls).toBe(0)
const comment = CommentModel.create({})
expect(calls).toBe(1)
self.items.push(comment)
const item = resolveIdentifier(CommentModel, self.items, comment.uid)
const item2 = self.items.find(i => i.uid === comment.uid)
expect(item).toBe(item2!)
}
}))
const c = CommentStore.create({})
c.test1()
})
test("#1019-2", () => {
const Item = types.model({
id: types.optional(types.identifier, "dummykey")
})
expect(getIdentifier(Item.create())).toBe("dummykey")
expect(Item.create().id).toBe("dummykey")
})
test("identifierAttribute of the type", () => {
const M1 = types.model({})
expect(M1.identifierAttribute).toBeUndefined()
const M2 = types.model({ myId: types.identifier })
expect(M2.identifierAttribute).toBe("myId")
const M3 = types.model({ myId: types.optional(types.identifier, () => "hi") })
expect(M3.identifierAttribute).toBe("myId")
})
test("items detached from arrays don't corrupt identifierCache", () => {
const Item = types.model("Item", { id: types.identifier })
const ItemArray = types.model("ItemArray", { items: types.array(Item) }).actions(self => ({
removeSecondItemWithDetach() {
detach(self.items[1])
}
}))
const smallArray = ItemArray.create({
items: [{ id: "A" }, { id: "B" }, { id: "C" }, { id: "D" }, { id: "E" }]
})
expect(resolveIdentifier(Item, smallArray, "E")).toBeDefined()
smallArray.removeSecondItemWithDetach()
expect(smallArray.items.length).toBe(4)
expect(resolveIdentifier(Item, smallArray, "B")).toBeUndefined()
expect(resolveIdentifier(Item, smallArray, "E")).toBeDefined()
const largeArray = ItemArray.create({
items: [
{ id: "A" },
{ id: "B" },
{ id: "C" },
{ id: "D" },
{ id: "E" },
{ id: "F" },
{ id: "G" },
{ id: "H" },
{ id: "I" },
{ id: "J" },
{ id: "K" }
]
})
expect(resolveIdentifier(Item, largeArray, "K")).toBeDefined()
largeArray.removeSecondItemWithDetach()
expect(largeArray.items.length).toBe(10)
expect(resolveIdentifier(Item, largeArray, "B")).toBeUndefined()
expect(resolveIdentifier(Item, largeArray, "J")).toBeDefined()
// The following expectation was failing in version 5.1.8 and earlier
expect(resolveIdentifier(Item, largeArray, "K")).toBeDefined()
})
================================================
FILE: __tests__/core/jsonpatch.test.ts
================================================
import {
getSnapshot,
unprotect,
recordPatches,
types,
IType,
IJsonPatch,
Instance,
cast,
IAnyModelType,
IMSTMap,
escapeJsonPath,
getPath,
resolvePath,
splitJsonPath,
joinJsonPath
} from "../../src"
import { expect, test } from "bun:test"
function testPatches(
type: IType,
snapshot: C,
fn: any,
expectedPatches: IJsonPatch[]
) {
const instance = type.create(snapshot)
const baseSnapshot = getSnapshot(instance)
const recorder = recordPatches(instance)
unprotect(instance)
fn(instance)
recorder.stop()
expect(recorder.patches).toEqual(expectedPatches)
const clone = type.create(snapshot)
recorder.replay(clone)
expect(getSnapshot(clone)).toEqual(getSnapshot(instance))
recorder.undo()
expect(getSnapshot(instance)).toEqual(baseSnapshot)
}
const Node = types.model("Node", {
id: types.identifierNumber,
text: "Hi",
children: types.optional(types.array(types.late((): IAnyModelType => Node)), [])
})
test("it should apply simple patch", () => {
testPatches(
Node,
{ id: 1 },
(n: Instance) => {
n.text = "test"
},
[
{
op: "replace",
path: "/text",
value: "test"
}
]
)
})
test("it should apply deep patches to arrays", () => {
testPatches(
Node,
{ id: 1, children: [{ id: 2 }] },
(n: Instance) => {
const children = n.children as unknown as Instance[]
children[0].text = "test" // update
children[0] = cast({ id: 2, text: "world" }) // this reconciles; just an update
children[0] = cast({ id: 4, text: "coffee" }) // new object
children[1] = cast({ id: 3, text: "world" }) // addition
children.splice(0, 1) // removal
},
[
{
op: "replace",
path: "/children/0/text",
value: "test"
},
{
op: "replace",
path: "/children/0/text",
value: "world"
},
{
op: "replace",
path: "/children/0",
value: {
id: 4,
text: "coffee",
children: []
}
},
{
op: "add",
path: "/children/1",
value: {
id: 3,
text: "world",
children: []
}
},
{
op: "remove",
path: "/children/0"
}
]
)
})
test("it should apply deep patches to arrays with object instances", () => {
testPatches(
Node,
{ id: 1, children: [{ id: 2 }] },
(n: Instance) => {
const children = n.children as unknown as Instance[]
children[0].text = "test" // update
children[0] = Node.create({ id: 2, text: "world" }) // this does not reconcile, new instance is provided
children[0] = Node.create({ id: 4, text: "coffee" }) // new object
},
[
{
op: "replace",
path: "/children/0/text",
value: "test"
},
{
op: "replace",
path: "/children/0",
value: {
id: 2,
text: "world",
children: []
}
},
{
op: "replace",
path: "/children/0",
value: {
id: 4,
text: "coffee",
children: []
}
}
]
)
})
test("it should apply non flat patches", () => {
testPatches(
Node,
{ id: 1 },
(n: Instance) => {
const children = n.children as unknown as Instance[]
children.push(
cast({
id: 2,
children: [{ id: 4 }, { id: 5, text: "Tea" }]
})
)
},
[
{
op: "add",
path: "/children/0",
value: {
id: 2,
text: "Hi",
children: [
{
id: 4,
text: "Hi",
children: []
},
{
id: 5,
text: "Tea",
children: []
}
]
}
}
]
)
})
test("it should apply non flat patches with object instances", () => {
testPatches(
Node,
{ id: 1 },
(n: Instance) => {
const children = n.children as unknown as Instance[]
children.push(
Node.create({
id: 2,
children: [{ id: 5, text: "Tea" }]
})
)
},
[
{
op: "add",
path: "/children/0",
value: {
id: 2,
text: "Hi",
children: [
{
id: 5,
text: "Tea",
children: []
}
]
}
}
]
)
})
test("it should apply deep patches to maps", () => {
// If user does not transpile const/let to var, trying to call Late' subType
// property getter during map's tryCollectModelTypes() will throw ReferenceError.
// But if it's transpiled to var, then subType will become 'undefined'.
const NodeMap = types.model("NodeMap", {
id: types.identifierNumber,
text: "Hi",
children: types.optional(types.map(types.late((): IAnyModelType => NodeMap)), {})
})
testPatches(
NodeMap,
{ id: 1, children: { 2: { id: 2 } } },
(n: Instance) => {
const children = n.children as IMSTMap
children.get("2")!.text = "test" // update
children.put({ id: 2, text: "world" }) // this reconciles; just an update
children.set(
"4",
NodeMap.create({ id: 4, text: "coffee", children: { 23: { id: 23 } } })
) // new object
children.put({ id: 3, text: "world", children: { 7: { id: 7 } } }) // addition
children.delete("2") // removal
},
[
{
op: "replace",
path: "/children/2/text",
value: "test"
},
{
op: "replace",
path: "/children/2/text",
value: "world"
},
{
op: "add",
path: "/children/4",
value: {
children: {
23: {
children: {},
id: 23,
text: "Hi"
}
},
id: 4,
text: "coffee"
}
},
{
op: "add",
path: "/children/3",
value: {
children: {
7: {
children: {},
id: 7,
text: "Hi"
}
},
id: 3,
text: "world"
}
},
{
op: "remove",
path: "/children/2"
}
]
)
})
test("it should apply deep patches to objects", () => {
const NodeObject = types.model("NodeObject", {
id: types.identifierNumber,
text: "Hi",
child: types.maybe(types.late((): IAnyModelType => NodeObject))
})
testPatches(
NodeObject,
{ id: 1, child: { id: 2 } },
(n: Instance) => {
n.child!.text = "test" // update
n.child = cast({ id: 2, text: "world" }) // this reconciles; just an update
n.child = NodeObject.create({ id: 2, text: "coffee", child: { id: 23 } })
n.child = cast({ id: 3, text: "world", child: { id: 7 } }) // addition
n.child = undefined // removal
},
[
{
op: "replace",
path: "/child/text",
value: "test"
},
{
op: "replace",
path: "/child/text",
value: "world"
},
{
op: "replace",
path: "/child",
value: {
child: {
child: undefined,
id: 23,
text: "Hi"
},
id: 2,
text: "coffee"
}
},
{
op: "replace",
path: "/child",
value: {
child: {
child: undefined,
id: 7,
text: "Hi"
},
id: 3,
text: "world"
}
},
{
op: "replace",
path: "/child",
value: undefined
}
]
)
})
test("it should correctly split/join json patches", () => {
function isValid(str: string, array: string[], altStr?: string) {
expect(splitJsonPath(str)).toEqual(array)
expect(joinJsonPath(array)).toBe(altStr !== undefined ? altStr : str)
}
isValid("", [])
isValid("/", [""])
isValid("//", ["", ""])
isValid("/a", ["a"])
isValid("/a/", ["a", ""])
isValid("/a//", ["a", "", ""])
isValid(".", ["."])
isValid("..", [".."])
isValid("./a", [".", "a"])
isValid("../a", ["..", "a"])
isValid("/.a", [".a"])
isValid("/..a", ["..a"])
// rooted relatives are equivalent to plain relatives
isValid("/.", ["."], ".")
isValid("/..", [".."], "..")
isValid("/./a", [".", "a"], "./a")
isValid("/../a", ["..", "a"], "../a")
function isInvalid(str: string) {
expect(() => {
splitJsonPath(str)
}).toThrow("a json path must be either rooted, empty or relative")
}
isInvalid("a")
isInvalid("a/")
isInvalid("a//")
isInvalid(".a")
isInvalid(".a/")
isInvalid("..a")
isInvalid("..a/")
})
test("it should correctly escape/unescape json patches", () => {
expect(escapeJsonPath("http://example.com")).toBe("http:~1~1example.com")
const AppStore = types.model({
items: types.map(types.frozen())
})
testPatches(
AppStore,
{ items: {} },
(store: typeof AppStore.Type) => {
store.items.set("with/slash~tilde", 1)
},
[{ op: "add", path: "/items/with~1slash~0tilde", value: 1 }]
)
})
test("weird keys are handled correctly", () => {
const Store = types.model({
map: types.map(
types.model({
model: types.model({
value: types.string
})
})
)
})
const store = Store.create({
map: {
"": { model: { value: "val1" } },
"/": { model: { value: "val2" } },
"~": { model: { value: "val3" } }
}
})
{
const target = store.map.get("")!.model
const path = getPath(target)
expect(path).toBe("/map//model")
expect(resolvePath(store, path)).toBe(target)
}
{
const target = store.map.get("/")!.model
const path = getPath(target)
expect(path).toBe("/map/~1/model")
expect(resolvePath(store, path)).toBe(target)
}
{
const target = store.map.get("~")!.model
const path = getPath(target)
expect(path).toBe("/map/~0/model")
expect(resolvePath(store, path)).toBe(target)
}
})
test("relativePath with a different base than the root works correctly", () => {
const Store = types.model({
map: types.map(
types.model({
model: types.model({
value: types.string
})
})
)
})
const store = Store.create({
map: {
"1": { model: { value: "val1" } },
"2": { model: { value: "val2" } }
}
})
{
const target = store.map.get("1")!.model
expect(resolvePath(store.map, "./1/model")).toBe(target)
expect(resolvePath(store.map, "../map/1/model")).toBe(target)
// rooted relative should resolve to the given base as root
expect(resolvePath(store.map, "/./1/model")).toBe(target)
expect(resolvePath(store.map, "/../map/1/model")).toBe(target)
}
{
const target = store.map.get("2")!.model
expect(resolvePath(store.map, "./2/model")).toBe(target)
expect(resolvePath(store.map, "../map/2/model")).toBe(target)
// rooted relative should resolve to the given base as root
expect(resolvePath(store.map, "/./2/model")).toBe(target)
expect(resolvePath(store.map, "/../map/2/model")).toBe(target)
}
})
test("it should emit one patch for array clear", () => {
testPatches(
Node,
{ id: 1, children: [{ id: 2 }, { id: 3 }] },
(n: Instance) => {
n.children.clear()
},
[
{
op: "replace",
path: "/children",
value: []
}
]
)
})
test("it should emit one patch for array replace", () => {
testPatches(
Node,
{ id: 1, children: [{ id: 2 }, { id: 3 }] },
(n: Instance) => {
n.children.replace([{ id: 4 }, { id: 5 }])
},
[
{
op: "replace",
path: "/children",
value: [
{
id: 4,
text: "Hi",
children: []
},
{
id: 5,
text: "Hi",
children: []
}
]
}
]
)
})
================================================
FILE: __tests__/core/late.test.ts
================================================
import { types, typecheck, IAnyModelType } from "../../src"
import { expect, spyOn, test } from "bun:test"
if (process.env.NODE_ENV !== "production") {
test("it should throw if late doesnt received a function as parameter", () => {
expect(() => {
types.model({
after: types.late(1 as any)
})
}).toThrow()
})
}
test("it should accept a type and infer it correctly", () => {
const Before = types.model({
after: types.late(() => After)
})
const After = types.model({
name: types.maybe(types.string)
})
expect(() => Before.create({ after: { name: "Hello, it's me." } })).not.toThrow()
})
test("late should allow circular references", () => {
// TypeScript isn't smart enough to infer self referencing types.
const Node = types.model({
childs: types.optional(types.array(types.late((): IAnyModelType => Node)), [])
})
expect(() => Node.create()).not.toThrow()
expect(() => Node.create({ childs: [{}, { childs: [] }] })).not.toThrow()
})
test("late should describe correctly circular references", () => {
// TypeScript isn't smart enough to infer self referencing types.
const Node = types.model("Node", {
childs: types.array(types.late((): IAnyModelType => Node))
})
expect(Node.describe()).toEqual("{ childs: late(() => Node)[]? }")
})
test("should typecheck", () => {
const NodeObject = types.model("NodeObject", {
id: types.identifierNumber,
text: "Hi",
child: types.maybe(types.late((): IAnyModelType => NodeObject))
})
const x = NodeObject.create({ id: 1 })
try {
;(x as any).child = 3
;(x as any).floepie = 3
} catch (e) {
// ignore, this is about TS
}
})
test("typecheck should throw an Error when called at runtime, but not log the error", () => {
const consoleSpy = spyOn(console, "error")
const NodeObject = types.model("NodeObject", {
id: types.identifierNumber,
text: types.string
})
expect(() => {
typecheck(NodeObject, { id: 1, text: 1 } as any)
}).toThrow()
try {
typecheck(NodeObject, { id: 1, text: 1 } as any)
} catch (error) {
expect(error).toBeDefined()
expect(consoleSpy).not.toHaveBeenCalled()
}
})
test("#825, late type checking ", () => {
const Product = types.model({
details: types.late(() => types.optional(Details, {}))
})
const Details = types.model({
name: types.maybe(types.string)
})
const p2 = Product.create({})
const p = Product.create({ details: { name: "bla" } })
})
test("#916 - 0", () => {
const Todo = types.model("Todo", {
title: types.string,
newTodo: types.optional(
types.late((): IAnyModelType => Todo),
{}
) // N.B. this definition is never instantiateable!
})
})
test("#916 - 1", () => {
const Todo = types.model("Todo", {
title: types.string,
newTodo: types.maybe(types.late((): IAnyModelType => Todo))
})
const t = Todo.create({
title: "Get Coffee"
})
})
test("#916 - 2", () => {
const Todo = types.model("Todo", {
title: types.string,
newTodo: types.maybe(types.late((): IAnyModelType => Todo))
})
expect(
Todo.is({
title: "A",
newTodo: { title: " test" }
})
).toBe(true)
expect(
Todo.is({
title: "A",
newTodo: { title: 7 }
})
).toBe(false)
})
test("#916 - 3", () => {
const Todo = types.model("Todo", {
title: types.string,
newTodo: types.maybe(types.late((): IAnyModelType => Todo))
})
const t = Todo.create({
title: "Get Coffee",
newTodo: { title: "test" }
})
expect(t.newTodo!.title).toBe("test")
})
================================================
FILE: __tests__/core/lazy.test.ts
================================================
import { when } from "mobx"
import { getRoot, types } from "../../src"
import { expect, test } from "bun:test"
interface IRootModel {
shouldLoad: boolean
}
test("it should load the correct type", async () => {
const LazyModel = types
.model("LazyModel", {
width: types.number,
height: types.number
})
.views(self => ({
get area() {
return self.height * self.width
}
}))
const Root = types
.model("Root", {
shouldLoad: types.optional(types.boolean, false),
lazyModel: types.lazy("lazy", {
loadType: () => Promise.resolve(LazyModel),
shouldLoadPredicate: parent => parent.shouldLoad == true
})
})
.actions(self => ({
load: () => {
self.shouldLoad = true
}
}))
const store = Root.create({
lazyModel: {
width: 3,
height: 2
}
})
expect(store.lazyModel.width).toBe(3)
expect(store.lazyModel.height).toBe(2)
expect(store.lazyModel.area).toBeUndefined()
store.load()
await when(() => store.lazyModel && store.lazyModel.area !== undefined, { timeout: 2000 })
await expect(store.lazyModel.area).toBe(6)
})
test("maintains the tree structure when loaded", async () => {
const LazyModel = types
.model("LazyModel", {
width: types.number,
height: types.number
})
.views(self => ({
get area() {
const root = getRoot<{ rootValue: number }>(self)
return self.height * self.width * root.rootValue
}
}))
const Root = types
.model("Root", {
shouldLoad: types.optional(types.boolean, false),
lazyModel: types.lazy("lazy", {
loadType: () => Promise.resolve(LazyModel),
shouldLoadPredicate: parent => parent.shouldLoad == true
})
})
.views(() => ({
get rootValue() {
return 5
}
}))
.actions(self => ({
load: () => {
self.shouldLoad = true
}
}))
const store = Root.create({
lazyModel: {
width: 3,
height: 2
}
})
expect(store.lazyModel.width).toBe(3)
expect(store.lazyModel.height).toBe(2)
expect(store.rootValue).toEqual(5)
expect(store.lazyModel.area).toBeUndefined()
store.load()
const promise = new Promise((resolve, reject) => {
when(
() => store.lazyModel && store.lazyModel.area !== undefined,
() => resolve(store.lazyModel.area)
)
setTimeout(reject, 2000)
})
await expect(promise).resolves.toBe(30)
})
================================================
FILE: __tests__/core/literal.test.ts
================================================
import { types } from "../../src"
import { expect, test } from "bun:test"
if (process.env.NODE_ENV !== "production") {
test("it should allow only primitives", () => {
const error = expect(() => {
types.model({
complexArg: types.literal({ a: 1 } as any)
})
}).toThrow("expected primitive as argument")
})
test("it should fail if not optional and no default provided", () => {
const Factory = types.literal("hello")
expect(() => {
;(Factory.create as any)()
}).toThrow(/is not assignable to type/)
})
test("it should throw if a different type is given", () => {
const Factory = types.model("TestFactory", {
shouldBeOne: types.literal(1)
})
expect(() => {
Factory.create({ shouldBeOne: 2 as any })
}).toThrow(/is not assignable to type/)
})
}
test("it should support null type", () => {
const M = types.model({
nullish: types.null
})
expect(
M.is({
nullish: null
})
).toBe(true)
expect(M.is({ nullish: undefined })).toBe(false)
expect(M.is({ nullish: 17 })).toBe(false)
})
test("it should support undefined type", () => {
const M = types.model({
undefinedish: types.undefined
})
expect(
M.is({
undefinedish: undefined
})
).toBe(true)
expect(M.is({})).toBe(true) // MWE: disputable, should be false?
expect(M.is({ undefinedish: null })).toBe(false)
expect(M.is({ undefinedish: 17 })).toBe(false)
})
================================================
FILE: __tests__/core/map.test.ts
================================================
import { configure } from "mobx"
import {
onSnapshot,
onPatch,
applyPatch,
applySnapshot,
getSnapshot,
types,
unprotect,
isStateTreeNode,
SnapshotOut,
IJsonPatch,
IAnyModelType,
detach
} from "../../src"
import { describe, expect, it, test } from "bun:test"
const createTestFactories = () => {
const ItemFactory = types.model({
to: "world"
})
const Factory = types.map(ItemFactory)
const PrimitiveMapFactory = types.model({
boolean: types.map(types.boolean),
string: types.map(types.string),
number: types.map(types.number)
})
return { Factory, ItemFactory, PrimitiveMapFactory }
}
// === FACTORY TESTS ===
test("it should create a factory", () => {
const { Factory } = createTestFactories()
const snapshot = getSnapshot(Factory.create())
expect(snapshot).toEqual({})
})
test("it should succeed if not optional and no default provided", () => {
const Factory = types.map(types.string)
expect(Factory.create().toJSON()).toEqual({})
})
test("it should restore the state from the snapshot", () => {
const { Factory } = createTestFactories()
const instance = Factory.create({ hello: { to: "world" } })
expect(getSnapshot(instance)).toEqual({ hello: { to: "world" } })
expect(("" + instance).replace(/@\d+/, "@xx")).toBe("[object ObservableMap]") // default toString
})
// === SNAPSHOT TESTS ===
test("it should emit snapshots", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
let snapshots: SnapshotOut[] = []
onSnapshot(doc, snapshot => snapshots.push(snapshot))
doc.set("hello", ItemFactory.create())
expect(snapshots).toEqual([{ hello: { to: "world" } }])
})
test("it should apply snapshots", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
applySnapshot(doc, { hello: { to: "universe" } })
expect(getSnapshot(doc)).toEqual({ hello: { to: "universe" } })
})
test("it should return a snapshot", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
doc.set("hello", ItemFactory.create())
expect(getSnapshot(doc)).toEqual({ hello: { to: "world" } })
})
test("it should be the same each time", () => {
const { PrimitiveMapFactory } = createTestFactories()
const data = {
string: { a: "a", b: "" },
boolean: { a: true, b: false },
number: { a: 0, b: 42, c: NaN }
}
const doc = PrimitiveMapFactory.create(data)
expect(getSnapshot(doc)).toEqual(data)
applySnapshot(doc, data)
expect(getSnapshot(doc)).toEqual(data)
applySnapshot(doc, data)
expect(getSnapshot(doc)).toEqual(data)
})
// === PATCHES TESTS ===
test("it should emit add patches", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
let patches: IJsonPatch[] = []
onPatch(doc, patch => patches.push(patch))
doc.set("hello", ItemFactory.create({ to: "universe" }))
expect(patches).toEqual([{ op: "add", path: "/hello", value: { to: "universe" } }])
})
test("it should apply an add patch", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
applyPatch(doc, { op: "add", path: "/hello", value: { to: "universe" } })
expect(getSnapshot(doc)).toEqual({ hello: { to: "universe" } })
})
test("it should emit update patches", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
doc.set("hello", ItemFactory.create())
let patches: IJsonPatch[] = []
onPatch(doc, patch => patches.push(patch))
doc.set("hello", ItemFactory.create({ to: "universe" }))
expect(patches).toEqual([{ op: "replace", path: "/hello", value: { to: "universe" } }])
})
test("it should apply an update patch", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
applyPatch(doc, { op: "replace", path: "/hello", value: { to: "universe" } })
expect(getSnapshot(doc)).toEqual({ hello: { to: "universe" } })
})
test("it should emit remove patches", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
doc.set("hello", ItemFactory.create())
let patches: IJsonPatch[] = []
onPatch(doc, patch => patches.push(patch))
doc.delete("hello")
expect(patches).toEqual([{ op: "remove", path: "/hello" }])
})
test("it should apply a remove patch", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
doc.set("hello", ItemFactory.create())
applyPatch(doc, { op: "remove", path: "/hello" })
expect(getSnapshot(doc)).toEqual({})
})
test("it should apply patches", () => {
const { Factory, ItemFactory } = createTestFactories()
const doc = Factory.create()
applyPatch(doc, [
{ op: "add", path: "/hello", value: { to: "mars" } },
{ op: "replace", path: "/hello", value: { to: "universe" } }
])
expect(getSnapshot(doc)).toEqual({ hello: { to: "universe" } })
})
// === TYPE CHECKS ===
test("it should check the type correctly", () => {
const { Factory } = createTestFactories()
const doc = Factory.create()
expect(Factory.is(doc)).toEqual(true)
expect(Factory.is([])).toEqual(false)
expect(Factory.is({})).toEqual(true)
expect(Factory.is({ hello: { to: "mars" } })).toEqual(true)
expect(Factory.is({ hello: { wrongKey: true } })).toEqual(true)
expect(Factory.is({ hello: { to: true } })).toEqual(false)
})
test("it should support identifiers", () => {
configure({
useProxies: "never"
})
const Store = types.model({
todos: types.optional(
types.map(
types.model({
id: types.identifier
})
),
{}
)
})
const store = Store.create()
unprotect(store)
store.todos.set("17", { id: "17" })
const a = store.todos.get("17")
applySnapshot(store.todos, { "16": { id: "16" }, "17": { id: "17" } })
expect(a === store.todos.get("17")).toBe(true) // same instance still
expect(store.todos.get("17")!.id).toBe("17")
store.todos.put({ id: "19" })
expect(store.todos.get("19")!.id).toBe("19")
expect("" + store.todos.get("19")).toBe("AnonymousModel@/todos/19(id: 19)")
if (process.env.NODE_ENV !== "production") {
expect(() => applySnapshot(store.todos, { "17": { id: "18" } })).toThrow(
"[mobx-state-tree] A map of objects containing an identifier should always store the object under their own identifier. Trying to store key '18', but expected: '17'"
)
}
})
test("#184 - types.map().get(key) should not throw if key doesnt exists", () => {
const { Factory } = createTestFactories()
const doc = Factory.create({
hello: {
to: "world"
}
})
expect(() => {
doc.get("notexistingkey")
}).not.toThrow()
})
test("#192 - put should not throw when identifier is a number", () => {
const Todo = types.model("Todo", { todo_id: types.identifierNumber, title: types.string })
const TodoStore = types
.model("TodoStore", {
todos: types.optional(types.map(Todo), {})
})
.actions(self => {
function addTodo(todo: typeof Todo.Type | typeof Todo.CreationType) {
self.todos.put(todo)
}
return {
addTodo
}
})
const todoStore = TodoStore.create({})
expect(() => {
todoStore.addTodo({
todo_id: 1,
title: "Test"
})
}).not.toThrow()
if (process.env.NODE_ENV !== "production") {
expect(() => {
todoStore.addTodo({ todo_id: "1", title: "Test" } as any)
}).toThrow(
'at path "/todo_id" value `"1"` is not assignable to type: `identifierNumber` (Value is not a valid identifierNumber, expected a number)'
)
}
})
test("#192 - map should not mess up keys when putting twice", () => {
const Todo = types.model("Todo", { todo_id: types.identifierNumber, title: types.string })
const TodoStore = types
.model("TodoStore", {
todos: types.optional(types.map(Todo), {})
})
.actions(self => {
function addTodo(todo: typeof Todo.Type | typeof Todo.CreationType) {
self.todos.put(todo)
}
return {
addTodo
}
})
const todoStore = TodoStore.create({})
todoStore.addTodo({
todo_id: 1,
title: "Test"
})
expect(getSnapshot(todoStore.todos)).toEqual({ "1": { todo_id: 1, title: "Test" } })
todoStore.addTodo({
todo_id: 1,
title: "Test Edited"
})
expect(getSnapshot(todoStore.todos)).toEqual({ "1": { todo_id: 1, title: "Test Edited" } })
})
test("#694 - map.put should return new node", () => {
configure({
useProxies: "never"
})
const Todo = types.model("Todo", {
todo_id: types.identifier,
title: types.string
})
const TodoStore = types
.model("TodoStore", {
todos: types.map(Todo)
})
.actions(self => {
function addAndReturnTodo(todo: typeof Todo.Type | typeof Todo.CreationType) {
return self.todos.put(todo)
}
return {
addAndReturnTodo
}
})
const todoStore = TodoStore.create({ todos: {} })
const addedTodo = todoStore.addAndReturnTodo(
Todo.create({
todo_id: "1",
title: "Test 1"
})
)
expect(isStateTreeNode(addedTodo)).toEqual(true)
expect(getSnapshot(addedTodo)).toEqual({ todo_id: "1", title: "Test 1" })
const editedTodo = todoStore.addAndReturnTodo({
todo_id: "1",
title: "Test 1 Edited"
})
expect(isStateTreeNode(editedTodo)).toEqual(true)
expect(getSnapshot(editedTodo)).toEqual({ todo_id: "1", title: "Test 1 Edited" })
expect(editedTodo).toEqual(addedTodo)
const addedTodo2 = todoStore.addAndReturnTodo({
todo_id: "2",
title: "Test 2"
})
expect(isStateTreeNode(addedTodo2)).toEqual(true)
expect(getSnapshot(addedTodo2)).toEqual({ todo_id: "2", title: "Test 2" })
})
test("it should not throw when removing a non existing item from a map", () => {
expect(() => {
const AppModel = types
.model({
myMap: types.map(types.number)
})
.actions(self => {
function something() {
return self.myMap.delete("1020")
}
return {
something
}
})
const store = AppModel.create()
expect(store.something()).toBe(false)
}).not.toThrow()
})
test("it should get map keys from reversePatch when deleted an item from a nested map", () => {
const AppModel = types
.model({
value: types.map(types.map(types.map(types.number)))
})
.actions(self => ({
remove(k: string) {
self.value.delete(k)
}
}))
const store = AppModel.create({ value: { a: { b: { c: 10 } } } })
onPatch(store, (patch, reversePatch) => {
expect(patch).toEqual({ op: "remove", path: "/value/a" })
expect(reversePatch).toEqual({ op: "add", path: "/value/a", value: { b: { c: 10 } } })
})
store.remove("a")
})
test("map expects regular identifiers", () => {
const A = types.model("A", { a: types.identifier })
const B = types.model("B", { b: types.identifier })
// NOTE: we can determine identifier attribute upfront, so no need to wait for error while craetion
expect(() => types.map(types.union(A, B))).toThrow(
`[mobx-state-tree] The objects in a map should all have the same identifier attribute, expected 'a', but child of type 'B' declared attribute 'b' as identifier`
)
})
test("issue #876 - map.put works fine for models with preProcessSnapshot", () => {
const Note = types.model("Item", {
text: types.string
})
const Item = types
.model("Item", {
id: types.identifier,
title: types.string,
notes: types.array(Note)
})
.preProcessSnapshot(snapshot => {
const result = Object.assign({}, snapshot)
if (typeof result.title !== "string") result.title = ""
return result
})
const Store = types
.model("Store", {
items: types.optional(types.map(Item), {})
})
.actions(self => ({
afterCreate() {
self.items.put({
id: "1",
title: "",
notes: [{ text: "first note" }, { text: "second note" }]
})
}
}))
let store!: typeof Store.Type
expect(() => {
store = Store.create({})
}).not.toThrow()
expect(getSnapshot(store)).toEqual({
items: {
"1": {
id: "1",
notes: [{ text: "first note" }, { text: "second note" }],
title: ""
}
}
})
})
test("map can resolve late identifiers", () => {
const Late = types.model({
id: types.identifier,
children: types.map(types.late((): IAnyModelType => Late))
})
const snapshot = {
id: "1",
children: {
"2": {
id: "2",
children: {}
}
}
}
expect(() => Late.create(snapshot)).not.toThrow()
})
test("get should return value when key is a number", () => {
const Todo = types.model("Todo", {
todo_id: types.identifierNumber,
title: types.string
})
const TodoStore = types
.model("TodoStore", {
todos: types.optional(types.map(Todo), {})
})
.actions(self => {
function addTodo(aTodo: typeof Todo.Type | typeof Todo.CreationType) {
self.todos.put(aTodo)
}
return {
addTodo
}
})
const todoStore = TodoStore.create({})
const todo = {
todo_id: 1,
title: "Test"
}
todoStore.addTodo(todo)
expect(todoStore.todos.get(1 as any as string)!.title).toEqual("Test")
})
test("numeric keys should work", () => {
const M = types.model({
id: types.identifier,
title: "test"
})
const S = types.model({
mies: types.map(M),
ref: types.maybe(types.reference(M))
})
const s = S.create({
mies: {}
})
unprotect(s)
s.mies.set(7, { id: "7" })
const i7 = s.mies.get(7)!
expect(i7.title).toBe("test")
expect(s.mies.has("7")).toBeTruthy()
expect(s.mies.has(7 as any as string)).toBeTruthy()
expect(s.mies.get("7")).toBeTruthy()
expect(s.mies.get(7 as any as string)).toBeTruthy()
s.mies.set("8", { id: "8" })
expect(s.mies.has("8")).toBeTruthy()
expect(s.mies.has(8 as any as string)).toBeTruthy()
expect(s.mies.get("8")).toBeTruthy()
expect(s.mies.get(8 as any as string)).toBeTruthy()
expect(Array.from(s.mies.keys())).toEqual(["7", "8"])
s.mies.put({ id: "7", title: "coffee" })
expect(s.mies.size).toBe(2)
expect(s.mies.has("7")).toBeTruthy()
expect(s.mies.has(7 as any as string)).toBeTruthy()
expect(s.mies.get("7")).toBeTruthy()
expect(s.mies.get(7 as any as string)).toBeTruthy()
expect(i7.title).toBe("coffee")
expect(s.mies.delete(8 as any as string)).toBeTruthy()
expect(s.mies.size).toBe(1)
})
describe("#826, adding stuff twice", () => {
const Store = types
.model({
map: types.optional(types.map(types.boolean), {})
})
.actions(self => ({
toogleMap: (id: string) => {
self.map.set(id, !self.map.get(id))
}
}))
// This one pass fine 👍
test("Toogling once shouldn't throw", () => {
const store = Store.create({})
expect(() => {
store.toogleMap("1")
}).not.toThrow()
})
// This one throws with 'Not a child 1' error 👎
test("Toogling twice shouldn't throw", () => {
const store = Store.create({})
expect(() => {
store.toogleMap("1")
store.toogleMap("1")
}).not.toThrow()
})
})
test("#751 restore from snapshot should work", () => {
const Submodel = types.model("Submodel", {
id: types.identifierNumber
})
const Model = types.model("Model", {
map: types.map(Submodel)
})
const server = Model.create({ map: {} })
// We add an item with a number id
unprotect(server)
server.map.set(1 as any as string, { id: 1 })
// We can access it using a number
expect(server.map.get(1 as any as string)!.id).toBe(1)
// But if we get a snapshot...
const snapshot = getSnapshot(server)
// And apply it back...
const browser = Model.create(snapshot)
// We can access it using a string
expect(server.map.get("1")!.id).toBe(1)
// And as number
expect(server.map.get(1 as any as string)!.id).toBe(1)
expect(server.map.size).toBe(1)
})
test("#1173 - detaching a map should not eliminate its children", () => {
const M = types.model({})
const AM = types.map(M)
const Store = types.model({ items: AM })
const s = Store.create({ items: { x: {}, y: {}, z: {} } })
const n0 = s.items.get("x")
unprotect(s)
const detachedItems = detach(s.items)
expect(s.items).not.toBe(detachedItems)
expect(s.items.size).toBe(0)
expect(detachedItems.size).toBe(3)
expect(detachedItems.get("x")).toBe(n0!)
})
test("#1131 - put with optional identifier", () => {
const Test = types.model({
id: types.optional(types.identifier, () => Math.random().toString(36).substr(2)),
value: "hi"
})
const myMap = types.map(Test).create()
unprotect(myMap)
const val = myMap.put({})
expect(val.id).toBeTruthy()
expect(val.value).toBe("hi")
})
/**
* This test exercises the TypeScript types fo `MSTMap`, to ensure that our typings stay consistent. In PR #2072,
* we changed from accepting `[string, any][] | IKeyValueMap | Map | undefined,` in `initialdata`
* to accepting `IObservableMapInitialValues | undefined,`.
*
* This test demonstrates backwards compatibility for the change, and will let us know if anything changes and breaks
* if we ever update those types as well, or if MobX changes the exported `IObservableMapInitialValues` type.
*
* It looks like `[string, any][]` and `Map` are actually not supported, so we just test the `IKeyValueMap` and `undefined` cases
* for now. See https://github.com/mobxjs/mobx-state-tree/pull/2072#issuecomment-1747482100
*/
describe("#2072 - IObservableMapInitialValues types should work correctly", () => {
it("should accept IKeyValueMap", () => {
const initialData = {
"1": "Tyler",
"2": "Jamon"
}
const mapInstance = types.map(types.string).create(initialData)
expect(mapInstance.size).toBe(2)
})
it("should accept undefined", () => {
const mapInstance = types.map(types.string).create(undefined)
expect(mapInstance.size).toBe(0)
})
})
================================================
FILE: __tests__/core/model.test.ts
================================================
import { applySnapshot, getSnapshot, types } from "../../src"
import { Hook } from "../../src/internal"
import { describe, expect, it, jest, test } from "bun:test"
describe("Model instantiation", () => {
describe("Model name", () => {
test("Providing a string as the first argument should set it as the model's name.", () => {
const Model = types.model("Name", {})
expect(Model.name).toBe("Name")
})
test("Providing an empty string as the first argument should set it as the model's name.", () => {
const Model = types.model("", {})
expect(Model.name).toBe("")
})
describe("Providing a non-string argument as the first argument should set the model's name as 'AnonymousModel'.", () => {
const testCases = [
{},
null,
undefined,
1,
true,
[],
function () {},
new Date(),
/a/,
new Map(),
new Set(),
Symbol(),
new Error(),
NaN,
Infinity
]
testCases.forEach(testCase => {
test(`Providing ${JSON.stringify(
testCase
)} as the first argument should set the model's name as 'AnonymousModel'.`, () => {
const Model = types.model(testCase as any)
expect(Model.name).toBe("AnonymousModel")
})
})
})
})
describe("Model properties", () => {
test("Providing a string as the first argument and an object as the second argument should use the object's properties in the model.", () => {
const Model = types.model("name", {
prop1: "prop1",
prop2: 2
})
expect(Model.properties).toHaveProperty("prop1")
expect(Model.properties).toHaveProperty("prop2")
})
test("Providing an object as the first argument should parse and use its properties.", () => {
const Model = types.model({
prop1: "prop1",
prop2: 2
})
expect(Model.properties).toHaveProperty("prop1")
expect(Model.properties).toHaveProperty("prop2")
})
test("Providing a string as the first argument and a falsy value as the second argument should result in an empty set of properties.", () => {
const Model = types.model("name", null as any)
expect(Model.properties).toEqual({})
})
test("Model should not mutate properties object", () => {
const properties = {
prop1: "prop1",
prop2: 2
}
const Model = types.model("name", properties)
expect(properties).toEqual({
prop1: "prop1",
prop2: 2
})
})
})
describe("Model identifier", () => {
test("If no identifier attribute is provided, the identifierAttribute should be undefined.", () => {
const Model = types.model("name", {})
expect(Model.identifierAttribute).toBeUndefined()
})
test("If an identifier attribute is provided, the identifierAttribute should be set for the object.", () => {
const Model = types.model("name", {
id: types.identifier
})
expect(Model.identifierAttribute).toBe("id")
})
test("If an identifier attribute has already been provided, an error should be thrown when attempting to provide a second one.", () => {
expect(() => {
types.model("name", {
id: types.identifier,
id2: types.identifier
})
}).toThrow(
"[mobx-state-tree] Cannot define property 'id2' as object identifier, property 'id' is already defined as identifier property"
)
})
})
describe("Edge case behavior", () => {
describe("when we provide no arguments to the function", () => {
test("the model will be named AnonymousModel", () => {
const Model = types.model()
expect(Model.name).toBe("AnonymousModel")
})
test("the model will have no properties", () => {
const Model = types.model()
const modelSnapshot = getSnapshot(Model.create())
expect(modelSnapshot).toEqual({})
})
})
test("the model will have no properties", () => {
const Model = types.model()
const modelSnapshot = getSnapshot(Model.create())
expect(modelSnapshot).toEqual({})
})
if (process.env.NODE_ENV !== "production") {
test("it should not throw an error", () => {
expect(() => {
types.model()
}).not.toThrow()
})
}
})
describe("when we provide an invalid name value, but a valid property object", () => {
if (process.env.NODE_ENV === "production") {
test("the model will be named AnonymousModel", () => {
const Model = types.model(null as any, {
prop1: "prop1",
prop2: 2
})
expect(Model.name).toBe("AnonymousModel")
})
test("the model will have no properties", () => {
const Model = types.model(null as any, {
prop1: "prop1",
prop2: 2
})
const modelSnapshot = getSnapshot(Model.create())
// @ts-expect-error - we explicitly allowed an invalid input, so we expect an empty object, but TS doesn't.
expect(modelSnapshot).toEqual({})
})
} else {
test("it should complain about invalid name", () => {
expect(() => {
types.model(null as any, {
prop1: "prop1",
prop2: 2
})
}).toThrow(
"[mobx-state-tree] Model creation failed. First argument must be a string when two arguments are provided"
)
})
}
})
describe("when we provide three arguments to the function", () => {
test("the model gets the correct name", () => {
// @ts-ignore
const Model = types.model("name", {}, {})
expect(Model.name).toBe("name")
})
test("the model gets the correct properties", () => {
const Model = types.model(
"name",
{
prop1: "prop1",
prop2: 2
},
// @ts-ignore
{}
)
const modelSnapshot = getSnapshot(Model.create())
expect(modelSnapshot).toEqual({
prop1: "prop1",
prop2: 2
})
})
})
test("it should call preProcessSnapshot with the correct argument", () => {
const onSnapshot = jest.fn((snapshot: any) => {
return {
val: snapshot.val + 1
}
})
const Model = types
.model({
val: types.number
})
.preProcessSnapshot(onSnapshot)
const model = Model.create({ val: 0 })
applySnapshot(model, { val: 1 })
expect(onSnapshot).toHaveBeenLastCalledWith({ val: 1 })
})
describe("Should show a friendly message when a model has a property overridden by", () => {
test("a view", () => {
const UserModel = types
.model("UserModel", {
id: types.identifier,
name: types.string
})
.views(user => ({
get name() {
return user.name
}
}))
expect(() =>
UserModel.create({
id: "chakri",
name: "Subramanya Chakravarthy"
})
).toThrow("[mobx-state-tree] 'name' is a property and cannot be declared as a view")
})
test("an action", () => {
const StringSet = types
.model("StringSet", {
setName: types.string,
items: types.array(types.string)
})
.actions(self => ({
setName(name: string) {
self.setName = name
}
}))
expect(() =>
StringSet.create({
setName: "Fruits",
items: ["banana", "apple"]
})
).toThrow(
"[mobx-state-tree] 'setName' is a property and cannot be declared as an action"
)
})
test("a volatile", () => {
const UserModel = types
.model("UserModel", {
id: types.identifier,
name: types.string
})
.volatile(_self => ({
name: "Subramanya Chakravarthy"
}))
expect(() =>
UserModel.create({
id: "chakri",
name: "Subramanya Chakravarthy"
})
).toThrow(
"[mobx-state-tree] 'name' is a property and cannot be declared as volatile state"
)
})
})
describe("with all of the property types", () => {
const IdentifiedWithString = types.model({ id: types.identifier })
const IdentifiedWithNumber = types.model({ id: types.identifierNumber })
const Custom = types.custom({
name: "angle bracketed",
fromSnapshot(snapshot, _env) {
return `<${snapshot}>`
},
toSnapshot(value) {
return value.slice(1, -1)
},
isTargetType(value): boolean {
return value.startsWith("<") && value.endsWith(">")
},
getValidationMessage(snapshot): string {
if (typeof snapshot == "string") return ""
throw new Error(`${snapshot} missing surrounding angle brackets`)
}
})
const Everything = types.model({
boolean: types.boolean,
custom: Custom,
Date: types.Date,
enumeration: types.enumeration(["A", "B"]),
float: types.float,
finite: types.finite,
frozen: types.frozen<{ s: string }>(),
integer: types.integer,
late: types.late(() => types.string),
lazy: types.lazy("lazy", {
loadType: () => Promise.resolve(types.string),
shouldLoadPredicate: () => true
}),
literal: types.literal("literal"),
maybe: types.maybe(types.string),
maybeNull: types.maybeNull(types.number),
null: types.null,
number: types.number,
optional: types.optional(types.string, "default"),
reference: types.reference(IdentifiedWithString),
refinement: types.refinement(types.string, s => s.length > 2),
string: types.string,
safeReference: types.safeReference(IdentifiedWithNumber),
undefined: types.undefined,
union: types.union(types.string, types.number)
} satisfies Record<
Exclude<
keyof typeof types,
| "compose"
| "model"
| "identifier"
| "identifierNumber"
| "bigint"
| "map"
| "array"
| "snapshotProcessor"
>,
any
>)
const Root = types.model({
everything: types.snapshotProcessor(Everything, {
preProcessor(snapshot) {
if (snapshot.refinement.length < 2) {
return { ...snapshot, refinement: "" }
}
return snapshot
}
}),
mapOfStrings: types.map(IdentifiedWithString),
arrayOfNumbers: types.array(IdentifiedWithNumber)
})
it("does not throw with input snapshots", () => {
const value = Root.create({
everything: {
boolean: true,
custom: "custom",
Date: 0,
enumeration: "A",
float: 1.23,
finite: 1,
frozen: { s: "test" },
integer: 1,
late: "test",
lazy: "test",
literal: "literal",
maybe: "test",
maybeNull: 1,
null: null,
number: 1,
optional: "test",
reference: "id-a",
refinement: "test",
string: "test",
safeReference: 1,
undefined: undefined,
union: "test"
},
mapOfStrings: {
"id-a": { id: "id-a" }
},
arrayOfNumbers: [{ id: 1 }]
})
expect(getSnapshot(value)).toEqual({
everything: {
boolean: true,
custom: "custom",
Date: 0,
enumeration: "A",
float: 1.23,
finite: 1,
frozen: { s: "test" },
integer: 1,
late: "test",
lazy: "test",
literal: "literal",
maybe: "test",
maybeNull: 1,
null: null,
number: 1,
optional: "test",
reference: "id-a",
refinement: "test",
string: "test",
safeReference: 1,
undefined: undefined,
union: "test"
},
mapOfStrings: {
"id-a": { id: "id-a" }
},
arrayOfNumbers: [{ id: 1 }]
})
})
it("does not throw with input instances", () => {
const instanceA = IdentifiedWithString.create({ id: "id-a" })
const instance1 = IdentifiedWithNumber.create({ id: 1 })
const value = Root.create({
everything: {
boolean: types.boolean.create(true),
custom: Custom.create("custom"),
Date: types.Date.create(0),
enumeration: types.enumeration(["A", "B"]).create("A"),
float: types.float.create(1.23),
finite: types.finite.create(1),
frozen: types.frozen<{ s: string }>().create({ s: "test" }),
integer: types.integer.create(1),
late: types.string.create("test"),
lazy: types.string.create("test"),
literal: types.literal("literal").create("literal"),
maybe: types.maybe(types.string).create("test"),
maybeNull: types.maybeNull(types.number).create(1),
null: types.null.create(null),
number: types.number.create(1),
optional: types.optional(types.string, "default").create("test"),
reference: instanceA,
refinement: types.refinement(types.string, s => s.length > 2).create("test"),
string: types.string.create("test"),
safeReference: instance1,
undefined: types.undefined.create(undefined),
union: types.union(types.string, types.number).create("test")
},
mapOfStrings: { "id-a": instanceA },
arrayOfNumbers: [instance1]
})
expect(getSnapshot(value)).toEqual({
everything: {
boolean: true,
custom: "custom",
Date: 0,
enumeration: "A",
float: 1.23,
finite: 1,
frozen: { s: "test" },
integer: 1,
late: "test",
lazy: "test",
literal: "literal",
maybe: "test",
maybeNull: 1,
null: null,
number: 1,
optional: "test",
reference: "id-a",
refinement: "test",
string: "test",
safeReference: 1,
undefined: undefined,
union: "test"
},
mapOfStrings: {
"id-a": { id: "id-a" }
},
arrayOfNumbers: [{ id: 1 }]
})
})
})
})
describe("Model properties objects", () => {
describe("when a user names a property the same as an MST lifecycle hook", () => {
test("it throws an error", () => {
const hookValues = Object.values(Hook)
hookValues.forEach(hook => {
expect(() => {
types.model({
[hook]: types.string
})
}).toThrow()
})
})
})
describe("when a user attempts to define a property with the get keyword", () => {
test("it throws an error", () => {
expect(() => {
types.model({
get foo() {
return "bar"
}
})
}).toThrow(
"[mobx-state-tree] Getters are not supported as properties. Please use views instead"
)
})
})
describe("when a user attempts to define a property with null as the value", () => {
test("it throws an error", () => {
expect(() => {
types.model({
foo: null as any
})
}).toThrow(
"[mobx-state-tree] The default value of an attribute cannot be null or undefined as the type cannot be inferred. Did you mean `types.maybe(someType)`?"
)
})
})
describe("when a user attempts to define a property with undefined as the value", () => {
test("it throws an error", () => {
expect(() => {
types.model({
foo: undefined as any
})
}).toThrow(
"[mobx-state-tree] The default value of an attribute cannot be null or undefined as the type cannot be inferred. Did you mean `types.maybe(someType)`?"
)
})
})
describe("when a user defines a property using a primitive value (not null or undefined)", () => {
describe("and the primitive value is a string", () => {
test("it converts a string to an optional string", () => {
const Model = types.model({
foo: "bar"
})
const modelDescription = Model.describe()
expect(modelDescription).toBe("{ foo: string? }")
})
test("it uses the primitive value as the default value", () => {
const Model = types.model({
foo: "bar"
})
const modelSnapshot = getSnapshot(Model.create())
expect(modelSnapshot).toEqual({
foo: "bar"
})
})
})
describe("and the primitive value is a number", () => {
test("it converts a number to an optional number", () => {
const Model = types.model({
foo: 1
})
const modelDescription = Model.describe()
expect(modelDescription).toBe("{ foo: number? }")
})
test("it uses the primitive value as the default value", () => {
const Model = types.model({
foo: 1
})
const modelSnapshot = getSnapshot(Model.create())
expect(modelSnapshot).toEqual({
foo: 1
})
})
})
describe("and the primitive value is a boolean", () => {
test("it converts a boolean to an optional boolean", () => {
const Model = types.model({
foo: true
})
const modelDescription = Model.describe()
expect(modelDescription).toBe("{ foo: boolean? }")
})
test("it uses the primitive value as the default value", () => {
const Model = types.model({
foo: true
})
const modelSnapshot = getSnapshot(Model.create())
expect(modelSnapshot).toEqual({
foo: true
})
})
})
describe("and the primitive value is a date", () => {
test("it converts a date to an optional date", () => {
const Model = types.model({
foo: new Date()
})
const modelDescription = Model.describe()
expect(modelDescription).toBe("{ foo: Date? }")
})
test("it sets a default value with the date in unix milliseconds timestamp", () => {
const date = new Date("2023-07-24T04:26:04.701Z")
const Model = types.model({
foo: date
})
const modelSnapshot = getSnapshot(Model.create())
expect(modelSnapshot).toEqual({
foo: 1690172764701
})
})
})
})
describe("when a user defines a property using a complex type", () => {
describe('and that type is "types.map"', () => {
test("it sets the default value to an empty map", () => {
const Model = types.model({
foo: types.map(types.string)
})
const modelSnapshot = getSnapshot(Model.create())
expect(modelSnapshot).toEqual({
foo: {}
})
})
})
describe('and that type is "types.array"', () => {
test("it sets the default value to an empty array", () => {
const Model = types.model({
foo: types.array(types.string)
})
const modelSnapshot = getSnapshot(Model.create())
expect(modelSnapshot).toEqual({
foo: []
})
})
})
describe("and that type is another model", () => {
test("it sets the default value to the default of that model", () => {
const Todo = types.model({
task: types.optional(types.string, "test")
})
const TodoStore = types.model("TodoStore", {
todo1: types.optional(Todo, () => Todo.create())
})
const modelSnapshot = getSnapshot(TodoStore.create())
expect(modelSnapshot).toEqual({
todo1: {
task: "test"
}
})
})
})
})
describe("when a user defines a property using a function", () => {
if (process.env.NODE_ENV !== "production") {
test("it throws an error when not in production", () => {
expect(() => {
// @ts-ignore
types.model({
foo: () => "bar"
})
}).toThrow(
"[mobx-state-tree] Invalid type definition for property 'foo', it looks like you passed a function. Did you forget to invoke it, or did you intend to declare a view / action?"
)
})
}
})
describe("when a user defines a property using a plain JavaScript object", () => {
if (process.env.NODE_ENV !== "production") {
test("it throws an error when not in production", () => {
expect(() => {
// @ts-ignore
types.model({
foo: {}
})
}).toThrow()
})
}
})
describe("when a user uses `.props` to create a child model", () => {
it("does not modify the parent properties", () => {
const Parent = types.model({
first: types.string
})
const Child = Parent.props({
second: types.string
})
expect(Parent.properties).not.toHaveProperty("second")
})
})
})
================================================
FILE: __tests__/core/name.test.ts
================================================
import { types } from "../../src"
import { getDebugName } from "mobx"
import { expect, test } from "bun:test"
test("it should have a debug name", () => {
const Model = types.model("Name")
const model = Model.create()
const array = types.array(Model).create()
const map = types.map(Model).create()
expect(getDebugName(model)).toBe("Name")
expect(getDebugName(array)).toBe("Name[]")
expect(getDebugName(map)).toBe("Map")
})
================================================
FILE: __tests__/core/node.test.ts
================================================
import {
getPath,
getSnapshot,
getParent,
hasParent,
getRoot,
getIdentifier,
getPathParts,
isAlive,
clone,
getType,
getChildType,
recordActions,
recordPatches,
types,
destroy,
unprotect,
hasParentOfType,
getParentOfType,
detach,
getNodeId
} from "../../src"
import { autorun, configure } from "mobx"
import { expect, test } from "bun:test"
// getParent
test("it should resolve to the parent instance", () => {
const Row = types.model({
article_id: 0
})
const Document = types.model({
rows: types.optional(types.array(Row), [])
})
const doc = Document.create()
unprotect(doc)
const row = Row.create()
doc.rows.push(row)
expect(getParent(row)).toEqual(doc.rows)
})
// hasParent
test("it should check for parent instance", () => {
const Row = types.model({
article_id: 0
})
const Document = types.model({
rows: types.optional(types.array(Row), [])
})
const doc = Document.create()
unprotect(doc)
const row = Row.create()
doc.rows.push(row)
expect(hasParent(row)).toEqual(true)
})
test("it should check for parent instance (unbound)", () => {
const Row = types.model({
article_id: 0
})
const row = Row.create()
expect(hasParent(row)).toEqual(false)
})
// getParentOfType
test("it should resolve to the given parent instance", () => {
configure({
useProxies: "never"
})
const Cell = types.model({})
const Row = types.model({
cells: types.optional(types.array(Cell), [])
})
const Document = types.model({
rows: types.optional(types.array(Row), [])
})
const doc = Document.create({
rows: [
{
cells: [{}]
}
]
})
expect(getParentOfType(doc.rows[0].cells[0], Document)).toEqual(doc)
})
test("it should throw if there is not parent of type", () => {
const Cell = types.model({})
const Row = types.model({
cells: types.optional(types.array(Cell), [])
})
const Document = types.model({
rows: types.optional(types.array(Row), [])
})
const row = Row.create({
cells: [{}]
})
expect(() => getParentOfType(row.cells[0], Document)).toThrow(
"[mobx-state-tree] Failed to find the parent of AnonymousModel@/cells/0 of a given type"
)
})
// hasParentOfType
test("it should check for parent instance of given type", () => {
const Cell = types.model({})
const Row = types.model({
cells: types.optional(types.array(Cell), [])
})
const Document = types.model({
rows: types.optional(types.array(Row), [])
})
const doc = Document.create({
rows: [
{
cells: [{}]
}
]
})
expect(hasParentOfType(doc.rows[0].cells[0], Document)).toEqual(true)
})
test("it should check for parent instance of given type (unbound)", () => {
const Cell = types.model({})
const Row = types.model({
cells: types.optional(types.array(Cell), [])
})
const Document = types.model({
rows: types.optional(types.array(Row), [])
})
const row = Row.create({
cells: [{}]
})
expect(hasParentOfType(row.cells[0], Document)).toEqual(false)
})
// getRoot
test("it should resolve to the root of an object", () => {
const Row = types.model("Row", {
article_id: 0
})
const Document = types.model("Document", {
rows: types.optional(types.array(Row), [])
})
const doc = Document.create()
unprotect(doc)
const row = Row.create()
doc.rows.push(row)
expect(getRoot(row)).toBe(doc)
})
// getIdentifier
test("it should resolve to the identifier of the object", () => {
const Document = types.model("Document", {
id: types.identifier
})
const doc = Document.create({
id: "document_1"
})
// get identifier of object
expect(getIdentifier(doc)).toBe("document_1")
})
// getPath
test("it should resolve the path of an object", () => {
const Row = types.model({
article_id: 0
})
const Document = types.model({
rows: types.optional(types.array(Row), [])
})
const doc = Document.create()
unprotect(doc)
const row = Row.create()
doc.rows.push(row)
expect(getPath(row)).toEqual("/rows/0")
})
// getPathParts
test("it should resolve the path of an object", () => {
const Row = types.model({
article_id: 0
})
const Document = types.model({
rows: types.optional(types.array(Row), [])
})
const doc = Document.create()
unprotect(doc)
const row = Row.create()
doc.rows.push(row)
expect(getPathParts(row)).toEqual(["rows", "0"])
})
test("it should resolve parents", () => {
const Row = types.model({
article_id: 0
})
const Document = types.model({
rows: types.optional(types.array(Row), [])
})
const doc = Document.create()
unprotect(doc)
const row = Row.create()
doc.rows.push(row)
expect(hasParent(row)).toBe(true) // array
expect(hasParent(row, 2)).toBe(true) // row
expect(hasParent(row, 3)).toBe(false)
expect(getParent(row) === doc.rows).toBe(true) // array
expect(getParent(row, 2) === doc).toBe(true) // row
expect(() => getParent(row, 3)).toThrow(
"[mobx-state-tree] Failed to find the parent of AnonymousModel@/rows/0 at depth 3"
)
})
// clone
test("it should clone a node", () => {
configure({
useProxies: "never"
})
const Row = types.model({
article_id: 0
})
const Document = types.model({
rows: types.optional(types.array(Row), [])
})
const doc = Document.create()
unprotect(doc)
const row = Row.create()
doc.rows.push(row)
const cloned = clone(doc)
expect(doc).toEqual(cloned)
expect(getSnapshot(doc)).toEqual(getSnapshot(cloned))
})
test("it should be possible to clone a dead object", () => {
configure({
useProxies: "never"
})
const Task = types.model("Task", {
x: types.string
})
const a = Task.create({ x: "a" })
const store = types
.model({
todos: types.optional(types.array(Task), [])
})
.create({
todos: [a]
})
unprotect(store)
expect(store.todos.slice()).toEqual([a])
expect(isAlive(a)).toBe(true)
store.todos.splice(0, 1)
expect(isAlive(a)).toBe(false)
const a2 = clone(a)
store.todos.splice(0, 0, a2)
expect(store.todos[0].x).toBe("a")
})
// getModelFactory
test("it should return the model factory", () => {
const Document = types.model({
customer_id: 0
})
const doc = Document.create()
expect(getType(doc)).toEqual(Document)
})
// getChildModelFactory
test("it should return the child model factory", () => {
const Row = types.model({
article_id: 0
})
const ArrayOfRow = types.optional(types.array(Row), [])
const Document = types.model({
rows: ArrayOfRow
})
const doc = Document.create()
expect(getChildType(doc, "rows")).toEqual(ArrayOfRow)
})
test("a node can exists only once in a tree", () => {
const Row = types.model({
article_id: 0
})
const Document = types.model({
rows: types.optional(types.array(Row), []),
foos: types.optional(types.array(Row), [])
})
const doc = Document.create()
unprotect(doc)
const row = Row.create()
doc.rows.push(row)
expect(() => {
doc.foos.push(row)
}).toThrow(
"[mobx-state-tree] Cannot add an object to a state tree if it is already part of the same or another state tree. Tried to assign an object to '/foos/0', but it lives already at '/rows/0'"
)
})
test("make sure array filter works properly", () => {
const Row = types.model({
done: false
})
const Document = types
.model({
rows: types.optional(types.array(Row), [])
})
.actions(self => {
function clearDone() {
self.rows.filter(row => row.done === true).forEach(destroy)
}
return {
clearDone
}
})
const doc = Document.create()
unprotect(doc)
const a = Row.create({ done: true })
const b = Row.create({ done: false })
doc.rows.push(a)
doc.rows.push(b)
doc.clearDone()
expect(getSnapshot(doc)).toEqual({ rows: [{ done: false }] })
})
// === RECORD PATCHES ===
test("it can record and replay patches", () => {
const Row = types.model({
article_id: 0
})
const Document = types.model({
customer_id: 0,
rows: types.optional(types.array(Row), [])
})
const source = Document.create()
unprotect(source)
const target = Document.create()
const recorder = recordPatches(source)
source.customer_id = 1
source.rows.push(Row.create({ article_id: 1 }))
recorder.replay(target)
expect(getSnapshot(source)).toEqual(getSnapshot(target))
})
// === RECORD ACTIONS ===
test("it can record and replay actions", () => {
const Row = types
.model({
article_id: 0
})
.actions(self => {
function setArticle(article_id: number) {
self.article_id = article_id
}
return {
setArticle
}
})
const Document = types
.model({
customer_id: 0,
rows: types.optional(types.array(Row), [])
})
.actions(self => {
function setCustomer(customer_id: number) {
self.customer_id = customer_id
}
function addRow() {
self.rows.push(Row.create())
}
return {
setCustomer,
addRow
}
})
const source = Document.create()
const target = Document.create()
const recorder = recordActions(source)
source.setCustomer(1)
source.addRow()
source.rows[0].setArticle(1)
recorder.replay(target)
expect(getSnapshot(source)).toEqual(getSnapshot(target))
})
test("Liveliness issue #683", () => {
const User = types.model({ id: types.identifierNumber, name: types.string })
const Users = types
.model({
list: types.map(User)
})
.actions(self => ({
put(aUser: typeof User.CreationType | typeof User.Type) {
// if (self.has(user.id)) detach(self.get(user.id));
self.list.put(aUser)
},
get(id: string) {
return self.list.get(id)
},
has(id: string) {
return self.list.has(id)
}
}))
const users = Users.create({
list: {
1: { name: "Name", id: 1 }
}
})
const user = users.get("1")
expect(user!.name).toBe("Name")
users.put({ id: 1, name: "NameX" })
expect(user!.name).toBe("NameX")
expect(users.get("1")!.name).toBe("NameX")
})
test("triggers on changing paths - 1", () => {
const Todo = types.model({
title: types.string
})
const App = types
.model({
todos: types.array(Todo)
})
.actions(self => ({
do(fn: () => void) {
fn()
}
}))
const t1 = Todo.create({ title: "t1 " })
const t2 = Todo.create({ title: "t2 " })
const app = App.create({
todos: [t1]
})
const events: string[] = []
const d1 = autorun(() => {
events.push("t1@" + getPath(t1))
})
const d2 = autorun(() => {
events.push("t2@" + getPath(t2))
})
expect(events.splice(0)).toEqual(["t1@/todos/0", "t2@"])
app.do(() => {
app.todos.unshift(t2)
})
expect(events.splice(0)).toEqual(["t2@/todos/0", "t1@/todos/1"])
app.do(() => {
detach(t2)
})
expect(events.splice(0)).toEqual(["t1@/todos/0", "t2@"])
app.do(() => {
app.todos.splice(0)
})
expect(events.splice(0)).toEqual(["t1@"])
})
test("getNodeId works", () => {
const M = types.model({})
const m1 = M.create()
const m2 = M.create()
const m1Id = getNodeId(m1)
const m2Id = getNodeId(m2)
expect(m1Id).toBeGreaterThan(0)
expect(m2Id).toBe(m1Id + 1)
})
================================================
FILE: __tests__/core/number.test.ts
================================================
import { t } from "../../src"
import { Hook, NodeLifeCycle } from "../../src/internal"
import { describe, it, expect, test } from "bun:test"
describe("types.number", () => {
describe("methods", () => {
describe("create", () => {
describe("with no arguments", () => {
if (process.env.NODE_ENV !== "production") {
it("should throw an error in development", () => {
expect(() => {
t.number.create()
}).toThrow()
})
}
})
describe("with a number argument", () => {
it("should return a number", () => {
const n = t.number.create(1)
expect(typeof n).toBe("number")
})
})
describe("with argument of different types", () => {
// Keep in mind, Infinity and NaN are treated as numbers in JavaScript, so we won't test for them here.
const testCases = [
null,
undefined,
"string",
true,
[],
function () {},
new Date(),
/a/,
new Map(),
new Set(),
Symbol(),
new Error()
]
if (process.env.NODE_ENV !== "production") {
testCases.forEach(testCase => {
it(`should throw an error when passed ${JSON.stringify(testCase)}`, () => {
expect(() => {
t.number.create(testCase as any)
}).toThrow()
})
})
}
})
})
describe("describe", () => {
it("should return the value 'number'", () => {
const description = t.number.describe()
expect(description).toBe("number")
})
})
describe("getSnapshot", () => {
it("should return the value passed in", () => {
const n = t.number.instantiate(null, "", {}, 1)
const snapshot = t.number.getSnapshot(n)
expect(snapshot).toBe(1)
})
})
describe("getSubtype", () => {
it("should return null", () => {
const subtype = t.number.getSubTypes()
expect(subtype).toBe(null)
})
})
describe("instantiate", () => {
if (process.env.NODE_ENV !== "production") {
describe("with invalid arguments", () => {
it("should not throw an error", () => {
expect(() => {
// @ts-ignore
t.number.instantiate()
}).not.toThrow()
})
})
}
describe("with a number argument", () => {
it("should return an object", () => {
const n = t.number.instantiate(null, "", {}, 1)
expect(typeof n).toBe("object")
})
})
})
describe("is", () => {
describe("with a number argument", () => {
it("should return true", () => {
const result = t.number.is(1)
expect(result).toBe(true)
})
})
describe("with argument of different types", () => {
// Keep in mind, Infinity and NaN are treated as numbers in JavaScript, so we won't test for them here.
const testCases = [
null,
undefined,
"string",
true,
[],
function () {},
new Date(),
/a/,
new Map(),
new Set(),
Symbol(),
new Error()
]
testCases.forEach(testCase => {
it(`should return false when passed ${JSON.stringify(testCase)}`, () => {
const result = t.number.is(testCase as any)
expect(result).toBe(false)
})
})
})
})
describe("isAssignableFrom", () => {
describe("with a number argument", () => {
it("should return true", () => {
const result = t.number.isAssignableFrom(t.number)
expect(result).toBe(true)
})
})
describe("with argument of different types", () => {
const testCases = [
t.Date,
t.boolean,
t.finite,
t.float,
t.identifier,
t.identifierNumber,
t.integer,
t.null,
t.string,
t.undefined
]
testCases.forEach(testCase => {
it(`should return false when passed ${JSON.stringify(testCase)}`, () => {
const result = t.number.isAssignableFrom(testCase as any)
expect(result).toBe(false)
})
})
})
})
// TODO: we need to test this, but to be honest I'm not sure what the expected behavior is on single number nodes.
describe.skip("reconcile", () => {})
describe("validate", () => {
describe("with a number argument", () => {
it("should return with no validation errors", () => {
const result = t.number.validate(1, [])
expect(result).toEqual([])
})
})
describe("with argument of different types", () => {
// Keep in mind, Infinity and NaN are treated as numbers in JavaScript, so we won't test for them here.
const testCases = [
null,
undefined,
"string",
true,
[],
function () {},
new Date(),
/a/,
new Map(),
new Set(),
Symbol(),
new Error()
]
testCases.forEach(testCase => {
it(`should return with a validation error when passed ${JSON.stringify(
testCase
)}`, () => {
const result = t.number.validate(testCase as any, [])
expect(result).toEqual([
{
context: [],
message: "Value is not a number",
value: testCase
}
])
})
})
})
})
})
describe("properties", () => {
describe("flags", () => {
test("return the correct value", () => {
const flags = t.number.flags
expect(flags).toBe(2)
})
})
describe("identifierAttribute", () => {
// We don't have a way to set the identifierAttribute on a primitive type, so this should return undefined.
test("returns undefined", () => {
const identifierAttribute = t.number.identifierAttribute
expect(identifierAttribute).toBeUndefined()
})
})
describe("isType", () => {
test("returns true", () => {
const isType = t.number.isType
expect(isType).toBe(true)
})
})
describe("name", () => {
test('returns "number"', () => {
const name = t.number.name
expect(name).toBe("number")
})
})
})
describe("instance", () => {
describe("methods", () => {
describe("aboutToDie", () => {
it("calls the beforeDetach hook", () => {
const n = t.number.instantiate(null, "", {}, 1)
let called = false
n.registerHook(Hook.beforeDestroy, () => {
called = true
})
n.aboutToDie()
expect(called).toBe(true)
})
})
describe("die", () => {
it("kills the node", () => {
const n = t.number.instantiate(null, "", {}, 1)
n.die()
expect(n.isAlive).toBe(false)
})
it("should mark the node as dead", () => {
const n = t.number.instantiate(null, "", {}, 1)
n.die()
expect(n.state).toBe(NodeLifeCycle.DEAD)
})
})
describe("finalizeCreation", () => {
it("should mark the node as finalized", () => {
const n = t.number.instantiate(null, "", {}, 1)
n.finalizeCreation()
expect(n.state).toBe(NodeLifeCycle.FINALIZED)
})
})
describe("finalizeDeath", () => {
it("should mark the node as dead", () => {
const n = t.number.instantiate(null, "", {}, 1)
n.finalizeDeath()
expect(n.state).toBe(NodeLifeCycle.DEAD)
})
})
describe("getReconciliationType", () => {
it("should return the correct type", () => {
const n = t.number.instantiate(null, "", {}, 1)
const type = n.getReconciliationType()
expect(type).toBe(t.number)
})
})
describe("getSnapshot", () => {
it("should return the value passed in", () => {
const n = t.number.instantiate(null, "", {}, 1)
const snapshot = n.getSnapshot()
expect(snapshot).toBe(1)
})
})
describe("registerHook", () => {
it("should register a hook and call it", () => {
const n = t.number.instantiate(null, "", {}, 1)
let called = false
n.registerHook(Hook.beforeDestroy, () => {
called = true
})
n.die()
expect(called).toBe(true)
})
})
describe("setParent", () => {
if (process.env.NODE_ENV !== "production") {
describe("with null", () => {
it("should throw an error", () => {
const n = t.number.instantiate(null, "", {}, 1)
expect(() => {
n.setParent(null, "foo")
}).toThrow()
})
})
describe("with a parent object", () => {
it("should throw an error", () => {
const Parent = t.model({
child: t.number
})
const parent = Parent.create({ child: 1 })
const n = t.number.instantiate(null, "", {}, 1)
expect(() => {
// @ts-ignore
n.setParent(parent, "bar")
}).toThrow(
"[mobx-state-tree] assertion failed: scalar nodes cannot change their parent"
)
})
})
}
})
})
})
})
================================================
FILE: __tests__/core/object-node.test.ts
================================================
import { t } from "../../src/index"
import { Hook, ObjectNode, onPatch, unprotect } from "../../src/internal"
import { describe, expect, jest, it, spyOn } from "bun:test"
const TestModel = t.model("TestModel", {
title: t.string
})
const TestArray = t.array(TestModel)
const TestMap = t.map(TestModel)
const Parent = t.model("Parent", {
child: t.maybe(TestModel)
})
const TestModelWithIdentifier = t.model("TestModelWithIdentifier", {
id: t.identifier,
title: t.string
})
/**
* These tests were added to help understand the ObjectNode class and how it interacts internally in MST.
* As such, they test internals of the library that aren't intended to be used by consumers. That means:
*
* 1. Please do not use these examples in application-level code with MST, these are not necessarily best practices, and are definitely not intended as a public API
* 2. These tests may not test every single case, but they cover scenarios I was interested in understanding.
* 3. Since the tests are tightly coupled to implementation, this test suite may end up being noisy.
*
* If you are making a change to MST and get failures here, please consider that as a yellow flag, not a red flag.
* Feel free to make changes you need to, or even skip tests if they're a nuisance.
*/
describe("ObjectNode", () => {
describe("constructor", () => {
// Since ObjectNode is not exported as part of the MST API, we don't have tests for invalid parameters, but we expect an error in this scenario.
it("throws if type is not a complex type", () => {
expect(() => new ObjectNode(t.string as any, null, "", {}, "foo")).toThrow(
"complexType.initializeChildNodes is not a function"
)
})
it("works with a complex type", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node).toBeDefined()
})
})
describe("methods", () => {
describe("aboutToDie", () => {
describe("if the observable node is unitialized", () => {
it("does not call the onAboutToDie hook", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const hook = jest.fn()
node.registerHook(Hook.beforeDestroy, hook)
node.aboutToDie()
expect(hook).not.toHaveBeenCalled()
})
})
describe("if the observable node is initialized", () => {
it("calls the onAboutToDie hook", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const hook = jest.fn()
node.registerHook(Hook.beforeDestroy, hook)
node.createObservableInstance() // createObservableInstance calls finalizeCreation internally, and marks the observable node as being created.
node.aboutToDie()
expect(hook).toHaveBeenCalled()
})
})
})
describe("addDisposer", () => {
it("adds a disposer to the node", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const disposer = jest.fn()
node.addDisposer(disposer)
node.createObservableInstance()
expect(node.hasDisposer(disposer)).toBe(true)
})
})
describe("addMiddleWare", () => {
it("adds a middleware to the node", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const middleware = jest.fn((call, next) => {
next(call)
})
node.addMiddleWare(middleware)
node.createObservableInstance()
node.applySnapshot({ title: "hello" } as any)
expect(middleware).toHaveBeenCalled()
})
})
describe("applyPatchLocally", () => {
describe("when the node is protected", () => {
it("throws an error", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(() =>
node.applyPatchLocally("", {
op: "replace",
path: "",
value: { title: "hello" }
})
).toThrow(
"[mobx-state-tree] Cannot modify 'TestModel@', the object is protected and can only be modified by using an action."
)
})
})
describe("when the node is not alive", () => {
it("warns by default", () => {
const warnSpy = spyOn(console, "warn")
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected
node.die()
node.applyPatchLocally("", {
op: "replace",
path: "",
value: { title: "hello" }
})
expect(warnSpy).toHaveBeenCalled()
})
})
describe("when the node is alive and not protected", () => {
describe("for models", () => {
it("does not allow remove", () => {
const node = new ObjectNode(
TestModel as any,
null,
"",
{},
{ title: "hello" }
)
unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected
expect(() => {
node.applyPatchLocally("", {
op: "remove",
path: ""
})
}).toThrow("[mobx-state-tree] object does not support operation remove")
})
it("allows add", () => {
const node = new ObjectNode(
TestModel as any,
null,
"",
{},
{ title: "hello" }
)
unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected
node.applyPatchLocally("title", {
op: "add",
path: "",
value: "world"
})
// @ts-ignore
expect(node.storedValue.title).toBe("world")
})
it("allows replace", () => {
const node = new ObjectNode(
TestModel as any,
null,
"",
{},
{ title: "hello" }
)
unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected
node.applyPatchLocally("title", {
op: "replace",
path: "",
value: "world"
})
// @ts-ignore
expect(node.storedValue.title).toBe("world")
})
})
describe("for arrays", () => {
it("works for replace", () => {
const node = new ObjectNode(TestArray as any, null, "", {}, [
{ title: "hello" }
])
unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected
node.applyPatchLocally("0", {
op: "replace",
path: "",
value: { title: "world" }
})
// @ts-ignore
expect(node.storedValue[0].title).toBe("world")
})
it("works for add", () => {
const node = new ObjectNode(TestArray as any, null, "", {}, [
{ title: "hello" }
])
unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected
node.applyPatchLocally("1", {
op: "add",
path: "",
value: { title: "world" }
})
// @ts-ignore
expect(node.storedValue.length).toBe(2)
// @ts-ignore
expect(node.storedValue[1].title).toBe("world")
})
it("works for remove", () => {
const node = new ObjectNode(TestArray as any, null, "", {}, [
{ title: "hello" }
])
unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected
node.applyPatchLocally("0", {
op: "remove",
path: ""
})
// @ts-ignore
expect(node.storedValue.length).toBe(0)
})
})
describe("for maps", () => {
it("works for add", () => {
const node = new ObjectNode(
TestMap as any,
null,
"",
{},
{ hello: { title: "hello" } }
)
unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected
node.applyPatchLocally("world", {
op: "add",
path: "",
value: { title: "world" }
})
// @ts-ignore
expect(node.storedValue.get("world").title).toBe("world")
})
it("works for replace", () => {
const node = new ObjectNode(
TestMap as any,
null,
"",
{},
{ hello: { title: "hello" } }
)
unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected
node.applyPatchLocally("hello", {
op: "replace",
path: "",
value: { title: "world" }
})
// @ts-ignore
expect(node.storedValue.get("hello").title).toBe("world")
})
it("works for remove", () => {
const node = new ObjectNode(
TestMap as any,
null,
"",
{},
{ hello: { title: "hello" } }
)
unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected
node.applyPatchLocally("hello", {
op: "remove",
path: ""
})
// @ts-ignore
expect(node.storedValue.size).toBe(0)
})
})
})
})
describe("applyPatches", () => {
describe("when the path is not specified", () => {
it("applies the value as a snapshot", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
// Notice we're saying to "remove" the value, but using just a snapshot. This will just apply a snapshot based on how applyPatches runs.
// @ts-ignore
node.applyPatches([{ op: "remove", value: { title: "world" } }])
// @ts-ignore
expect(node.storedValue.title).toBe("world")
})
})
describe("with correct paths", () => {
it("applies the patch", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.applyPatches([{ op: "replace", path: "/title", value: "world" }])
// @ts-ignore
expect(node.storedValue.title).toBe("world")
})
})
})
describe("applySnapshot", () => {
it("works", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.applySnapshot({ title: "world" })
// @ts-ignore
expect(node.storedValue.title).toBe("world")
})
})
describe("assertAlive", () => {
describe("when the node is alive", () => {
it("does not warn", () => {
const warnSpy = spyOn(console, "warn")
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.assertAlive({})
expect(warnSpy).not.toHaveBeenCalled()
})
})
describe("when the node is not alive", () => {
it("warns about liveliness", () => {
const warnSpy = spyOn(console, "warn")
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.die()
node.assertAlive({})
const receivedErrorMessage = warnSpy.mock.calls[0][0].toString()
expect(receivedErrorMessage).toBe(
"Error: [mobx-state-tree] You are trying to read or write to an object that is no longer part of a state tree. (Object type: 'TestModel', Path upon death: '', Subpath: '', Action: ''). Either detach nodes first, or don't use objects after removing / replacing them in the tree."
)
})
})
})
describe("assertWritable", () => {
describe("when the node is not alive", () => {
it("throws an error", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.die()
expect(() => node.assertWritable({})).toThrow(
"[mobx-state-tree] Cannot modify 'TestModel@ [dead]', the object is protected and can only be modified by using an action."
)
})
})
describe("when the node is alive and protected", () => {
it("throws an error", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
// Nodes are protected by default
expect(() => node.assertWritable({})).toThrow(
"[mobx-state-tree] Cannot modify 'TestModel@', the object is protected and can only be modified by using an action."
)
})
})
describe("when the node is alive and not protected", () => {
it("does not throw an error", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
unprotect(node.root.value)
expect(() => node.assertWritable({})).not.toThrow()
})
})
})
describe("clearParent", () => {
it("removes the parent from a node", () => {
const parent = Parent.create({ child: { title: "hello" } })
const child = parent.child
expect(child).toBeDefined()
// We can't directly modify the tree without unprotecting it first
unprotect(parent)
// The object node is made available through the $treenode property
child!.$treenode.clearParent()
expect(parent.child).toBeUndefined()
})
})
describe("createObservableInstance", () => {
describe("when the node is still initializing", () => {
it("works", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.storedValue).toBeUndefined()
expect(node.state).toBe(0)
node.createObservableInstance()
expect(node.storedValue).toBeDefined()
expect(node.state).toBe(2)
})
})
if (process.env.NODE_ENV !== "production") {
describe("when the node has been initialized", () => {
it("does not work", () => {
const node = new ObjectNode(
TestModel as any,
null,
"",
{},
{ title: "hello" }
)
node.createObservableInstance()
expect(() => node.createObservableInstance()).toThrow(
"[mobx-state-tree] assertion failed: the creation of the observable instance must be done on the initializing phase"
)
})
})
}
if (process.env.NODE_ENV !== "production") {
describe("if the node is dead", () => {
it("does not work", () => {
const node = new ObjectNode(
TestModel as any,
null,
"",
{},
{ title: "hello" }
)
node.die()
expect(() => node.createObservableInstance()).toThrow(
"[mobx-state-tree] assertion failed: the creation of the observable instance must be done on the initializing phase"
)
})
})
}
})
describe("createObservableInstanceIfNeeded", () => {
describe("when the node is still initializing", () => {
it("works", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.storedValue).toBeUndefined()
expect(node.state).toBe(0)
node.createObservableInstanceIfNeeded()
expect(node.storedValue).toBeDefined()
expect(node.state).toBe(2)
})
})
describe("when the node has been initialized", () => {
it("does not throw, but an observable instance should be available", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.createObservableInstanceIfNeeded()
expect(() => node.createObservableInstanceIfNeeded()).not.toThrow()
expect(node.storedValue).toBeDefined()
expect(node.state).toBe(2)
})
})
if (process.env.NODE_ENV !== "production") {
describe("if the node is dead", () => {
it("does not work", () => {
const node = new ObjectNode(
TestModel as any,
null,
"",
{},
{ title: "hello" }
)
node.die()
expect(() => node.createObservableInstance()).toThrow(
"[mobx-state-tree] assertion failed: the creation of the observable instance must be done on the initializing phase"
)
})
})
}
})
describe("detach", () => {
describe("when the node is not alive", () => {
it("does throws an error", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.die()
expect(() => node.detach()).toThrow("Error while detaching, node is not alive.")
})
})
describe("when the node is alive and does not have a parent", () => {
it("does not throw an error", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(() => node.detach()).not.toThrow()
})
})
describe("when the node is alive and has a parent", () => {
it("detaches the node from the parent", () => {
const parent = Parent.create({ child: { title: "hello" } })
const child = parent.child
expect(child).toBeDefined()
// We can't directly modify the tree without unprotecting it first
unprotect(parent)
// The object node is made available through the $treenode property
child!.$treenode.detach()
expect(parent.child).toBeUndefined()
})
})
})
describe("die", () => {
describe("if the node is already dead", () => {
it("does nothing", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.die()
expect(() => node.die()).not.toThrow()
})
})
describe("if the node is detaching", () => {
it("does nothing", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.detach()
expect(() => node.die()).not.toThrow()
})
})
describe("if the node is unititalized", () => {
it("does not call the onAboutToDie hooks", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const hook = jest.fn()
node.registerHook(Hook.beforeDestroy, hook)
node.die()
expect(hook).not.toHaveBeenCalled()
})
})
describe("if the die method gets past lifecycle checks", () => {
it('calls the "aboutToDie" hook', () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const hook = jest.fn()
node.registerHook(Hook.beforeDestroy, hook)
node.createObservableInstance()
node.die()
expect(hook).toHaveBeenCalled()
})
it("finalizes the death of its children", () => {
const parent = Parent.create({ child: { title: "hello" } })
const child = parent.child
expect(child).toBeDefined()
// We can't directly modify the tree without unprotecting it first
unprotect(parent)
// The object node is made available through the $treenode property
parent.$treenode.die()
expect(child!.$treenode.state).toBe(4)
})
it("notifies the identifier cache that it has died", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const identifierCache = node.root.identifierCache
const identifierCacheNotifySpy = identifierCache
? spyOn(identifierCache, "notifyDied")
: jest.fn()
node.createObservableInstance()
node.die()
expect(identifierCacheNotifySpy).toHaveBeenCalledWith(node)
})
it("stores the snapshot upon death", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.createObservableInstance()
node.die()
expect(node.snapshot).toEqual({ title: "hello" })
})
it("sets the subpath upon death", () => {
const parent = Parent.create({ child: { title: "hello" } })
const child = parent.child
expect(child).toBeDefined()
// We can't directly modify the tree without unprotecting it first
unprotect(parent)
// The object node is made available through the $treenode property
parent.$treenode.die()
expect(child!.$treenode.subpathUponDeath).toBe("child")
})
it("sets the path upon death", () => {
const parent = Parent.create({ child: { title: "hello" } })
const child = parent.child
expect(child).toBeDefined()
// We can't directly modify the tree without unprotecting it first
unprotect(parent)
// The object node is made available through the $treenode property
parent.$treenode.die()
expect(child!.$treenode.pathUponDeath).toBe("/child")
})
it("sets its parent to null", () => {
const parent = Parent.create({ child: { title: "hello" } })
const child = parent.child
expect(child).toBeDefined()
// We can't directly modify the tree without unprotecting it first
unprotect(parent)
// The object node is made available through the $treenode property
parent.$treenode.die()
expect(child!.$treenode.parent).toBeNull()
})
it("sets the state to dead", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.createObservableInstance()
node.die()
expect(node.state).toBe(4)
})
})
})
describe("emitPatch", () => {
it("emits the patch and a reverse patch", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.createObservableInstance()
const patchMock = jest.fn()
onPatch(node.storedValue, patchMock)
node.emitPatch(
{
op: "replace",
path: "title",
value: "world",
oldValue: "hello"
},
node
)
expect(patchMock).toHaveBeenCalledWith(
{
op: "replace",
path: "/title",
value: "world"
},
{
op: "replace",
path: "/title",
value: "hello"
}
)
})
it("emits the patch and a reverse patch through its parent", () => {
const parent = Parent.create({ child: { title: "hello" } })
const patchMock = jest.fn()
onPatch(parent, patchMock)
parent.child!.$treenode.emitPatch(
{
op: "replace",
path: "title",
value: "world",
oldValue: "hello"
},
parent.child!.$treenode
)
expect(patchMock).toHaveBeenCalledWith(
{
op: "replace",
path: "/child/title",
value: "world"
},
{
op: "replace",
path: "/child/title",
value: "hello"
}
)
})
})
describe("finalizeCreation", () => {
if (process.env.NODE_ENV !== "production") {
describe("if the node is not alive", () => {
it("fails", () => {
const node = new ObjectNode(
TestModel as any,
null,
"",
{},
{ title: "hello" }
)
node.die()
expect(() => node.finalizeCreation()).toThrow(
"assertion failed: cannot finalize the creation of a node that is already dead"
)
})
})
}
describe("when a node has no parent", () => {
it("calls the afterCreationFinalization hook", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const hook = jest.fn()
node.registerHook(Hook.afterCreationFinalization, hook)
node.state = 1 // Force the state to CREATED so we don't bail out in the isAlive check as per the prior test
node.finalizeCreation()
expect(hook).toHaveBeenCalled()
})
it('sets the state to "FINALIZED"', () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.state = 1 // Force the state to CREATED so we don't bail out in the isAlive check as per the prior test
node.finalizeCreation()
// @ts-expect-error - We're testing the internal state here
expect(node.state).toBe(2)
})
})
describe("when the node has a parent", () => {
describe("but the parent is not yet finalized", () => {
it("does not call the afterAttach hook", () => {
const parent = new ObjectNode(Parent as any, null, "", {}, {})
const child = new ObjectNode(
TestModel as any,
parent,
"",
{},
{ title: "hello" }
)
const hook = jest.fn()
child.registerHook(Hook.afterCreationFinalization, hook)
child.setParent(parent, "child")
child.state = 1 // Force the state to CREATED in the child so we don't bail out in the isAlive check as per the prior test
child.finalizeCreation()
expect(hook).not.toHaveBeenCalled()
})
it('does not set the state to "FINALIZED"', () => {
const parent = new ObjectNode(Parent as any, null, "", {}, {})
const child = new ObjectNode(
TestModel as any,
parent,
"",
{},
{ title: "hello" }
)
child.setParent(parent, "child")
child.state = 1 // Force the state to CREATED in the child so we don't bail out in the isAlive check as per the prior test
child.finalizeCreation()
expect(child.state).toBe(1)
})
})
describe("and the parent is finalized", () => {
it("calls the afterAttach hook", () => {
const parent = new ObjectNode(Parent as any, null, "", {}, {})
const child = new ObjectNode(
TestModel as any,
parent,
"",
{},
{ title: "hello" }
)
const hook = jest.fn()
child.registerHook(Hook.afterCreationFinalization, hook)
child.setParent(parent, "child")
child.state = 1 // Force the state to CREATED in the child so we don't bail out in the isAlive check as per the prior test
parent.state = 2 // Force the state to FINALIZED so we don't bail out during the baseFinalizeCreation on child
child.finalizeCreation()
expect(hook).toHaveBeenCalled()
})
it('sets the state to "FINALIZED"', () => {
const parent = new ObjectNode(Parent as any, null, "", {}, {})
const child = new ObjectNode(
TestModel as any,
parent,
"",
{},
{ title: "hello" }
)
child.setParent(parent, "child")
child.state = 1 // Force the state to CREATED in the child so we don't bail out in the isAlive check as per the prior test
parent.state = 2 // Force the state to FINALIZED so we don't bail out during the baseFinalizeCreation on child
child.finalizeCreation()
// @ts-expect-error - We're testing the internal state here
expect(child.state).toBe(2)
})
})
})
describe("when the node has children", () => {
it("fires the finalizeCreation hook on the parent", () => {
const env = {}
const child = new ObjectNode(TestModel as any, null, "", env, {
title: "hello"
})
const parent = new ObjectNode(Parent as any, null, "", env, {
child: child.storedValue
})
child.setParent(parent, "child")
const hook = jest.fn()
parent.registerHook(Hook.afterCreationFinalization, hook)
child.state = 1
expect(hook).not.toHaveBeenCalled()
parent.state = 1 // Force the state to CREATED so we don't bail out in the isAlive check as per the prior test
parent.finalizeCreation()
expect(hook).toHaveBeenCalled()
})
it("fires the afterAttach hook on the child", () => {
const env = {}
const child = new ObjectNode(TestModel as any, null, "", env, {
title: "hello"
})
const parent = new ObjectNode(Parent as any, null, "", env, {
child: child.storedValue
})
child.setParent(parent, "child")
const c = parent.getChildNode("child")
const hook = jest.fn()
c.registerHook(Hook.afterAttach, hook)
c.state = 1
expect(hook).not.toHaveBeenCalled()
parent.state = 1 // Force the state to CREATED so we don't bail out in the isAlive check as per the prior test
parent.finalizeCreation()
expect(hook).toHaveBeenCalled()
})
it("sets both child and parent states to finalized", () => {
const env = {}
const child = new ObjectNode(TestModel as any, null, "", env, {
title: "hello"
})
const parent = new ObjectNode(Parent as any, null, "", env, {
child: child.storedValue
})
child.setParent(parent, "child")
const c = parent.getChildNode("child")
c.state = 1
parent.state = 1 // Force the state to CREATED so we don't bail out in the isAlive check as per the prior test
parent.finalizeCreation()
// @ts-expect-error - We're testing the internal state here
expect(c.state).toBe(2)
// @ts-expect-error - We're testing the internal state here
expect(parent.state).toBe(2)
})
})
})
describe("finalizeDeath", () => {
it("does everything die() does without calling aboutToDie()", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const hook = jest.fn()
node.registerHook(Hook.beforeDestroy, hook)
node.createObservableInstance()
node.finalizeDeath()
expect(hook).not.toHaveBeenCalled()
expect(node.state).toBe(4)
})
})
describe("getChildNode", () => {
if (process.env.NODE_ENV !== "production") {
describe("when the node is not alive", () => {
it("fails", () => {
const warnSpy = spyOn(console, "warn")
const node = new ObjectNode(
TestModel as any,
null,
"",
{},
{ title: "hello" }
)
node.die()
node.getChildNode("title")
const receivedErrorMessage = warnSpy.mock.calls[0][0].toString()
expect(receivedErrorMessage).toBe(
"Error: [mobx-state-tree] You are trying to read or write to an object that is no longer part of a state tree. (Object type: 'TestModel', Path upon death: '', Subpath: 'title', Action: ''). Either detach nodes first, or don't use objects after removing / replacing them in the tree."
)
})
})
}
describe("when the node is alive", () => {
it("returns the child node", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.createObservableInstance()
const childNode = node.getChildNode("title")
expect(childNode).toBeDefined()
expect(childNode.storedValue).toBe("hello")
})
})
})
describe("getChildType", () => {
it("returns the child type", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const childType = node.getChildType("title")
expect(childType).toBe(t.string)
})
})
describe("getChildren", () => {
describe("when the node is not alive", () => {
it("fails", () => {
const warnSpy = spyOn(console, "warn")
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.die()
node.getChildren()
const receivedErrorMessage = warnSpy.mock.calls[0][0].toString()
expect(receivedErrorMessage).toBe(
"Error: [mobx-state-tree] You are trying to read or write to an object that is no longer part of a state tree. (Object type: 'TestModel', Path upon death: '', Subpath: '', Action: ''). Either detach nodes first, or don't use objects after removing / replacing them in the tree."
)
})
})
describe("when the node is alive and has children", () => {
it("returns an array of children", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.createObservableInstance()
const children = node.getChildren()
expect(children).toBeDefined()
expect(children.length).toBe(1)
expect(children[0].storedValue).toBe("hello")
})
})
})
describe("getReconciliationType", () => {
it("returns the complext type used to instantiate the node", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.getReconciliationType()).toBe(TestModel)
})
})
describe("getSnapshot", () => {
it("returns the snapshot", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.getSnapshot()).toEqual({ title: "hello" })
})
})
describe("hasDisposer", () => {
describe("when there are no disposers", () => {
it("returns false", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.hasDisposer(() => {})).toBe(false)
})
})
describe("when there are disposers", () => {
it("returns true", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const disposer = () => {}
node.addDisposer(disposer)
expect(node.hasDisposer(disposer)).toBe(true)
})
})
})
describe("onPatch", () => {
it("registers the patch listener", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const listener = jest.fn()
node.onPatch(listener)
node.createObservableInstance()
node.applyPatches([{ op: "replace", path: "/title", value: "world" }])
expect(listener).toHaveBeenCalledWith(
{
op: "replace",
path: "/title",
value: "world"
},
{
op: "replace",
path: "/title",
value: "hello"
}
)
})
})
describe("onSnapshot", () => {
it("registers the snapshot listener", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const listener = jest.fn()
node.onSnapshot(listener)
node.createObservableInstance()
node.applySnapshot({ title: "world" })
expect(listener).toHaveBeenCalledWith({ title: "world" })
})
})
describe("registerHook", () => {
describe("afterCreate", () => {
it("works", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const hook = jest.fn()
node.registerHook(Hook.afterCreate, hook)
// We call afterCreate during observable instance creation
node.createObservableInstance()
expect(hook).toHaveBeenCalled()
})
})
describe("afterAttach", () => {
describe("for a root node", () => {
it("does not get called", () => {
const node = new ObjectNode(
TestModel as any,
null,
"",
{},
{ title: "hello" }
)
const hook = jest.fn()
node.registerHook(Hook.afterAttach, hook)
// We call afterAttach during observable instance creation
node.createObservableInstance()
expect(hook).not.toHaveBeenCalled()
})
})
describe("for a non-root node", () => {
it("gets called", () => {
const env = {}
const child = new ObjectNode(TestModel as any, null, "", env, {
title: "hello"
})
const parent = new ObjectNode(Parent as any, null, "", env, {
child: child.storedValue
})
child.setParent(parent, "child")
const c = parent.getChildNode("child")
const hook = jest.fn()
c.registerHook(Hook.afterAttach, hook)
parent.createObservableInstance()
expect(hook).toHaveBeenCalled()
})
})
})
describe("afterCreationFinalization", () => {
it("works", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const hook = jest.fn()
node.registerHook(Hook.afterCreationFinalization, hook)
// We call afterCreationFinalization during observable instance creation
node.createObservableInstance()
expect(hook).toHaveBeenCalled()
})
})
describe("beforeDetach", () => {
describe("for a root node", () => {
it("does not get called", () => {
const node = new ObjectNode(
TestModel as any,
null,
"",
{},
{ title: "hello" }
)
const hook = jest.fn()
node.registerHook(Hook.beforeDetach, hook)
node.createObservableInstance()
node.detach()
expect(hook).not.toHaveBeenCalled()
})
})
describe("for a non-root node", () => {
it("gets called", () => {
const env = {}
const child = new ObjectNode(TestModel as any, null, "", env, {
title: "hello"
})
const parent = new ObjectNode(Parent as any, null, "", env, {
child: child.storedValue
})
child.setParent(parent, "child")
const hook = jest.fn()
child.registerHook(Hook.beforeDetach, hook)
parent.createObservableInstance()
unprotect(parent.storedValue) // In order to detach a child node directly, we need to unprotect the root here
child.detach()
expect(hook).toHaveBeenCalled()
})
})
})
describe("beforeDestroy", () => {
it("works", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const hook = jest.fn()
node.registerHook(Hook.beforeDestroy, hook)
// We need to create an observable instance before we can destroy it and get the hook to fire
node.createObservableInstance()
node.die()
expect(hook).toHaveBeenCalled()
})
})
})
describe("removeChild", () => {
describe("for models", () => {
it("removes the child by path", () => {
const parent = Parent.create({ child: { title: "hello" } })
const child = parent.child
expect(child).toBeDefined()
// We can't directly modify the tree without unprotecting it first
unprotect(parent)
// The object node is made available through the $treenode property
parent.$treenode.removeChild("child")
expect(parent.child).toBeUndefined()
})
})
describe("for arrays", () => {
it("removes the child by index", () => {
const parent = TestArray.create([{ title: "hello" }])
expect(parent[0]).toBeDefined()
// We can't directly modify the tree without unprotecting it first
unprotect(parent)
// The object node is made available through the $treenode property
parent.$treenode.removeChild(0)
expect(parent[0]).toBeUndefined()
})
})
describe("for maps", () => {
it("removes the child by key", () => {
const parent = TestMap.create({ hello: { title: "hello" } })
expect(parent.get("hello")).toBeDefined()
// We can't directly modify the tree without unprotecting it first
unprotect(parent)
// The object node is made available through the $treenode property
parent.$treenode.removeChild("hello")
expect(parent.get("hello")).toBeUndefined()
})
})
})
describe("removeDisposer", () => {
describe("when a disposer does not exist", () => {
it("throws an error", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(() => node.removeDisposer(() => {})).toThrow(
"[mobx-state-tree] cannot remove a disposer which was never registered for execution"
)
})
})
describe("when a disposer exists", () => {
it("removes the disposer", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const disposer = () => {}
node.addDisposer(disposer)
node.removeDisposer(disposer)
expect(node.hasDisposer(disposer)).toBe(false)
})
})
})
describe("setParent", () => {
describe("if the parent and subpath are unchanged", () => {
it("does nothing", () => {
const parent = new ObjectNode(Parent as any, null, "", {}, {})
const node = new ObjectNode(
TestModel as any,
parent,
"child",
{},
{ title: "hello" }
)
node.setParent(parent, "child")
expect(node.parent).toBe(parent)
expect(node.subpath).toBe("child")
})
})
if (process.env.NODE_ENV !== "production") {
describe("if there is no subpath", () => {
it("throws an error", () => {
const parent = new ObjectNode(Parent as any, null, "", {}, {})
const node = new ObjectNode(
TestModel as any,
parent,
"child",
{},
{ title: "hello" }
)
expect(() => node.setParent(parent, "")).toThrow(
"[mobx-state-tree] assertion failed: subpath expected"
)
})
})
describe("if there is no new parent", () => {
it("throws an error", () => {
const parent = new ObjectNode(Parent as any, null, "", {}, {})
const node = new ObjectNode(
TestModel as any,
parent,
"child",
{},
{ title: "hello" }
)
expect(() => node.setParent(null as any, "child")).toThrow(
"[mobx-state-tree] assertion failed: new parent expected"
)
})
})
describe("if the node already has a parent", () => {
it("throws an error", () => {
const parent = new ObjectNode(Parent as any, null, "", {}, {})
const node = new ObjectNode(
TestModel as any,
parent,
"child",
{},
{ title: "hello" }
)
const newParent = new ObjectNode(Parent as any, null, "", {}, {})
expect(() => node.setParent(newParent, "child")).toThrow(
"[mobx-state-tree] A node cannot exists twice in the state tree. Failed to add TestModel@/child to path '/child'."
)
})
})
describe("if the parent is made to be itself", () => {
it("throws an error", () => {
const node = new ObjectNode(
TestModel as any,
null,
"",
{},
{ title: "hello" }
)
expect(() => node.setParent(node, "child")).toThrow(
"[mobx-state-tree] A state tree is not allowed to contain itself. Cannot assign TestModel@ to path '/child'"
)
})
})
describe("if the parent exists in another state tree in a different environment", () => {
it("throws an error", () => {
const env1 = {}
const env2 = {}
const node = new ObjectNode(TestModel as any, null, "", env1, {
title: "hello"
})
const parent = new ObjectNode(Parent as any, null, "", env2, {})
expect(() => node.setParent(parent, "child")).toThrow(
"[mobx-state-tree] A state tree cannot be made part of another state tree as long as their environments are different."
)
})
})
}
describe("if the parent is different", () => {
it("gives the node a new parent", () => {
const env = {}
const parent = new ObjectNode(Parent as any, null, "", env, {})
const node = new ObjectNode(TestModel as any, null, "", env, { title: "hello" })
node.setParent(parent, "child")
expect(node.parent).toBe(parent)
})
it("fires the afterAttach hook", () => {
const env = {}
const parent = new ObjectNode(Parent as any, null, "", env, {})
const node = new ObjectNode(TestModel as any, null, "", env, { title: "hello" })
const hook = jest.fn()
node.registerHook(Hook.afterAttach, hook)
node.setParent(parent, "child")
expect(hook).toHaveBeenCalled()
})
})
describe("if the parent is the same but the subpath has changed", () => {
it("gives the node a new subpath", () => {
const env = {}
const parent = new ObjectNode(Parent as any, null, "", env, {})
const node = new ObjectNode(TestModel as any, parent, "", env, {
title: "hello"
})
node.setParent(parent, "child")
expect(node.subpath).toBe("child")
})
})
})
describe("toString", () => {
it("returns a string representation of the node", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.toString()).toBe("TestModel@")
})
})
describe("unbox", () => {
// This was probably intended to be used with `null` or `undefined`, but the implementation just checks for falsy values.
describe("when given some falsy value", () => {
it("returns the value", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.unbox(undefined)).toBeUndefined()
// @ts-expect-error - we're testing the behavior of unbox with non-undefined values
expect(node.unbox(null as any)).toBe(null)
// @ts-expect-error - we're testing the behavior of unbox with non-undefined values
expect(node.unbox(false as any)).toBe(false)
// @ts-expect-error - we're testing the behavior of unbox with non-undefined values
expect(node.unbox(0 as any)).toBe(0)
})
})
describe("when given a child node", () => {
it("gives back the value", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const childNode = node.getChildNode("title")
// @ts-expect-error - we're testing the behavior of unbox with non-undefined values
expect(node.unbox(childNode)).toBe("hello")
})
})
})
})
describe("properties", () => {
describe("_isRunningAciton", () => {
// The only time we ever set this to _true is during the operation sin createObservableInstance, so we don't have a great way to test it. For now, just test default value.
it("is false by default", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node._isRunningAction).toBe(false)
})
})
describe("environmment", () => {
it("returns the environment", () => {
const env = {}
const node = new ObjectNode(TestModel as any, null, "", env, { title: "hello" })
expect(node.environment).toBe(env)
})
it("matches by reference", () => {
const env = {}
// It's using the reference to the object, not just the value of an "empty" object
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.environment).not.toBe(env)
})
})
describe("hasSnapshotPostProcessor", () => {
it("returns false by default", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.hasSnapshotPostProcessor).toBe(false)
})
it("returns false if there is a pre processor and no post processor", () => {
const NewType = t.model("NewType", {
title: t.string
})
const NewType2 = t.snapshotProcessor(NewType, {
preProcessor(sn: any) {
return { title: sn.title.toUpperCase() }
}
})
const instance = NewType2.create({ title: "hello" })
const node = instance.$treenode
expect(node.hasSnapshotPostProcessor).toBe(false)
})
it("returns true if there is a post processor", () => {
const NewType = t.model("NewType", {
title: t.string
})
const NewType2 = t.snapshotProcessor(NewType, {
postProcessor(sn: any, node: any) {
return { title: sn.title.toUpperCase() }
}
})
const instance = NewType2.create({ title: "hello" })
const node = instance.$treenode
expect(node.hasSnapshotPostProcessor).toBe(true)
})
it("returns true if there is a pre processor and a post processor", () => {
const NewType = t.model("NewType", {
title: t.string
})
const NewType2 = t.snapshotProcessor(NewType, {
preProcessor(sn: any) {
return { title: sn.title.toUpperCase() }
},
postProcessor(sn: any, node: any) {
return { title: sn.title.toUpperCase() }
}
})
const instance = NewType2.create({ title: "hello" })
const node = instance.$treenode
expect(node.hasSnapshotPostProcessor).toBe(true)
})
})
describe("identifier", () => {
describe("when the type has no identifier", () => {
it("returns null", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.identifier).toBe(null)
})
})
describe("when the type has an identifier", () => {
it("returns the identifier", () => {
const node = new ObjectNode(
TestModelWithIdentifier as any,
null,
"",
{},
{ id: "1234", title: "hello" }
)
expect(node.identifier).toBe("1234")
})
})
})
describe("when the type does not have an identifier", () => {
it("returns undefined", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.identifierAttribute).toBeUndefined()
})
})
describe("when the type has an identifier", () => {
it("returns the identifier", () => {
const node = new ObjectNode(
TestModelWithIdentifier as any,
null,
"",
{},
{ id: "1234", title: "hello" }
)
expect(node.identifierAttribute).toBe("id")
})
})
describe("identifierCache", () => {
describe("if the node is the root", () => {
it("exists", () => {
const Parent = t.model("Parent", {
child: TestModel
})
const parent = Parent.create({ child: { title: "hello" } })
expect(parent.$treenode.identifierCache).toBeDefined()
})
it("keeps track of ids", () => {
const Parent = t.model("Parent", {
child: TestModelWithIdentifier
})
const parent = Parent.create({ child: { id: "1234", title: "hello" } })
const identifierCache = parent.$treenode.identifierCache
expect(identifierCache.has(TestModelWithIdentifier, "1234")).toBe(true)
expect(identifierCache.has(TestModelWithIdentifier, "aaa")).toBe(false)
})
})
describe("if the node is not the root", () => {
it("is undefined", () => {
const Parent = t.model("Parent", {
child: TestModel
})
const parent = Parent.create({ child: { title: "hello" } })
expect(parent.child.$treenode.identifierCache).toBeUndefined()
})
})
})
describe("isAlive", () => {
describe("when the node is dead", () => {
it("returns false", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.die()
expect(node.isAlive).toBe(false)
})
})
describe("when the node is initializing, created, finalized, or detaching", () => {
const testCases = [0, 1, 2, 3]
testCases.forEach(state => {
it(`returns true when the state is ${state}`, () => {
const node = new ObjectNode(
TestModel as any,
null,
"",
{},
{ title: "hello" }
)
node.state = state
expect(node.isAlive).toBe(true)
})
})
})
})
describe("isDetaching", () => {
describe("when the node is detaching", () => {
it("returns true", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.state = 3
expect(node.isDetaching).toBe(true)
})
})
describe("when the node is in any other state", () => {
const testCases = [0, 1, 2, 4]
testCases.forEach(state => {
it(`returns false when the state is ${state}`, () => {
const node = new ObjectNode(
TestModel as any,
null,
"",
{},
{ title: "hello" }
)
node.state = state
expect(node.isDetaching).toBe(false)
})
})
})
})
describe("isProtected", () => {
it("returns true by default", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.isProtected).toBe(true)
})
it("returns false if the node is unprotected", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.createObservableInstance()
unprotect(node.storedValue)
expect(node.isProtected).toBe(false)
})
})
describe("isRoot", () => {
it("returns true by default", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.isRoot).toBe(true)
})
it("returns false when it is made a child of another node", () => {
const env = {}
const parent = new ObjectNode(Parent as any, null, "", env, {})
const node = new ObjectNode(TestModel as any, null, "", env, { title: "hello" })
node.setParent(parent, "child")
expect(node.isRoot).toBe(false)
})
})
describe("middlewares", () => {
it("returns no middlewares by default", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.middlewares).toBeUndefined()
})
it("returns the middlewares", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const middleware = jest.fn()
node.addMiddleWare(middleware)
expect(node.middlewares).toBeDefined()
})
})
describe("nodeId", () => {
it("increments every time a node is created", () => {
const node1 = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
const node2 = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node1.nodeId).not.toBe(node2.nodeId)
})
})
/**
* observableIsAlive will follow the isAlive patterns,
* it just has a side effect of reporting observation when called
*/
describe("observableIsAlive", () => {
describe("when the node is dead", () => {
it("returns false", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.die()
expect(node.observableIsAlive).toBe(false)
})
})
describe("when the node is initializing, created, finalized, or detaching", () => {
const testCases = [0, 1, 2, 3]
testCases.forEach(state => {
it(`returns true when the state is ${state}`, () => {
const node = new ObjectNode(
TestModel as any,
null,
"",
{},
{ title: "hello" }
)
node.state = state
expect(node.observableIsAlive).toBe(true)
})
})
})
})
describe("parent", () => {
it("returns the parent node", () => {
const env = {}
const parent = new ObjectNode(Parent as any, null, "", env, {})
const node = new ObjectNode(TestModel as any, parent, "", env, { title: "hello" })
expect(node.parent).toBe(parent)
})
it("returns null when there is no parent", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.parent).toBe(null)
})
})
describe("path", () => {
it("returns the provided path", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.path).toBe("")
})
})
describe("root", () => {
it("returns the node when it is the root", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.root).toBe(node)
})
it("returns the root node when it is not the root", () => {
const env = {}
const parent = new ObjectNode(Parent as any, null, "", env, {})
const node = new ObjectNode(TestModel as any, parent, "", env, { title: "hello" })
expect(node.root).toBe(parent)
})
})
describe("snapshot", () => {
it("returns the snapshot", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.snapshot).toEqual({ title: "hello" })
})
})
describe("state", () => {
// We implicitly test this in many ways through the rest of the spec, so I've just left a simple one here.
it("works", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.state).toBe(0)
})
})
describe("storedValue", () => {
it("is undefined before the observable instance is created", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.storedValue).toBeUndefined()
})
it('is the "value" of the observable instance after it is created', () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
node.createObservableInstance()
expect(node.storedValue).toBeDefined()
// @ts-ignore
expect(node.storedValue.title).toBe("hello")
})
})
describe("subpath", () => {
it('returns "" when the node is the root', () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.subpath).toBe("")
})
it("returns the subpath when the node is not the root", () => {
const env = {}
const parent = new ObjectNode(Parent as any, null, "", env, {})
const node = new ObjectNode(TestModel as any, parent, "child", env, {
title: "hello"
})
expect(node.subpath).toBe("child")
})
})
describe("subpathUponDeath", () => {
it("remembers the subpath upon death", () => {
const env = {}
const parent = new ObjectNode(Parent as any, null, "", env, {})
const node = new ObjectNode(TestModel as any, parent, "child", env, {
title: "hello"
})
node.die()
expect(node.subpathUponDeath).toBe("child")
parent.die()
expect(parent.subpathUponDeath).toBe("")
})
})
describe("type", () => {
it("returns the given type of the objectnode", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
expect(node.type).toBe(TestModel as any)
})
})
describe("value", () => {
it("returns the value as returned by the given type", () => {
const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" })
// @ts-ignore
expect(node.value.title).toBe("hello")
})
})
})
})
================================================
FILE: __tests__/core/object.test.ts
================================================
import {
destroy,
detach,
onSnapshot,
onPatch,
onAction,
applyPatch,
applyAction,
applySnapshot,
getSnapshot,
unprotect,
types,
setLivelinessChecking,
getParent,
SnapshotOut,
IJsonPatch,
ISerializedActionCall,
isAlive,
cast,
resolveIdentifier
} from "../../src"
import { autorun, reaction, observable, configure, getDebugName } from "mobx"
import { MstError } from "../../src/internal"
import { expect, test } from "bun:test"
const createTestFactories = () => {
const Factory = types
.model({
to: "world"
})
.actions(self => {
function setTo(to: string) {
self.to = to
}
return {
setTo
}
})
const ComputedFactory = types
.model({
width: 100,
height: 200
})
.views(self => ({
get area() {
return self.width * self.height
}
}))
const ComputedFactory2 = types
.model({
props: types.map(types.number)
})
.views(self => ({
get area() {
return self.props.get("width")! * self.props.get("height")!
}
}))
.actions(self => {
function setWidth(value: number) {
self.props.set("width", value)
}
function setHeight(value: number) {
self.props.set("height", value)
}
return {
setWidth,
setHeight
}
})
const BoxFactory = types.model({
width: 0,
height: 0
})
const ColorFactory = types.model({
color: "#FFFFFF"
})
return { Factory, ComputedFactory, ComputedFactory2, BoxFactory, ColorFactory }
}
const createFactoryWithChildren = () => {
const File = types
.model("File", {
name: types.string
})
.actions(self => ({
rename(value: string) {
self.name = value
}
}))
const Folder = types
.model("Folder", {
name: types.string,
files: types.array(File)
})
.actions(self => ({
rename(value: string) {
self.name = value
}
}))
return Folder
}
// === FACTORY TESTS ===
test("it should create a factory", () => {
const { Factory } = createTestFactories()
const instance = Factory.create()
const snapshot = getSnapshot(instance)
expect(snapshot).toEqual({ to: "world" })
expect(getSnapshot(Factory.create())).toEqual({ to: "world" }) // toJSON is there as shortcut for getSnapshot(), primarily for debugging convenience
expect(Factory.create().toString()).toEqual("AnonymousModel@")
})
test("it should restore the state from the snapshot", () => {
const { Factory } = createTestFactories()
expect(getSnapshot(Factory.create({ to: "universe" }))).toEqual({ to: "universe" })
})
// === SNAPSHOT TESTS ===
test("it should emit snapshots", () => {
const { Factory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
let snapshots: SnapshotOut[] = []
onSnapshot(doc, snapshot => snapshots.push(snapshot))
doc.to = "universe"
expect(snapshots).toEqual([{ to: "universe" }])
})
test("it should emit snapshots for children", () => {
const Factory = createFactoryWithChildren()
const folder = Factory.create({
name: "Photos to sort",
files: [
{
name: "Photo1"
},
{
name: "Photo2"
}
]
})
let snapshotsP: SnapshotOut[] = []
let snapshotsC: SnapshotOut<(typeof folder.files)[0]>[] = []
onSnapshot(folder, snapshot => snapshotsP.push(snapshot))
folder.rename("Vacation photos")
expect(snapshotsP[0]).toEqual({
name: "Vacation photos",
files: [{ name: "Photo1" }, { name: "Photo2" }]
})
onSnapshot(folder.files[0], snapshot => snapshotsC.push(snapshot))
folder.files[0].rename("01-arrival")
expect(snapshotsP[1]).toEqual({
name: "Vacation photos",
files: [{ name: "01-arrival" }, { name: "Photo2" }]
})
expect(snapshotsC[0]).toEqual({ name: "01-arrival" })
folder.files[1].rename("02-hotel")
expect(snapshotsP[2]).toEqual({
name: "Vacation photos",
files: [{ name: "01-arrival" }, { name: "02-hotel" }]
})
expect(snapshotsP.length).toBe(3)
expect(snapshotsC.length).toBe(1)
})
test("it should apply snapshots", () => {
const { Factory } = createTestFactories()
const doc = Factory.create()
applySnapshot(doc, { to: "universe" })
expect(getSnapshot(doc)).toEqual({ to: "universe" })
})
test("it should apply and accept null value for types.maybe(complexType)", () => {
const Item = types.model("Item", {
value: types.string
})
const Model = types.model("Model", {
item: types.maybe(Item)
})
const myModel = Model.create()
applySnapshot(myModel, { item: { value: "something" } })
applySnapshot(myModel, { item: undefined })
expect(getSnapshot(myModel)).toEqual({ item: undefined })
})
test("it should apply and accept null value for types.maybeNull(complexType)", () => {
const Item = types.model("Item", {
value: types.string
})
const Model = types.model("Model", {
item: types.maybeNull(Item)
})
const myModel = Model.create()
applySnapshot(myModel, { item: { value: "something" } })
applySnapshot(myModel, { item: null })
expect(getSnapshot(myModel)).toEqual({ item: null })
})
test("it should return a snapshot", () => {
const { Factory } = createTestFactories()
const doc = Factory.create()
expect(getSnapshot(doc)).toEqual({ to: "world" })
})
// === PATCHES TESTS ===
test("it should emit patches", () => {
const { Factory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
let patches: IJsonPatch[] = []
onPatch(doc, patch => patches.push(patch))
doc.to = "universe"
expect(patches).toEqual([{ op: "replace", path: "/to", value: "universe" }])
})
test("it should apply a patch", () => {
const { Factory } = createTestFactories()
const doc = Factory.create()
applyPatch(doc, { op: "replace", path: "/to", value: "universe" })
expect(getSnapshot(doc)).toEqual({ to: "universe" })
})
test("it should apply patches", () => {
const { Factory } = createTestFactories()
const doc = Factory.create()
applyPatch(doc, [
{ op: "replace", path: "/to", value: "mars" },
{ op: "replace", path: "/to", value: "universe" }
])
expect(getSnapshot(doc)).toEqual({ to: "universe" })
})
test("it should stop listening to patches patches", () => {
const { Factory } = createTestFactories()
const doc = Factory.create()
unprotect(doc)
let patches: IJsonPatch[] = []
let disposer = onPatch(doc, patch => patches.push(patch))
doc.to = "universe"
disposer()
doc.to = "mweststrate"
expect(patches).toEqual([{ op: "replace", path: "/to", value: "universe" }])
})
// === ACTIONS TESTS ===
test("it should call actions correctly", () => {
const { Factory } = createTestFactories()
const doc = Factory.create()
doc.setTo("universe")
expect(getSnapshot(doc)).toEqual({ to: "universe" })
})
test("it should emit action calls", () => {
const { Factory } = createTestFactories()
const doc = Factory.create()
let actions: ISerializedActionCall[] = []
onAction(doc, action => actions.push(action))
doc.setTo("universe")
expect(actions).toEqual([{ name: "setTo", path: "", args: ["universe"] }])
})
test("it should apply action call", () => {
const { Factory } = createTestFactories()
const doc = Factory.create()
applyAction(doc, { name: "setTo", path: "", args: ["universe"] })
expect(getSnapshot(doc)).toEqual({ to: "universe" })
})
test("it should apply actions calls", () => {
const { Factory } = createTestFactories()
const doc = Factory.create()
applyAction(doc, [
{ name: "setTo", path: "", args: ["mars"] },
{ name: "setTo", path: "", args: ["universe"] }
])
expect(getSnapshot(doc)).toEqual({ to: "universe" })
})
// === COMPUTED VALUES ===
test("it should have computed properties", () => {
const { ComputedFactory } = createTestFactories()
const doc = ComputedFactory.create()
unprotect(doc)
doc.width = 3
doc.height = 2
expect(doc.area).toEqual(6)
})
test("it should throw if a replaced object is read or written to", () => {
const Todo = types
.model("Todo", {
title: "test",
arr: types.array(types.string),
map: types.map(types.string),
sub: types.optional(
types
.model("Sub", {
title: "test2"
})
.actions(self => ({
fn2() {}
})),
{}
)
})
.actions(self => ({
fn() {
self.sub.fn2()
}
}))
const Store = types.model("Store", {
todo: Todo
})
const data = {
title: "alive",
arr: ["arr0"],
map: { mapkey0: "mapval0" },
sub: { title: "title" }
}
const s = Store.create({ todo: { ...data, title: "dead" } })
unprotect(s)
const deadArr = s.todo.arr
s.todo.arr = cast(data.arr)
const deadMap = s.todo.map
s.todo.map = cast(data.map)
const deadSub = s.todo.sub
s.todo.sub = cast(data.sub)
const deadTodo = s.todo
s.todo = Todo.create(data)
expect(s.todo.title).toBe("alive")
setLivelinessChecking("error")
function getError(obj: any, path: string, subpath: string, action: string) {
return `You are trying to read or write to an object that is no longer part of a state tree. (Object type: '${getDebugName(
obj
)}', Path upon death: '${path}', Subpath: '${subpath}', Action: '${action}'). Either detach nodes first, or don't use objects after removing / replacing them in the tree.`
}
// dead todo
expect(() => {
deadTodo.fn()
}).toThrow(getError(deadTodo, "/todo", "", "/todo.fn()"))
expect(() => {
// tslint:disable-next-line:no-unused-expression
deadTodo.title
}).toThrow(getError(deadTodo, "/todo", "title", ""))
expect(() => {
deadTodo.title = "5"
}).toThrow(getError(deadTodo, "/todo", "title", ""))
expect(() => {
// tslint:disable-next-line:no-unused-expression
deadTodo.arr[0]
}).toThrow(getError(deadTodo, "/todo", "arr", ""))
expect(() => {
deadTodo.arr.push("arr1")
}).toThrow(getError(deadTodo, "/todo", "arr", ""))
expect(() => {
deadTodo.map.get("mapkey0")
}).toThrow(getError(deadTodo, "/todo", "map", ""))
expect(() => {
deadTodo.map.set("mapkey1", "val")
}).toThrow(getError(deadTodo, "/todo", "map", ""))
expect(() => {
deadTodo.sub.fn2()
}).toThrow(getError(deadTodo, "/todo", "sub", ""))
expect(() => {
// tslint:disable-next-line:no-unused-expression
deadTodo.sub.title
}).toThrow(getError(deadTodo, "/todo", "sub", ""))
expect(() => {
deadTodo.sub.title = "hi"
}).toThrow(getError(deadTodo, "/todo", "sub", ""))
// dead array
expect(() => {
// tslint:disable-next-line:no-unused-expression
deadArr[0]
}).toThrow(getError(deadArr, "/todo/arr", "0", ""))
expect(() => {
deadArr[0] = "hi"
}).toThrow(getError(deadArr, "/todo/arr", "0", ""))
expect(() => {
deadArr.push("hi")
}).toThrow(getError(deadArr, "/todo/arr", "1", ""))
// dead map
expect(() => {
deadMap.get("mapkey0")
}).toThrow(getError(deadMap, "/todo/map", "mapkey0", ""))
expect(() => {
deadMap.set("mapkey0", "val")
}).toThrow(getError(deadMap, "/todo/map", "mapkey0", ""))
// dead subobj
expect(() => {
deadSub.fn2()
}).toThrow(getError(deadSub, "/todo/sub", "", "/todo/sub.fn2()"))
expect(() => {
// tslint:disable-next-line:no-unused-expression
deadSub.title
}).toThrow(getError(deadSub, "/todo/sub", "title", ""))
expect(() => {
deadSub.title = "ho"
}).toThrow(getError(deadSub, "/todo/sub", "title", ""))
})
test("it should warn if a replaced object is read or written to", () => {
const Todo = types
.model("Todo", {
title: "test"
})
.actions(self => {
function fn() {}
return {
fn
}
})
const Store = types.model("Store", {
todo: Todo
})
const s = Store.create({
todo: { title: "3" }
})
unprotect(s)
const todo = s.todo
s.todo = Todo.create({ title: "4" })
expect(s.todo.title).toBe("4")
// try reading old todo
setLivelinessChecking("error")
const error =
"You are trying to read or write to an object that is no longer part of a state tree"
expect(() => todo.fn()).toThrow(error)
expect(() => todo.title).toThrow(error)
unprotect(todo)
expect(() => {
todo.title = "5"
}).toThrow(error)
})
// === COMPOSE FACTORY ===
test("it should compose factories", () => {
const { BoxFactory, ColorFactory } = createTestFactories()
const ComposedFactory = types.compose(BoxFactory, ColorFactory)
expect(getSnapshot(ComposedFactory.create())).toEqual({ width: 0, height: 0, color: "#FFFFFF" })
})
test("it should compose factories with computed properties", () => {
const { ComputedFactory2, ColorFactory } = createTestFactories()
const ComposedFactory = types.compose(ColorFactory, ComputedFactory2)
const store = ComposedFactory.create({ props: { width: 100, height: 200 } })
expect(getSnapshot(store)).toEqual({ props: { width: 100, height: 200 }, color: "#FFFFFF" })
expect(store.area).toBe(20000)
expect(typeof store.setWidth).toBe("function")
expect(typeof store.setHeight).toBe("function")
})
test("it should compose multiple types with computed properties", () => {
const { ComputedFactory2, ColorFactory } = createTestFactories()
const ComposedFactory = types.compose(ColorFactory, ComputedFactory2)
const store = ComposedFactory.create({ props: { width: 100, height: 200 } })
expect(getSnapshot(store)).toEqual({ props: { width: 100, height: 200 }, color: "#FFFFFF" })
expect(store.area).toBe(20000)
expect(typeof store.setWidth).toBe("function")
expect(typeof store.setHeight).toBe("function")
})
test("methods get overridden by compose", () => {
const A = types
.model({
count: types.optional(types.number, 0)
})
.actions(self => {
function increment() {
self.count += 1
}
return {
increment
}
})
const B = A.actions(self => ({
increment() {
self.count += 10
}
}))
const store = B.create()
expect(getSnapshot(store)).toEqual({ count: 0 })
expect(store.count).toBe(0)
store.increment()
expect(store.count).toBe(10)
})
test("compose should add new props", () => {
const A = types.model({
count: types.optional(types.number, 0)
})
const B = A.props({
called: types.optional(types.number, 0)
})
const store = B.create()
expect(getSnapshot(store)).toEqual({ count: 0, called: 0 })
expect(store.count).toBe(0)
})
test("models should expose their actions to be used in a composable way", () => {
const A = types
.model({
count: types.optional(types.number, 0)
})
.actions(self => {
function increment() {
self.count += 1
}
return {
increment
}
})
const B = A.props({
called: types.optional(types.number, 0)
}).actions(self => {
const baseIncrement = self.increment
return {
increment() {
baseIncrement()
self.called += 1
}
}
})
const store = B.create()
expect(getSnapshot(store)).toEqual({ count: 0, called: 0 })
expect(store.count).toBe(0)
store.increment()
expect(store.count).toBe(1)
expect(store.called).toBe(1)
})
test("compose should be overwrite", () => {
const A = types
.model({
name: "",
alias: ""
})
.views(self => ({
get displayName() {
return self.alias || self.name
}
}))
const B = A.props({
type: ""
}).views(self => ({
get displayName() {
return self.alias || self.name + self.type
}
}))
const storeA = A.create({ name: "nameA", alias: "aliasA" })
const storeB = B.create({ name: "nameB", alias: "aliasB", type: "typeB" })
const storeC = B.create({ name: "nameC", type: "typeC" })
expect(storeA.displayName).toBe("aliasA")
expect(storeB.displayName).toBe("aliasB")
expect(storeC.displayName).toBe("nameCtypeC")
})
// === TYPE CHECKS ===
test("it should check the type correctly", () => {
const { Factory } = createTestFactories()
const doc = Factory.create()
expect(Factory.is(doc)).toEqual(true)
expect(Factory.is([])).toEqual(false)
expect(Factory.is({})).toEqual(true)
expect(Factory.is({ to: "mars" })).toEqual(true)
expect(Factory.is({ wrongKey: true })).toEqual(true)
expect(Factory.is({ to: 3 })).toEqual(false)
})
if (process.env.NODE_ENV !== "production") {
test("complex map / array values are optional by default", () => {
expect(
types
.model({
todo: types.model({})
})
.is({})
).toBe(false)
expect(() =>
types
.model({
todo: types.model({})
})
.create({} as any)
).toThrow()
expect(
types
.model({
todo: types.array(types.string)
})
.is({})
).toBe(true) // TBD: or true?
expect(
getSnapshot(
types
.model({
todo: types.array(types.string)
})
.create({})
)
).toEqual({ todo: [] })
expect(
types
.model({
todo: types.map(types.string)
})
.is({})
).toBe(true)
expect(
getSnapshot(
types
.model({
todo: types.map(types.string)
})
.create({})
)
).toEqual({ todo: {} })
})
}
// === VIEW FUNCTIONS ===
test("view functions should be tracked", () => {
const model = types
.model({
x: 3
})
.views(self => ({
doubler() {
return self.x * 2
}
}))
.create()
unprotect(model)
const values: number[] = []
const d = autorun(() => {
values.push(model.doubler())
})
model.x = 7
expect(values).toEqual([6, 14])
})
test("view functions should not be allowed to change state", () => {
const model = types
.model({
x: 3
})
.views(self => ({
doubler() {
self.x *= 2
}
}))
.actions(self => {
function anotherDoubler() {
self.x *= 2
}
return {
anotherDoubler
}
})
.create()
expect(() => model.doubler()).toThrow()
model.anotherDoubler()
expect(model.x).toBe(6)
})
test("it should consider primitives as proposed defaults", () => {
const now = new Date()
const Todo = types.model({
id: 0,
name: "Hello world",
done: false,
createdAt: now
})
const doc = Todo.create()
expect(getSnapshot(doc)).toEqual({
id: 0,
name: "Hello world",
done: false,
createdAt: now.getTime()
})
})
test("it should throw if a non-primitive value is provided and no default can be created", () => {
expect(() => {
types.model({
complex: {
a: 1,
b: 2
} as any
})
}).toThrow()
})
if (process.env.NODE_ENV !== "production") {
test("it should not be possible to remove a node from a parent if it is required, see ", () => {
const A = types.model("A", { x: 3 })
const B = types.model("B", { a: A })
const b = B.create({ a: { x: 7 } })
unprotect(b)
expect(() => {
detach(b.a)
}).toThrow(/Error while converting `undefined` to `A`/)
expect(() => {
destroy(b.a)
}).toThrow(/Error while converting `undefined` to `A`/)
})
test("it should be possible to remove a node from a parent if it is defined as type maybe ", () => {
const A = types.model("A", { x: 3 })
const B = types.model("B", { a: types.maybe(A) })
const b = B.create({ a: { x: 7 } })
unprotect(b)
expect(() => {
const a = b.a!
detach(a)
destroy(a)
}).not.toThrow()
expect(b.a).toBeUndefined()
expect(getSnapshot(b).a).toBeUndefined()
})
test("it should be possible to remove a node from a parent if it is defined as type maybeNull ", () => {
const A = types.model("A", { x: 3 })
const B = types.model("B", { a: types.maybeNull(A) })
const b = B.create({ a: { x: 7 } })
unprotect(b)
expect(() => {
const a = b.a!
detach(a)
destroy(a)
}).not.toThrow()
expect(b.a).toBe(null)
expect(getSnapshot(b).a).toBe(null)
})
}
test("it should be possible to share states between views and actions using enhance", () => {
const A = types.model({}).extend(self => {
const localState = observable.box(3)
return {
views: {
get x() {
return localState.get()
}
},
actions: {
setX(value: number) {
localState.set(value)
}
}
}
})
let x = 0
let a = A.create()
const d = reaction(
() => a.x,
v => {
x = v
}
)
a.setX(7)
expect(a.x).toBe(7)
expect(x).toBe(7)
d()
})
test("It should throw if any other key is returned from extend", () => {
const A = types.model({}).extend(() => ({ stuff() {} }) as any)
expect(() => A.create()).toThrow(/stuff/)
})
test("782, TS + compose", () => {
const User = types.model("User", {
id: types.identifier,
name: types.maybe(types.string),
avatar: types.maybe(types.string)
})
const user = User.create({ id: "someId" })
})
test("961 - model creating should not change snapshot", () => {
const M = types.model({ foo: 1 })
const o = {}
const m = M.create(o)
expect(o).toEqual({})
expect(getSnapshot(m)).toEqual({ foo: 1 })
})
if (process.env.NODE_ENV === "development")
test("beautiful errors", () => {
expect(() => {
types.model("User", { x: (types.identifier as any)() })
}).toThrow("types.identifier is not a function")
expect(() => {
types.model("User", { x: { bla: true } as any })
}).toThrow(
"Invalid type definition for property 'x', it looks like you passed an object. Try passing another model type or a types.frozen"
)
expect(() => {
types.model("User", { x: function () {} as any })
}).toThrow(
"Invalid type definition for property 'x', it looks like you passed a function. Did you forget to invoke it, or did you intend to declare a view / action?"
)
})
test("#967 - changing values in afterCreate/afterAttach when node is instantiated from view", () => {
const Answer = types
.model("Answer", {
title: types.string,
selected: false
})
.actions(self => ({
toggle() {
self.selected = !self.selected
}
}))
const Question = types
.model("Question", { title: types.string, answers: types.array(Answer) })
.views(self => ({
get brokenView() {
// this should not be allowed
// MWE: disabled, MobX 6 no longer forbids this
// expect(() => {
// self.answers[0].toggle()
// }).toThrow()
return 0
}
}))
.actions(self => ({
afterCreate() {
// we should allow changes even when inside a computed property when done inside afterCreate/afterAttach
self.answers[0].toggle()
// but not further computed changes
expect(self.brokenView).toBe(0)
},
afterAttach() {
// we should allow changes even when inside a computed property when done inside afterCreate/afterAttach
self.answers[0].toggle()
expect(self.brokenView).toBe(0)
}
}))
const Product = types
.model("Product", {
questions: types.array(Question)
})
.views(self => ({
get selectedAnswers() {
const result = []
for (const question of self.questions) {
result.push(question.answers.find(a => a.selected))
}
return result
}
}))
const product = Product.create({
questions: [
{ title: "Q 0", answers: [{ title: "A 0.0" }, { title: "A 0.1" }] },
{ title: "Q 1", answers: [{ title: "A 1.0" }, { title: "A 1.1" }] }
]
})
// tslint:disable-next-line:no-unused-expression
product.selectedAnswers
})
test("#993-1 - after attach should have a parent when accesing a reference directly", () => {
const L4 = types
.model("Todo", {
id: types.identifier,
finished: false
})
.actions(self => ({
afterAttach() {
expect(getParent(self)).toBeTruthy()
}
}))
const L3 = types.model({ l4: L4 }).actions(self => ({
afterAttach() {
expect(getParent(self)).toBeTruthy()
}
}))
const L2 = types
.model({
l3: L3
})
.actions(self => ({
afterAttach() {
expect(getParent(self)).toBeTruthy()
}
}))
const L1 = types
.model({
l2: L2,
selected: types.reference(L4)
})
.actions(self => ({
afterAttach() {
throw new MstError("should never be called")
}
}))
const createL1 = () =>
L1.create({
l2: {
l3: {
l4: {
id: "11124091-11c1-4dda-b2ed-7dd6323491a5"
}
}
},
selected: "11124091-11c1-4dda-b2ed-7dd6323491a5"
})
// test 1, real child first
{
const l1 = createL1()
const a = l1.l2.l3.l4
const b = l1.selected
}
// test 2, reference first
{
const l1 = createL1()
const a = l1.selected
const b = l1.l2.l3.l4
}
})
test("#993-2 - references should have a parent even when the parent has not been accessed before", () => {
const events: string[] = []
const L4 = types
.model("Todo", {
id: types.identifier,
finished: false
})
.actions(self => ({
toggle() {
self.finished = !self.finished
},
afterCreate() {
events.push("l4-ac")
},
afterAttach() {
events.push("l4-at")
}
}))
const L3 = types.model({ l4: L4 }).actions(self => ({
afterCreate() {
events.push("l3-ac")
},
afterAttach() {
events.push("l3-at")
}
}))
const L2 = types
.model({
l3: L3
})
.actions(self => ({
afterCreate() {
events.push("l2-ac")
},
afterAttach() {
events.push("l2-at")
}
}))
const L1 = types
.model({
l2: L2,
selected: types.reference(L4)
})
.actions(self => ({
afterCreate() {
events.push("l1-ac")
},
afterAttach() {
events.push("l1-at")
}
}))
const createL1 = () =>
L1.create({
l2: {
l3: {
l4: {
id: "11124091-11c1-4dda-b2ed-7dd6323491a5"
}
}
},
selected: "11124091-11c1-4dda-b2ed-7dd6323491a5"
})
const expectedEvents = [
"l1-ac",
"l2-ac",
"l2-at",
"l3-ac",
"l3-at",
"l4-ac",
"l4-at",
"onSnapshot",
"-",
"onSnapshot"
]
// test 1, real child first
{
const l1 = createL1()
onSnapshot(l1, () => {
events.push("onSnapshot")
})
l1.l2.l3.l4.toggle()
events.push("-")
l1.selected.toggle()
expect(events).toEqual(expectedEvents)
}
const expectedEvents2 = [
"l1-ac",
"l4-ac",
"l3-ac",
"l2-ac",
"l2-at",
"l3-at",
"l4-at",
"onSnapshot",
"-",
"onSnapshot"
]
// test 2, reference first
// the order of hooks is different but they are all called
events.length = 0
{
const l1 = createL1()
onSnapshot(l1, () => {
events.push("onSnapshot")
})
l1.selected.toggle()
events.push("-")
l1.l2.l3.l4.toggle()
expect(events).toEqual(expectedEvents2)
}
// test 3, reference get parent should be available from the beginning and all the way to the root
{
const rootL1 = createL1()
const l4 = rootL1.selected
const l3 = getParent(l4)
expect(l3).toBeTruthy()
const l2 = getParent(l3)
expect(l2).toBeTruthy()
const l1 = getParent(l2)
expect(l1).toBeTruthy()
expect(l1).toBe(rootL1)
expect(l2).toBe(rootL1.l2)
expect(l3).toBe(rootL1.l2.l3)
expect(l4).toBe(rootL1.l2.l3.l4)
}
})
test("it should emit patches when applySnapshot is used", () => {
const { Factory } = createTestFactories()
const doc = Factory.create()
let patches: IJsonPatch[] = []
onPatch(doc, patch => patches.push(patch))
applySnapshot(doc, { ...getSnapshot(doc), to: "universe" })
expect(patches).toEqual([{ op: "replace", path: "/to", value: "universe" }])
})
test("isAlive must be reactive", () => {
const Todo = types.model({ text: types.string })
const TodoStore = types.model({
todos: types.array(Todo),
todo: types.maybe(Todo)
})
const store = TodoStore.create({
todos: [{ text: "1" }, { text: "2" }],
todo: { text: "3" }
})
unprotect(store)
const t1 = store.todos[0]!
const t2 = store.todos[1]!
const t3 = store.todo!
let calls = 0
const r1 = reaction(
() => isAlive(t1),
v => {
expect(v).toBe(false)
calls++
}
)
const r2 = reaction(
() => isAlive(t2),
v => {
expect(v).toBe(false)
calls++
}
)
const r3 = reaction(
() => isAlive(t3),
v => {
expect(v).toBe(false)
calls++
}
)
try {
store.todos = cast([])
store.todo = undefined
expect(calls).toBe(3)
} finally {
r1()
r2()
r3()
}
})
test("#1112 - identifier cache should be cleared for unaccessed wrapped objects", () => {
const mock1 = [
{ id: "1", name: "Kate" },
{ id: "2", name: "John" }
]
const mock2 = [
{ id: "3", name: "Andrew" },
{ id: "2", name: "John" }
]
const mock1_2 = mock1.map((i, index) => ({ text: `Text${index}`, entity: i }))
const mock2_2 = mock2.map((i, index) => ({ text: `Text${index}`, entity: i }))
const Entity = types.model({
id: types.identifier,
name: types.string
})
const Wrapper = types.model({
text: types.string,
entity: Entity
})
const Store = types
.model({
list: types.optional(types.array(Wrapper), []),
selectedId: 2
})
.views(self => ({
get selectedEntity() {
return resolveIdentifier(Entity, self, self.selectedId)
}
}))
const store = Store.create()
unprotect(store)
store.list.replace(mock1_2)
store.list.replace(mock2_2)
expect(store.selectedEntity!.id).toBe("2")
})
test("#1173 - detaching a model should not screw it", () => {
const AM = types.model({ x: 5 })
const Store = types.model({ item: types.maybe(AM) })
const s = Store.create({ item: { x: 6 } })
const n0 = s.item
unprotect(s)
const detachedItem = detach(s.item!)
expect(s.item).not.toBe(detachedItem)
expect(s.item).toBeUndefined()
expect(detachedItem.x).toBe(6)
expect(detachedItem).toBe(n0!)
})
test("#1702 - should not throw with useProxies: 'ifavailable'", () => {
configure({
useProxies: "ifavailable"
})
const M = types.model({ x: 5 }).views(self => ({
get y() {
return self.x
}
}))
expect(() => {
M.create({})
}).not.toThrow()
})
================================================
FILE: __tests__/core/optimizations.test.ts
================================================
import { getSnapshot, applySnapshot, unprotect, types } from "../../src"
import { expect, test } from "bun:test"
test("it should avoid processing patch if is exactly the current one in applySnapshot", () => {
const Model = types.model({
a: types.number,
b: types.string
})
const store = Model.create({ a: 1, b: "hello" })
const snapshot = getSnapshot(store)
applySnapshot(store, snapshot)
expect(getSnapshot(store)).toBe(snapshot) // no new snapshot emitted
})
test("it should avoid processing patch if is exactly the current one in reconcile", () => {
const Model = types.model({
a: types.number,
b: types.string
})
const RootModel = types.model({
a: Model
})
const store = RootModel.create({ a: { a: 1, b: "hello" } })
unprotect(store)
// NOTE: snapshots are not equal after property access anymore,
// so we test initial and actual ones separately
const snapshot = getSnapshot(store)
expect(getSnapshot(store)).toEqual(snapshot)
store.a = snapshot.a
// check whether reconciliation works on initial values
expect(getSnapshot(store)).toEqual(snapshot)
// access property to initialize observable instance
expect(getSnapshot(store.a)).toEqual(snapshot.a)
// check whether initializing instance does not cause snapshot invalidation
const actualSnapshot = getSnapshot(store)
expect(actualSnapshot.a).toBe(snapshot.a)
})
================================================
FILE: __tests__/core/optional-extension.test.ts
================================================
import { getSnapshot, types, unprotect } from "../../src"
import { describe, expect, test } from "bun:test"
describe("null as default", () => {
describe("basic tests", () => {
const M = types.model({
x: types.optional(types.number, 1, [null]),
y: types.optional(types.number, () => 2, [null])
})
test("with optional values, then assigned values", () => {
const m = M.create({
x: null,
y: null
})
unprotect(m)
expect(m.x).toBe(1)
expect(m.y).toBe(2)
expect(getSnapshot(m)).toEqual({
x: 1,
y: 2
})
m.x = 10
m.y = 20
expect(m.x).toBe(10)
expect(m.y).toBe(20)
expect(getSnapshot(m)).toEqual({
x: 10,
y: 20
})
})
test("with given values, then assigned optional values", () => {
const m = M.create({
x: 10,
y: 20
})
unprotect(m)
expect(m.x).toBe(10)
expect(m.y).toBe(20)
expect(getSnapshot(m)).toEqual({
x: 10,
y: 20
})
m.x = null as any
m.y = null as any
expect(m.x).toBe(1)
expect(m.y).toBe(2)
expect(getSnapshot(m)).toEqual({
x: 1,
y: 2
})
})
})
test("when the underlying type accepts undefined it should be ok", () => {
const M = types.model({
a: types.optional(types.union(types.undefined, types.number), undefined, [null]),
b: types.optional(types.union(types.undefined, types.number), 5, [null])
})
{
const m = M.create({
a: null,
b: null
})
expect(m.a).toBeUndefined()
expect(m.b).toBe(5)
expect(getSnapshot(m)).toEqual({
a: undefined,
b: 5
})
}
{
const m = M.create({
a: 10,
b: 20
})
expect(m.a).toBe(10)
expect(m.b).toBe(20)
expect(getSnapshot(m)).toEqual({
a: 10,
b: 20
})
}
{
const m = M.create({
a: undefined,
b: undefined
})
expect(m.a).toBeUndefined()
expect(m.b).toBeUndefined()
expect(getSnapshot(m)).toEqual({
a: undefined,
b: undefined
})
}
})
test("when the underlying type does not accept undefined, then undefined should throw", () => {
const M = types.model({
a: types.optional(types.number, 5, [null]),
b: types.optional(types.number, 6, [null])
})
{
const m = M.create({
a: null,
b: null
})
expect(m.a).toBe(5)
expect(m.b).toBe(6)
}
if (process.env.NODE_ENV !== "production") {
expect(() => {
M.create({
a: null,
b: undefined as any // undefined is not valid
})
}).toThrow("value `undefined` is not assignable to type: `number`")
expect(() => {
M.create({
a: null
// b: null missing, but should be there
} as any)
}).toThrow("value `undefined` is not assignable to type: `number`")
}
})
})
describe("'empty' or false as default", () => {
describe("basic tests", () => {
const M = types.model({
x: types.optional(types.number, 1, ["empty", false]),
y: types.optional(types.number, () => 2, ["empty", false])
})
test("with optional values, then assigned values", () => {
const m = M.create({
x: "empty",
y: false
})
unprotect(m)
expect(m.x).toBe(1)
expect(m.y).toBe(2)
expect(getSnapshot(m)).toEqual({
x: 1,
y: 2
})
m.x = 10
m.y = 20
expect(m.x).toBe(10)
expect(m.y).toBe(20)
expect(getSnapshot(m)).toEqual({
x: 10,
y: 20
})
})
test("with given values, then assigned 'empty'", () => {
const m = M.create({
x: 10,
y: 20
})
unprotect(m)
expect(m.x).toBe(10)
expect(m.y).toBe(20)
expect(getSnapshot(m)).toEqual({
x: 10,
y: 20
})
m.x = "empty" as any
m.y = false as any
expect(m.x).toBe(1)
expect(m.y).toBe(2)
expect(getSnapshot(m)).toEqual({
x: 1,
y: 2
})
})
})
test("when the underlying type accepts undefined it should be ok", () => {
const M = types.model({
a: types.optional(types.union(types.undefined, types.number), undefined, [
"empty",
false
]),
b: types.optional(types.union(types.undefined, types.number), 5, ["empty", false])
})
{
const m = M.create({
a: "empty",
b: false
})
expect(m.a).toBeUndefined()
expect(m.b).toBe(5)
expect(getSnapshot(m)).toEqual({
a: undefined,
b: 5
})
}
{
const m = M.create({
a: 10,
b: 20
})
expect(m.a).toBe(10)
expect(m.b).toBe(20)
expect(getSnapshot(m)).toEqual({
a: 10,
b: 20
})
}
{
const m = M.create({
a: undefined,
b: undefined
})
expect(m.a).toBeUndefined()
expect(m.b).toBeUndefined()
expect(getSnapshot(m)).toEqual({
a: undefined,
b: undefined
})
}
})
test("when the underlying type does not accept undefined, then undefined should throw", () => {
const M = types.model({
a: types.optional(types.number, 5, ["empty", false]),
b: types.optional(types.number, 6, ["empty", false])
})
{
const m = M.create({
a: "empty",
b: false
})
expect(m.a).toBe(5)
expect(m.b).toBe(6)
}
if (process.env.NODE_ENV !== "production") {
expect(() => {
M.create({
a: undefined as any,
b: undefined as any
})
}).toThrow("value `undefined` is not assignable to type: `number`")
}
})
})
test("cached snapshots should be ok when using default values", () => {
const M = types.model({ x: 5, y: 6 })
const Store = types.model({
deep: types.model({
a: types.optional(types.undefined, undefined),
b: types.optional(types.undefined, undefined, ["empty"]),
c: types.optional(types.number, 5),
d: types.optional(types.number, 5, ["empty"]),
a2: types.optional(types.undefined, () => undefined),
b2: types.optional(types.undefined, () => undefined, ["empty"]),
c2: types.optional(types.number, () => 5),
d2: types.optional(types.number, () => 5, ["empty"]),
a3: types.optional(M, { y: 20 }),
b3: types.optional(M, { y: 20 }, ["empty"]),
c3: types.optional(M, () => M.create({ y: 20 })),
d3: types.optional(M, () => M.create({ y: 20 }), ["empty"]),
e3: types.optional(M, () => ({ y: 20 })),
f3: types.optional(M, () => ({ y: 20 }), ["empty"])
})
})
const s = Store.create({
deep: {
b: "empty",
d: "empty",
b2: "empty",
d2: "empty",
b3: "empty",
d3: "empty",
f3: "empty"
}
})
expect(getSnapshot(s)).toEqual({
deep: {
a: undefined,
b: undefined,
c: 5,
d: 5,
a2: undefined,
b2: undefined,
c2: 5,
d2: 5,
a3: { x: 5, y: 20 },
b3: { x: 5, y: 20 },
c3: { x: 5, y: 20 },
d3: { x: 5, y: 20 },
e3: { x: 5, y: 20 },
f3: { x: 5, y: 20 }
}
})
})
================================================
FILE: __tests__/core/optional.test.ts
================================================
import { getSnapshot, types, unprotect, applySnapshot, cast } from "../../src"
import { expect, test } from "bun:test"
test("it should provide a default value, if no snapshot is provided", () => {
const Row = types.model({
name: "",
quantity: 0
})
const Factory = types.model({
rows: types.optional(types.array(Row), [{ name: "test" }])
})
const doc = Factory.create()
expect(getSnapshot(doc)).toEqual({ rows: [{ name: "test", quantity: 0 }] })
})
test("it should use the snapshot if provided", () => {
const Row = types.model({
name: "",
quantity: 0
})
const Factory = types.model({
rows: types.optional(types.array(Row), [{ name: "test" }])
})
const doc = Factory.create({ rows: [{ name: "snapshot", quantity: 0 }] })
expect(getSnapshot(doc)).toEqual({ rows: [{ name: "snapshot", quantity: 0 }] })
})
if (process.env.NODE_ENV !== "production") {
test("it should throw if default value is invalid snapshot", () => {
const Row = types.model({
name: types.string,
quantity: types.number
})
const error = expect(() => {
types.model({
rows: types.optional(types.array(Row), [{}] as any)
})
}).toThrow()
})
test("it should throw bouncing errors from its sub-type", () => {
const Row = types.model({
name: types.string,
quantity: types.number
})
const RowList = types.optional(types.array(Row), [])
const error = expect(() => {
RowList.create([
{ name: "a", quantity: 1 },
{ name: "b", quantity: "x" }
] as any)
}).toThrow()
})
}
test("it should accept a function to provide dynamic values", () => {
let defaultValue = 1
const Factory = types.model({
a: types.optional(types.number, () => defaultValue)
})
expect(getSnapshot(Factory.create())).toEqual({ a: 1 })
defaultValue = 2
expect(getSnapshot(Factory.create())).toEqual({ a: 2 })
defaultValue = "hello world!" as any
if (process.env.NODE_ENV !== "production") {
expect(() => Factory.create()).toThrow(
`[mobx-state-tree] Error while converting \`"hello world!"\` to \`number\`:\n\n value \`"hello world!"\` is not assignable to type: \`number\` (Value is not a number).`
)
}
})
test("Values should reset to default if omitted in snapshot", () => {
const Store = types.model({
todo: types.model({
id: types.identifier,
done: false,
title: "test",
thing: types.frozen({})
})
})
const store = Store.create({ todo: { id: "2" } })
unprotect(store)
store.todo.done = true
expect(store.todo.done).toBe(true)
store.todo = cast({ title: "stuff", id: "2" })
expect(store.todo.title).toBe("stuff")
expect(store.todo.done).toBe(false)
})
test("optional frozen should fallback to default value if snapshot is undefined", () => {
const Store = types.model({ thing: types.frozen({}) })
const store = Store.create({
thing: null
})
expect(store.thing).toBeNull()
applySnapshot(store, {})
expect(store.thing).toBeDefined()
expect(store.thing).toEqual({})
})
test("an instance is not a valid default value, snapshot or function that creates instance must be used", () => {
const Row = types.model("Row", {
name: "",
quantity: 0
})
// passing a node directly, without a generator function
expect(() => {
types.model({ rows: types.optional(types.array(Row), types.array(Row).create()) })
}).toThrow(
"default value cannot be an instance, pass a snapshot or a function that creates an instance/snapshot instead"
)
// an alike node but created from a different yet equivalent type
const e = expect(() => {
const Factory = types.model({
rows: types.optional(types.array(Row), () => types.array(Row).create())
})
// we need to create the node for it to throw, since generator functions are typechecked when nodes are created
// tslint:disable-next-line:no-unused-expression
Factory.create()
})
if (process.env.NODE_ENV === "production") {
e.not.toThrow()
} else {
e.toThrow("Error while converting <> to `Row[]`")
}
{
// a node created on a generator function of the exact same type
const RowArray = types.array(Row)
const Factory = types.model("Factory", {
rows: types.optional(RowArray, () => RowArray.create())
})
const doc = Factory.create()
expect(getSnapshot(doc)).toEqual({ rows: [] })
}
})
test("undefined can work as a missing value", () => {
const M = types.model({ x: types.union(types.undefined, types.number) })
const m1 = M.create({ x: 5 })
expect(m1.x).toBe(5)
const m2 = M.create({ x: undefined })
expect(m2.x).toBeUndefined()
const m3 = M.create({}) // is ok as well (even in TS)
expect(m3.x).toBeUndefined()
})
================================================
FILE: __tests__/core/parent-properties.test.ts
================================================
import { types, getEnv, getParent, getPath, Instance } from "../../src"
import { expect, test } from "bun:test"
const ChildModel = types
.model("Child", {
parentPropertyIsNullAfterCreate: false,
parentEnvIsNullAfterCreate: false,
parentPropertyIsNullAfterAttach: false
})
.views(self => {
return {
get parent(): IParentModelInstance {
return getParent(self)
}
}
})
.actions(self => ({
afterCreate() {
self.parentPropertyIsNullAfterCreate = typeof self.parent.fetch === "undefined"
self.parentEnvIsNullAfterCreate = typeof getEnv(self.parent).fetch === "undefined"
},
afterAttach() {
self.parentPropertyIsNullAfterAttach = typeof self.parent.fetch === "undefined"
}
}))
const ParentModel = types
.model("Parent", {
child: types.optional(ChildModel, {})
})
.views(self => ({
get fetch() {
return getEnv(self).fetch
}
}))
interface IParentModelInstance extends Instance {}
// NOTE: parents are now always created before children;
// moreover, we do not actually have actions hash during object-node creation
test("Parent property have value during child's afterCreate() event", () => {
const mockFetcher = () => Promise.resolve(true)
const parent = ParentModel.create({}, { fetch: mockFetcher })
// Because the child is created before the parent creation is finished, this one will yield `true` (the .fetch view is still undefined)
expect(parent.child.parentPropertyIsNullAfterCreate).toBe(false)
// ... but, the env is available
expect(parent.child.parentEnvIsNullAfterCreate).toBe(false)
})
test("Parent property has value during child's afterAttach() event", () => {
const mockFetcher = () => Promise.resolve(true)
const parent = ParentModel.create({}, { fetch: mockFetcher })
expect(parent.child.parentPropertyIsNullAfterAttach).toBe(false)
})
test("#917", () => {
const SubTodo = types
.model("SubTodo", {
id: types.optional(types.number, () => Math.random()),
title: types.string,
finished: false
})
.views(self => ({
get path() {
return getPath(self)
}
}))
.actions(self => ({
toggle() {
self.finished = !self.finished
}
}))
const Todo = types
.model("Todo", {
id: types.optional(types.number, () => Math.random()),
title: types.string,
finished: false,
subTodos: types.array(SubTodo)
})
.views(self => ({
get path() {
return getPath(self)
}
}))
.actions(self => ({
toggle() {
self.finished = !self.finished
}
}))
const TodoStore = types
.model("TodoStore", {
todos: types.array(Todo)
})
.views(self => ({
get unfinishedTodoCount() {
return self.todos.filter(todo => !todo.finished).length
}
}))
.actions(self => ({
addTodo(title: string) {
self.todos.push({
title,
subTodos: [
{
title
}
]
})
}
}))
const store2 = TodoStore.create({
todos: [
Todo.create({
title: "get Coffee",
subTodos: [
SubTodo.create({
title: "test"
})
]
})
]
})
expect(store2.todos[0].path).toBe("/todos/0")
expect(store2.todos[0].subTodos[0].path).toBe("/todos/0/subTodos/0")
})
================================================
FILE: __tests__/core/pointer.test.ts
================================================
import { types, unprotect, IAnyModelType, castToReferenceSnapshot } from "../../src"
import { expect, test } from "bun:test"
function Pointer(Model: IT) {
return types.model("PointerOf" + Model.name, {
value: types.maybe(types.reference(Model))
})
}
const Todo = types.model("Todo", {
id: types.identifier,
name: types.string
})
test("it should allow array of pointer objects", () => {
const TodoPointer = Pointer(Todo)
const AppStore = types.model("AppStore", {
todos: types.array(Todo),
selected: types.optional(types.array(TodoPointer), [])
})
const store = AppStore.create({
todos: [
{ id: "1", name: "Hello" },
{ id: "2", name: "World" }
],
selected: []
})
unprotect(store)
const ref = TodoPointer.create({ value: castToReferenceSnapshot(store.todos[0]) }) // Fails because store.todos does not belongs to the same tree
store.selected.push(ref)
expect(store.selected[0].value).toBe(store.todos[0])
})
test("it should allow array of pointer objects - 2", () => {
const TodoPointer = Pointer(Todo)
const AppStore = types.model({
todos: types.array(Todo),
selected: types.optional(types.array(TodoPointer), [])
})
const store = AppStore.create({
todos: [
{ id: "1", name: "Hello" },
{ id: "2", name: "World" }
],
selected: []
})
unprotect(store)
const ref = TodoPointer.create()
store.selected.push(ref)
ref.value = store.todos[0]
expect(store.selected[0].value).toBe(store.todos[0])
})
test("it should allow array of pointer objects - 3", () => {
const TodoPointer = Pointer(Todo)
const AppStore = types.model({
todos: types.array(Todo),
selected: types.optional(types.array(TodoPointer), [])
})
const store = AppStore.create({
todos: [
{ id: "1", name: "Hello" },
{ id: "2", name: "World" }
],
selected: []
})
unprotect(store)
const ref = TodoPointer.create({ value: castToReferenceSnapshot(store.todos[0]) })
store.selected.push(ref)
expect(store.selected[0].value).toBe(store.todos[0])
})
test("it should allow array of pointer objects - 4", () => {
const TodoPointer = Pointer(Todo)
const AppStore = types.model({
todos: types.array(Todo),
selected: types.optional(types.array(TodoPointer), [])
})
const store = AppStore.create({
todos: [
{ id: "1", name: "Hello" },
{ id: "2", name: "World" }
],
selected: []
})
unprotect(store)
const ref = TodoPointer.create() // Fails because ref is required
store.selected.push(ref)
ref.value = store.todos[0]
expect(ref.value).toBe(store.todos[0])
})
================================================
FILE: __tests__/core/primitives.test.ts
================================================
import { isFinite, isFloat, isInteger } from "../../src/utils"
import { types, applySnapshot, getSnapshot } from "../../src"
import { expect, test } from "bun:test"
test("Date instance can be reused", () => {
const Model = types.model({
a: types.model({
b: types.string
}),
c: types.Date // types.string -> types.Date
})
const Store = types
.model({
one: Model,
index: types.array(Model)
})
.actions(self => {
function set(one: typeof Model.Type) {
self.one = one
}
function push(model: typeof Model.Type) {
self.index.push(model)
}
return {
set,
push
}
})
const object = { a: { b: "string" }, c: new Date() } // string -> date (number)
const instance = Store.create({
one: object,
index: [object]
})
instance.set(object)
expect(() => instance.push(object)).not.toThrow()
expect(instance.one.c).toBe(object.c)
expect(instance.index[0].c).toBe(object.c)
})
test("Date can be rehydrated using unix timestamp", () => {
const time = new Date()
const newTime = 6813823163
const Factory = types.model({
date: types.optional(types.Date, () => time)
})
const store = Factory.create()
expect(store.date.getTime()).toBe(time.getTime())
applySnapshot(store, { date: newTime })
expect(store.date.getTime()).toBe(newTime)
expect(getSnapshot(store).date).toBe(newTime)
})
test("check isInteger", () => {
expect(isInteger(5)).toBe(true)
expect(isInteger(-5)).toBe(true)
expect(isInteger(5.2)).toBe(false)
})
test("Default inference for integers is 'number'", () => {
const A = types.model({
x: 3
})
expect(
A.is({
x: 2.5
})
).toBe(true)
})
test("check isFloat", () => {
expect(isFloat(3.14)).toBe(true)
expect(isFloat(-2.5)).toBe(true)
expect(isFloat(Infinity)).toBe(true)
expect(isFloat(10)).toBe(false)
expect(isFloat(0)).toBe(false)
expect(isFloat("3.14")).toBe(false)
expect(isFloat(null)).toBe(false)
expect(isFloat(undefined)).toBe(false)
expect(isFloat(NaN)).toBe(false)
})
test("check isFinite", () => {
expect(isFinite(3.14)).toBe(true)
expect(isFinite(-2.5)).toBe(true)
expect(isFinite(10)).toBe(true)
expect(isFinite(0)).toBe(true)
expect(isFinite("3.14")).toBe(false)
expect(isFinite(null)).toBe(false)
expect(isFinite(undefined)).toBe(false)
expect(isFinite(NaN)).toBe(false)
expect(isFinite(Infinity)).toBe(false)
})
if (process.env.NODE_ENV !== "production") {
test("Passing non integer to types.integer", () => {
const Size = types.model({
width: types.integer,
height: 20
})
expect(() => {
const size = Size.create({ width: 10 })
}).not.toThrow()
expect(() => {
const size = Size.create({ width: 10.5 })
}).toThrow()
})
}
test("types.bigint accepts bigint values", () => {
const Model = types.model({
id: types.bigint,
value: types.bigint
})
const instance = Model.create({ id: BigInt(1), value: BigInt(2) })
expect(instance.id).toBe(BigInt(1))
expect(instance.value).toBe(BigInt(2))
expect(types.bigint.describe()).toBe("bigint")
})
test("types.bigint snapshot round-trip preserves bigint", () => {
const Model = types.model({ id: types.bigint })
const instance = Model.create({ id: BigInt("9007199254740993") })
const snapshot = getSnapshot(instance)
expect(snapshot.id).toBe("9007199254740993")
expect(typeof snapshot.id).toBe("string")
})
test("types.bigint snapshot is JSON-serializable and deserializes correctly", () => {
const Model = types.model({ id: types.bigint, value: types.bigint })
const instance = Model.create({ id: BigInt(1), value: BigInt("9007199254740993") })
const snapshot = getSnapshot(instance)
const json = JSON.stringify(snapshot)
expect(json).toBe('{"id":"1","value":"9007199254740993"}')
const parsed = JSON.parse(json)
const restored = Model.create(parsed)
expect(restored.id).toBe(BigInt(1))
expect(restored.value).toBe(BigInt("9007199254740993"))
})
if (process.env.NODE_ENV !== "production") {
test("Passing non bigint/string/number to types.bigint", () => {
const Model = types.model({ id: types.bigint })
expect(() => Model.create({ id: BigInt(1) })).not.toThrow()
expect(() => Model.create({ id: "1" })).not.toThrow()
expect(() => Model.create({ id: 1 })).not.toThrow()
expect(() => Model.create({ id: null as any })).toThrow()
})
}
================================================
FILE: __tests__/core/protect.test.ts
================================================
import { protect, unprotect, applySnapshot, types, isProtected, getParent, cast } from "../../src"
import { expect, test } from "bun:test"
const Todo = types
.model("Todo", {
title: ""
})
.actions(self => {
function setTitle(newTitle: string) {
self.title = newTitle
}
return {
setTitle
}
})
const Store = types.model("Store", {
todos: types.array(Todo)
})
function createTestStore() {
return Store.create({
todos: [{ title: "Get coffee" }, { title: "Get biscuit" }]
})
}
test("it should be possible to protect an object", () => {
const store = createTestStore()
unprotect(store)
store.todos[1].title = "A"
protect(store)
expect(() => {
store.todos[0].title = "B"
}).toThrow(
"[mobx-state-tree] Cannot modify 'Todo@/todos/0', the object is protected and can only be modified by using an action."
)
expect(store.todos[1].title).toBe("A")
expect(store.todos[0].title).toBe("Get coffee")
store.todos[0].setTitle("B")
expect(store.todos[0].title).toBe("B")
})
test("protect should protect against any update", () => {
const store = createTestStore()
expect(
// apply Snapshot / patch are currently allowed, even outside protected mode
() => {
applySnapshot(store, { todos: [{ title: "Get tea" }] })
}
).not.toThrow(
"[mobx-state-tree] Cannot modify 'Todo@', the object is protected and can only be modified by using an action."
)
expect(() => {
store.todos.push({ title: "test" })
}).toThrow(
"[mobx-state-tree] Cannot modify 'Todo[]@/todos', the object is protected and can only be modified by using an action."
)
expect(() => {
store.todos[0].title = "test"
}).toThrow(
"[mobx-state-tree] Cannot modify 'Todo@/todos/0', the object is protected and can only be modified by using an action."
)
})
test("protect should also protect children", () => {
const store = createTestStore()
expect(() => {
store.todos[0].title = "B"
}).toThrow(
"[mobx-state-tree] Cannot modify 'Todo@/todos/0', the object is protected and can only be modified by using an action."
)
store.todos[0].setTitle("B")
expect(store.todos[0].title).toBe("B")
})
test("unprotected mode should be lost when attaching children", () => {
const store = Store.create({ todos: [] })
const t1 = Todo.create({ title: "hello" })
unprotect(t1)
expect(isProtected(t1)).toBe(false)
expect(isProtected(store)).toBe(true)
t1.title = "world" // ok
unprotect(store)
store.todos.push(t1)
protect(store)
expect(isProtected(t1)).toBe(true)
expect(isProtected(store)).toBe(true)
expect(() => {
t1.title = "B"
}).toThrow(
"[mobx-state-tree] Cannot modify 'Todo@/todos/0', the object is protected and can only be modified by using an action."
)
store.todos[0].setTitle("C")
expect(store.todos[0].title).toBe("C")
})
test("protected mode should be inherited when attaching children", () => {
const store = Store.create({ todos: [] })
unprotect(store)
const t1 = Todo.create({ title: "hello" })
expect(isProtected(t1)).toBe(true)
expect(isProtected(store)).toBe(false)
expect(() => {
t1.title = "B"
}).toThrow(
"[mobx-state-tree] Cannot modify 'Todo@', the object is protected and can only be modified by using an action."
)
store.todos.push(t1)
t1.title = "world" // ok, now unprotected
expect(isProtected(t1)).toBe(false)
expect(isProtected(store)).toBe(false)
expect(store.todos[0].title).toBe("world")
})
test("action cannot modify parent", () => {
const Child = types
.model("Child", {
x: 2
})
.actions(self => ({
setParentX() {
getParent(self).x += 1
}
}))
const Parent = types.model("Parent", {
x: 3,
child: Child
})
const p = Parent.create({ child: {} })
expect(() => p.child.setParentX()).toThrow(
"[mobx-state-tree] Cannot modify 'Parent@', the object is protected and can only be modified by using an action."
)
})
================================================
FILE: __tests__/core/recordPatches.test.ts
================================================
import {
getSnapshot,
unprotect,
recordPatches,
types,
IType,
IJsonPatch,
Instance,
cast,
IAnyModelType,
IMSTMap
} from "../../src"
import { expect, test } from "bun:test"
function testPatches(
type: IType,
snapshot: C,
fn: any,
expectedPatches: IJsonPatch[],
expectedInversePatches: IJsonPatch[]
) {
const instance = type.create(snapshot)
const baseSnapshot = getSnapshot(instance)
const recorder = recordPatches(instance)
unprotect(instance)
fn(instance)
recorder.stop()
expect(recorder.patches).toEqual(expectedPatches)
expect(recorder.inversePatches).toEqual(expectedInversePatches)
const clone = type.create(snapshot)
recorder.replay(clone)
expect(getSnapshot(clone)).toEqual(getSnapshot(instance))
recorder.undo()
expect(getSnapshot(instance)).toEqual(baseSnapshot)
}
const Node = types.model("Node", {
id: types.identifierNumber,
text: "Hi",
children: types.optional(types.array(types.late((): IAnyModelType => Node)), [])
})
test("it should apply simple patch", () => {
testPatches(
Node,
{ id: 1 },
(n: Instance) => {
n.text = "test"
},
[
{
op: "replace",
path: "/text",
value: "test"
}
],
[
{
op: "replace",
path: "/text",
value: "Hi"
}
]
)
})
test("it should apply deep patches to arrays", () => {
testPatches(
Node,
{ id: 1, children: [{ id: 2 }] },
(n: Instance) => {
const children = n.children as unknown as Instance[]
children[0].text = "test" // update
children[0] = cast({ id: 2, text: "world" }) // this reconciles; just an update
children[0] = cast({ id: 4, text: "coffee" }) // new object
children[1] = cast({ id: 3, text: "world" }) // addition
children.splice(0, 1) // removal
},
[
{
op: "replace",
path: "/children/0/text",
value: "test"
},
{
op: "replace",
path: "/children/0/text",
value: "world"
},
{
op: "replace",
path: "/children/0",
value: {
id: 4,
text: "coffee",
children: []
}
},
{
op: "add",
path: "/children/1",
value: {
id: 3,
text: "world",
children: []
}
},
{
op: "remove",
path: "/children/0"
}
],
[
{
op: "replace",
path: "/children/0/text",
value: "Hi"
},
{
op: "replace",
path: "/children/0/text",
value: "test"
},
{
op: "replace",
path: "/children/0",
value: {
children: [],
id: 2,
text: "world"
}
},
{
op: "remove",
path: "/children/1"
},
{
op: "add",
path: "/children/0",
value: {
children: [],
id: 4,
text: "coffee"
}
}
]
)
})
test("it should apply deep patches to maps", () => {
const NodeMap = types.model("NodeMap", {
id: types.identifierNumber,
text: "Hi",
children: types.optional(types.map(types.late((): IAnyModelType => NodeMap)), {})
})
testPatches(
NodeMap,
{ id: 1, children: { 2: { id: 2 } } },
(n: Instance) => {
const children = n.children as IMSTMap
children.get("2")!.text = "test" // update
children.put({ id: 2, text: "world" }) // this reconciles; just an update
children.set(
"4",
NodeMap.create({ id: 4, text: "coffee", children: { 23: { id: 23 } } })
) // new object
children.put({ id: 3, text: "world", children: { 7: { id: 7 } } }) // addition
children.delete("2") // removal
},
[
{
op: "replace",
path: "/children/2/text",
value: "test"
},
{
op: "replace",
path: "/children/2/text",
value: "world"
},
{
op: "add",
path: "/children/4",
value: {
children: {
23: {
children: {},
id: 23,
text: "Hi"
}
},
id: 4,
text: "coffee"
}
},
{
op: "add",
path: "/children/3",
value: {
children: {
7: {
children: {},
id: 7,
text: "Hi"
}
},
id: 3,
text: "world"
}
},
{
op: "remove",
path: "/children/2"
}
],
[
{
op: "replace",
path: "/children/2/text",
value: "Hi"
},
{
op: "replace",
path: "/children/2/text",
value: "test"
},
{
op: "remove",
path: "/children/4"
},
{
op: "remove",
path: "/children/3"
},
{
op: "add",
path: "/children/2",
value: {
children: {},
id: 2,
text: "world"
}
}
]
)
})
test("it should apply deep patches to objects", () => {
const NodeObject = types.model("NodeObject", {
id: types.identifierNumber,
text: "Hi",
child: types.maybe(types.late((): IAnyModelType => NodeObject))
})
testPatches(
NodeObject,
{ id: 1, child: { id: 2 } },
(n: Instance) => {
n.child!.text = "test" // update
n.child = cast({ id: 2, text: "world" }) // this reconciles; just an update
n.child = NodeObject.create({ id: 2, text: "coffee", child: { id: 23 } })
n.child = cast({ id: 3, text: "world", child: { id: 7 } }) // addition
n.child = undefined // removal
},
[
{
op: "replace",
path: "/child/text",
value: "test"
},
{
op: "replace",
path: "/child/text",
value: "world"
},
{
op: "replace",
path: "/child",
value: {
child: {
child: undefined,
id: 23,
text: "Hi"
},
id: 2,
text: "coffee"
}
},
{
op: "replace",
path: "/child",
value: {
child: {
child: undefined,
id: 7,
text: "Hi"
},
id: 3,
text: "world"
}
},
{
op: "replace",
path: "/child",
value: undefined
}
],
[
{
op: "replace",
path: "/child/text",
value: "Hi"
},
{
op: "replace",
path: "/child/text",
value: "test"
},
{
op: "replace",
path: "/child",
value: {
child: undefined,
id: 2,
text: "world"
}
},
{
op: "replace",
path: "/child",
value: {
child: {
child: undefined,
id: 23,
text: "Hi"
},
id: 2,
text: "coffee"
}
},
{
op: "replace",
path: "/child",
value: {
child: {
child: undefined,
id: 7,
text: "Hi"
},
id: 3,
text: "world"
}
}
]
)
})
================================================
FILE: __tests__/core/reference-custom.test.ts
================================================
import { reaction, when, values } from "mobx"
import {
types,
recordPatches,
getSnapshot,
applySnapshot,
applyPatch,
unprotect,
getRoot,
onSnapshot,
flow,
Instance,
resolveIdentifier
} from "../../src"
import { expect, test } from "bun:test"
test("it should support custom references - basics", () => {
const User = types.model({
id: types.identifier,
name: types.string
})
const UserByNameReference = types.maybeNull(
types.reference(User, {
// given an identifier, find the user
get(identifier, parent): any {
return (
(parent as Instance)!.users.find(u => u.name === identifier) ||
null
)
},
// given a user, produce the identifier that should be stored
set(value) {
return value.name
}
})
)
const Store = types.model({
users: types.array(User),
selection: UserByNameReference
})
const s = Store.create({
users: [
{ id: "1", name: "Michel" },
{ id: "2", name: "Mattia" }
],
selection: "Mattia"
})
unprotect(s)
expect(s.selection!.name).toBe("Mattia")
expect(s.selection === s.users[1]).toBe(true)
expect(getSnapshot(s).selection).toBe("Mattia")
s.selection = s.users[0]
expect(s.selection!.name).toBe("Michel")
expect(s.selection === s.users[0]).toBe(true)
expect(getSnapshot(s).selection).toBe("Michel")
s.selection = null
expect(getSnapshot(s).selection).toBe(null)
applySnapshot(s, Object.assign({}, getSnapshot(s), { selection: "Mattia" }))
// @ts-expect-error - typescript doesn't know that applySnapshot will update the selection
expect(s.selection).toBe(s.users[1])
applySnapshot(s, Object.assign({}, getSnapshot(s), { selection: "Unknown" }))
expect(s.selection).toBe(null)
})
test("it should support custom references - adv", () => {
const User = types.model({
id: types.identifier,
name: types.string
})
const NameReference = types.reference(User, {
get(identifier, parent): any {
if (identifier === null) return null
const users = values(getRoot>(parent!).users)
return users.filter(u => u.name === identifier)[0] || null
},
set(value) {
return value ? value.name : ""
}
})
const Store = types.model({
users: types.map(User),
selection: NameReference
})
const s = Store.create({
users: {
"1": { id: "1", name: "Michel" },
"2": { id: "2", name: "Mattia" }
},
selection: "Mattia"
})
unprotect(s)
expect(s.selection.name).toBe("Mattia")
expect(s.selection === s.users.get("2")).toBe(true)
expect(getSnapshot(s).selection).toBe("Mattia")
const p = recordPatches(s)
const r: any[] = []
onSnapshot(s, r.push.bind(r))
const ids: (string | null)[] = []
reaction(
() => s.selection,
selection => {
ids.push(selection ? selection.id : null)
}
)
s.selection = s.users.get("1")!
expect(s.selection.name).toBe("Michel")
expect(s.selection === s.users.get("1")).toBe(true)
expect(getSnapshot(s).selection).toBe("Michel")
applySnapshot(s, Object.assign({}, getSnapshot(s), { selection: "Mattia" }))
// @ts-expect-error - typescript doesn't know that applySnapshot will update the selection
expect(s.selection).toBe(s.users.get("2"))
applyPatch(s, { op: "replace", path: "/selection", value: "Michel" })
// @ts-expect-error - typescript doesn't know that applyPatch will update the selection
expect(s.selection).toBe(s.users.get("1"))
s.users.delete("1")
// @ts-expect-error - typescript doesn't know how delete will affect the selection
expect(s.selection).toBe(null)
s.users.put({ id: "3", name: "Michel" })
expect(s.selection.id).toBe("3")
expect(ids).toMatchSnapshot()
expect(r).toMatchSnapshot()
expect(p.patches).toMatchSnapshot()
expect(p.inversePatches).toMatchSnapshot()
})
test("it should support dynamic loading", async () => {
const events: string[] = []
const User = types.model({
name: types.string,
age: 0
})
const UserByNameReference = types.maybe(
types.reference(User, {
get(identifier: string, parent): any {
return (parent as Instance).getOrLoadUser(identifier)
},
set(value) {
return value.name
}
})
)
const Store = types
.model({
users: types.array(User),
selection: UserByNameReference
})
.actions(self => ({
loadUser: flow(function* loadUser(name: string) {
events.push("loading " + name)
self.users.push({ name })
yield new Promise(resolve => {
setTimeout(resolve, 200)
})
events.push("loaded " + name)
const user = (self.users.find(u => u.name === name)!.age = name.length * 3) // wonderful!
})
}))
.views(self => ({
// Important: a view so that the reference will automatically react to the reference being changed!
getOrLoadUser(name: string) {
const user = self.users.find(u => u.name === name) || null
if (!user) {
/*
TODO: this is ugly, but workaround the idea that views should be side effect free.
We need a more elegant solution..
*/
setImmediate(() => self.loadUser(name))
}
return user
}
}))
const s = Store.create({
users: [],
selection: "Mattia"
})
unprotect(s)
expect(events).toEqual([])
expect(s.users.length).toBe(0)
// @ts-expect-error - typescript doesn't know that the user will be loaded
expect(s.selection).toBe(null)
await when(() => s.users.length === 1 && s.users[0].age === 18 && s.users[0].name === "Mattia")
expect(s.selection).toBe(s.users[0])
expect(events).toEqual(["loading Mattia", "loaded Mattia"])
})
test("custom reference / safe custom reference to another store works", () => {
const Todo = types.model({ id: types.identifier })
const TodoStore = types.model({ todos: types.array(Todo) })
const OtherStore = types.model({
todoRef: types.maybe(
types.reference(Todo, {
get(id) {
const node = resolveIdentifier(Todo, todos, id)
if (!node) {
throw new Error("Invalid ref")
}
return node
},
set(value) {
return value.id
}
})
),
safeRef: types.safeReference(Todo, {
get(id) {
const node = resolveIdentifier(Todo, todos, id)
if (!node) {
throw new Error("Invalid ref")
}
return node
},
set(value) {
return value.id
}
})
})
const todos = TodoStore.create({
todos: [{ id: "1" }, { id: "2" }, { id: "3" }]
})
unprotect(todos)
// from a snapshot
const otherStore = OtherStore.create({
todoRef: "1",
safeRef: "1"
})
unprotect(otherStore)
expect(otherStore.todoRef!.id).toBe("1")
expect(otherStore.safeRef!.id).toBe("1")
// assigning an id
otherStore.todoRef = "2" as any
otherStore.safeRef = "2" as any
expect(otherStore.todoRef!.id).toBe("2")
expect(otherStore.safeRef!.id).toBe("2")
// assigning a node directly
otherStore.todoRef = todos.todos[2]
otherStore.safeRef = todos.todos[2]
expect(otherStore.todoRef!.id).toBe("3")
expect(otherStore.safeRef!.id).toBe("3")
// getting the snapshot
expect(getSnapshot(otherStore)).toEqual({
todoRef: "3",
safeRef: "3"
})
// the removed node should throw on standard refs access
// and be set to undefined on safe ones
todos.todos.splice(2, 1)
expect(() => otherStore.todoRef).toThrow("Invalid ref")
expect(otherStore.safeRef).toBeUndefined()
})
================================================
FILE: __tests__/core/reference-onInvalidated.test.ts
================================================
import {
types,
OnReferenceInvalidated,
Instance,
ReferenceIdentifier,
IAnyStateTreeNode,
unprotect,
OnReferenceInvalidatedEvent,
getSnapshot,
applySnapshot,
clone,
destroy
} from "../../src"
import { describe, expect, it, test } from "bun:test"
const Todo = types.model({ id: types.identifier })
const createSnapshot = (partialSnapshot: any) => ({
todos: [{ id: "1" }, { id: "2" }, { id: "3" }, { id: "4" }],
...partialSnapshot
})
const createStore = (
partialSnapshot: any,
onInvalidated?: OnReferenceInvalidated>,
customRef = false
) => {
const refOptions = {
onInvalidated,
get(identifier: ReferenceIdentifier, parent: IAnyStateTreeNode | null) {
return (parent as Instance).todos.find(t => t.id === identifier)
},
set(value: Instance): ReferenceIdentifier {
return value.id
}
}
if (!customRef) {
// @ts-ignore
delete refOptions.get
// @ts-ignore
delete refOptions.set
}
const Store = types.model({
todos: types.array(Todo),
onInv: types.maybe(types.reference(Todo, refOptions as any)),
single: types.safeReference(Todo),
deep: types.optional(
types.model({
single: types.safeReference(Todo)
}),
{}
),
arr: types.array(types.safeReference(Todo)),
map: types.map(types.safeReference(Todo))
})
const s = Store.create(createSnapshot(partialSnapshot))
unprotect(s)
return s
}
for (const customRef of [false, true]) {
describe(`onInvalidated - customRef: ${customRef}`, () => {
test("from snapshot without accessing the referenced node", () => {
let ev: OnReferenceInvalidatedEvent> | undefined
let oldRefId!: string
let calls = 0
const onInv: OnReferenceInvalidated> = ev1 => {
calls++
oldRefId = ev1.invalidTarget!.id
expect(ev1.invalidId).toBe(oldRefId)
ev = ev1
ev1.removeRef()
}
const store = createStore({ onInv: "1" }, onInv)
expect(calls).toBe(0)
store.todos.splice(0, 1)
expect(calls).toBe(1)
expect(ev!.parent).toBe(store)
expect(oldRefId).toBe("1")
expect(ev!.removeRef).toBeTruthy()
expect(ev!.replaceRef).toBeTruthy()
expect(store.onInv).toBeUndefined()
expect(getSnapshot(store).onInv).toBeUndefined()
store.onInv = store.todos[0]
expect(calls).toBe(1)
store.todos.splice(0, 1)
expect(calls).toBe(2)
expect(ev!.parent).toBe(store)
expect(oldRefId).toBe("2")
expect(ev!.removeRef).toBeTruthy()
expect(ev!.replaceRef).toBeTruthy()
expect(store.onInv).toBeUndefined()
expect(getSnapshot(store).onInv).toBeUndefined()
})
test("applying snapshot without accesing the referenced node", () => {
let ev: OnReferenceInvalidatedEvent> | undefined
let oldRefId!: string
let calls = 0
const onInv: OnReferenceInvalidated> = ev1 => {
calls++
oldRefId = ev1.invalidTarget!.id
expect(ev1.invalidId).toBe(oldRefId)
ev = ev1
ev1.removeRef()
}
const store = createStore({}, onInv)
expect(calls).toBe(0)
applySnapshot(store, createSnapshot({ onInv: "1" }))
expect(calls).toBe(0)
store.todos.splice(0, 1)
expect(calls).toBe(1)
expect(ev!.parent).toBe(store)
expect(oldRefId).toBe("1")
expect(ev!.removeRef).toBeTruthy()
expect(ev!.replaceRef).toBeTruthy()
expect(store.onInv).toBeUndefined()
expect(getSnapshot(store).onInv).toBeUndefined()
store.onInv = store.todos[0]
expect(calls).toBe(1)
store.todos.splice(0, 1)
expect(calls).toBe(2)
expect(ev!.parent).toBe(store)
expect(oldRefId).toBe("2")
expect(ev!.removeRef).toBeTruthy()
expect(ev!.replaceRef).toBeTruthy()
expect(store.onInv).toBeUndefined()
expect(getSnapshot(store).onInv).toBeUndefined()
})
test("runtime change", () => {
let ev: OnReferenceInvalidatedEvent> | undefined
let oldRefId!: string
let calls = 0
const onInv: OnReferenceInvalidated> = ev1 => {
calls++
oldRefId = ev1.invalidTarget!.id
expect(ev1.invalidId).toBe(oldRefId)
ev = ev1
ev1.removeRef()
}
const store = createStore({}, onInv)
expect(calls).toBe(0)
store.onInv = store.todos[1]
expect(calls).toBe(0)
store.onInv = store.todos[0]
expect(calls).toBe(0)
store.todos.remove(store.todos[0])
expect(calls).toBe(1)
expect(ev!.parent).toBe(store)
expect(oldRefId).toBe("1")
expect(ev!.removeRef).toBeTruthy()
expect(ev!.replaceRef).toBeTruthy()
expect(store.onInv).toBeUndefined()
expect(getSnapshot(store).onInv).toBeUndefined()
store.onInv = store.todos[0]
expect(calls).toBe(1)
store.todos.remove(store.todos[0])
expect(calls).toBe(2)
expect(ev!.parent).toBe(store)
expect(oldRefId).toBe("2")
expect(ev!.removeRef).toBeTruthy()
expect(ev!.replaceRef).toBeTruthy()
expect(store.onInv).toBeUndefined()
expect(getSnapshot(store).onInv).toBeUndefined()
})
test("replacing ref", () => {
let ev: OnReferenceInvalidatedEvent> | undefined
let oldRefId!: string
let calls = 0
const onInv: OnReferenceInvalidated> = ev1 => {
calls++
oldRefId = ev1.invalidTarget!.id
expect(ev1.invalidId).toBe(oldRefId)
ev = ev1
ev1.replaceRef(store.todos[1])
}
const store = createStore({}, onInv)
expect(calls).toBe(0)
store.onInv = store.todos[0]
expect(calls).toBe(0)
store.todos.remove(store.todos[0])
expect(calls).toBe(1)
expect(ev!.parent).toBe(store)
expect(oldRefId).toBe("1")
expect(ev!.removeRef).toBeTruthy()
expect(ev!.replaceRef).toBeTruthy()
expect(store.onInv!.id).toBe("2")
expect(getSnapshot(store).onInv).toBe("2")
})
test("cloning works", () => {
let ev: OnReferenceInvalidatedEvent> | undefined
let oldRefId!: string
let calls = 0
const onInv: OnReferenceInvalidated> = ev1 => {
calls++
oldRefId = ev1.invalidTarget!.id
expect(ev1.invalidId).toBe(oldRefId)
ev = ev1
ev1.removeRef()
}
const store1 = createStore({}, onInv)
expect(calls).toBe(0)
store1.onInv = store1.todos[0]
expect(calls).toBe(0)
const store = clone(store1)
unprotect(store)
expect(calls).toBe(0)
store.onInv = store.todos[0]
expect(calls).toBe(0)
store.todos.remove(store.todos[0])
expect(calls).toBe(1)
expect(ev!.parent).toBe(store)
expect(oldRefId).toBe("1")
expect(ev!.removeRef).toBeTruthy()
expect(ev!.replaceRef).toBeTruthy()
expect(store.onInv).toBeUndefined()
expect(getSnapshot(store).onInv).toBeUndefined()
// make sure other ref stil points to the right one
expect(store1.onInv).toBe(store1.todos[0])
})
})
}
describe("safeReference", () => {
test("model property", () => {
const store = createStore({})
expect(store.single).toBeUndefined()
store.single = store.todos[0]
expect(store.single).toBe(store.todos[0])
store.todos.remove(store.todos[0])
expect(store.single).toBeUndefined()
})
test("deep model property", () => {
const store = createStore({})
expect(store.deep.single).toBeUndefined()
store.deep.single = store.todos[0]
expect(store.deep.single).toBe(store.todos[0])
store.todos.remove(store.todos[0])
expect(store.deep.single).toBeUndefined()
})
test("array child", () => {
const store = createStore({})
expect(store.arr.length).toBe(0)
store.arr.push(store.todos[0])
store.arr.push(store.todos[2])
expect(store.arr.length).toBe(2)
expect(store.arr[0]!.id).toBe("1")
expect(store.arr[1]!.id).toBe("3")
store.todos.splice(0, 1)
expect(store.arr.length).toBe(1)
expect(store.arr[0]!.id).toBe("3")
})
test("map child", () => {
const store = createStore({})
expect(store.map.size).toBe(0)
store.map.set("a", store.todos[0])
store.map.set("c", store.todos[2])
expect(store.map.size).toBe(2)
expect(store.map.get("a")!.id).toBe("1")
expect(store.map.get("c")!.id).toBe("3")
store.todos.splice(0, 1)
expect(store.map.size).toBe(1)
expect(store.map.get("c")!.id).toBe("3")
})
test("invalid references in a snapshot should be removed", () => {
const store = createStore({ single: "100", arr: ["100", "1"], map: { a: "100", b: "1" } })
expect(store.single).toBeUndefined()
expect(store.arr.length).toBe(1)
expect(store.arr[0]!.id).toBe("1")
expect(store.map.size).toBe(1)
expect(store.map.get("b")!.id).toBe("1")
// check reassignation still works
store.single = store.todos[0]
expect(store.single).toBe(store.todos[0])
store.todos.remove(store.todos[0])
expect(store.single).toBeUndefined()
})
test("setting it to an invalid id and then accessing it should still result in an error", () => {
const store = createStore({})
store.single = "100" as any
expect(() => {
const s = store.single
}).toThrow("Failed to resolve reference")
})
})
test("#1115 - safe reference doesn't become invalidated when the reference has never been acessed", () => {
const MyRefModel = types.model("MyRefModel", {
id: types.identifier
})
const SafeRef = types.model("SafeRef", {
ref: types.safeReference(MyRefModel)
})
const RootModel = types
.model("RootModel", {
mapOfRef: types.map(MyRefModel),
arrayOfSafeRef: types.array(SafeRef)
})
.actions(self => ({
deleteSqr(id: string) {
self.mapOfRef.delete(id)
}
}))
const rootModel = RootModel.create({
mapOfRef: {
sqr1: {
id: "sqr1"
},
sqr2: {
id: "sqr2"
}
},
arrayOfSafeRef: [
{
ref: "sqr2"
},
{
ref: "sqr1"
},
{
ref: "sqr2"
}
]
})
expect(getSnapshot(rootModel.arrayOfSafeRef)).toEqual([
{
ref: "sqr2"
},
{
ref: "sqr1"
},
{
ref: "sqr2"
}
])
rootModel.deleteSqr("sqr1")
expect(getSnapshot(rootModel.arrayOfSafeRef)).toEqual([
{
ref: "sqr2"
},
{
ref: undefined
},
{
ref: "sqr2"
}
])
rootModel.deleteSqr("sqr2")
expect(getSnapshot(rootModel.arrayOfSafeRef)).toEqual([
{
ref: undefined
},
{
ref: undefined
},
{
ref: undefined
}
])
})
describe("safeReference with acceptsUndefined: false", () => {
const MyRefModel = types.model("MyRefModel", {
id: types.identifier
})
const SafeRef = types.safeReference(MyRefModel, { acceptsUndefined: false })
it("removes invalidates items from map/array", () => {
const Store = types.model({
todos: types.array(MyRefModel),
arr: types.array(SafeRef),
map: types.map(SafeRef)
})
const store = Store.create({
todos: [{ id: "1" }, { id: "2" }],
arr: ["1", "2"],
map: {
a1: "1",
a2: "2"
}
})
unprotect(store)
// just to check TS is happy with this
const arr: Instance[] = store.arr
store.todos.splice(0, 1)
expect(store.arr.length).toBe(1)
expect(store.map.size).toBe(1)
})
if (process.env.NODE_ENV !== "production") {
it("throws when a model property is invalidated", () => {
const Store = types.model({
todos: types.array(MyRefModel),
single: SafeRef
})
const store = Store.create({
todos: [{ id: "1" }, { id: "2" }],
single: "1"
})
unprotect(store)
expect(() => {
store.todos.splice(0, 1)
}).toThrow("value `undefined` is not assignable to type")
})
it("does not accept undefined in the array", () => {
const Store = types.model({
todos: types.array(MyRefModel),
arr: types.array(SafeRef)
})
expect(() =>
Store.create({
todos: [{ id: "1" }, { id: "2" }],
arr: ["1", undefined as any]
})
).toThrow("value `undefined` is not assignable to type")
})
it("does not accept undefined in the map", () => {
const Store = types.model({
todos: types.array(MyRefModel),
map: types.map(SafeRef)
})
expect(() =>
Store.create({
todos: [{ id: "1" }, { id: "2" }],
map: {
a1: "1",
a2: undefined as any
}
})
).toThrow("value `undefined` is not assignable to type")
})
}
})
test("#1275 - removing an object from a map should result in the snapshot of references being modified", () => {
const Item = types.model({
id: types.identifier
})
const Root = types.model({
items: types.map(Item),
refs: types.array(types.safeReference(Item))
})
const thing = Root.create({
items: { aa: { id: "a" }, bb: { id: "b" }, cc: { id: "c" } },
refs: ["a", "b", "c"]
})
unprotect(thing)
destroy(thing.items.get("bb")!)
expect(getSnapshot(thing.refs)).toEqual(["a", "c"])
})
================================================
FILE: __tests__/core/reference.test.ts
================================================
import { reaction, autorun, isObservable, configure } from "mobx"
import {
types,
getSnapshot,
applySnapshot,
onPatch,
applyPatch,
unprotect,
detach,
resolveIdentifier,
getRoot,
cast,
SnapshotOut,
IAnyModelType,
Instance,
SnapshotOrInstance,
isAlive,
destroy,
castToReferenceSnapshot,
tryReference,
isValidReference,
isStateTreeNode,
addDisposer
} from "../../src"
import { expect, jest, test } from "bun:test"
test("it should support prefixed paths in maps", () => {
const User = types.model({
id: types.identifier,
name: types.string
})
const UserStore = types.model({
user: types.reference(User),
users: types.map(User)
})
const store = UserStore.create({
user: "17",
users: {
"17": { id: "17", name: "Michel" },
"18": { id: "18", name: "Veria" }
}
})
unprotect(store)
expect(store.users.get("17")!.name).toBe("Michel")
expect(store.users.get("18")!.name).toBe("Veria")
expect(store.user.name).toBe("Michel")
store.user = store.users.get("18")!
expect(store.user.name).toBe("Veria")
store.users.get("18")!.name = "Noa"
expect(store.user.name).toBe("Noa")
expect(getSnapshot(store)).toEqual({
user: "18",
users: { "17": { id: "17", name: "Michel" }, "18": { id: "18", name: "Noa" } }
} as SnapshotOut)
})
test("it should support prefixed paths in arrays", () => {
const User = types.model({
id: types.identifier,
name: types.string
})
const UserStore = types.model({
user: types.reference(User),
users: types.array(User)
})
const store = UserStore.create({
user: "17",
users: [
{ id: "17", name: "Michel" },
{ id: "18", name: "Veria" }
]
})
unprotect(store)
expect(store.users[0].name).toBe("Michel")
expect(store.users[1].name).toBe("Veria")
expect(store.user.name).toBe("Michel")
store.user = store.users[1]
expect(store.user.name).toBe("Veria")
store.users[1].name = "Noa"
expect(store.user.name).toBe("Noa")
expect(getSnapshot(store)).toEqual({
user: "18",
users: [
{ id: "17", name: "Michel" },
{ id: "18", name: "Noa" }
]
} as SnapshotOut)
})
if (process.env.NODE_ENV !== "production") {
test("identifiers are required", () => {
const Todo = types.model({
id: types.identifier
})
expect(Todo.is({})).toBe(false)
expect(Todo.is({ id: "x" })).toBe(true)
expect(() => (Todo.create as any)()).toThrow(
" `undefined` is not assignable to type: `identifier` (Value is not a valid identifier, expected a string)"
)
})
test("identifiers cannot be modified", () => {
const Todo = types.model({
id: types.identifier
})
const todo = Todo.create({ id: "x" })
unprotect(todo)
expect(() => (todo.id = "stuff")).toThrow(
"[mobx-state-tree] Tried to change identifier from 'x' to 'stuff'. Changing identifiers is not allowed."
)
expect(() => applySnapshot(todo, { id: "stuff" })).toThrow(
"[mobx-state-tree] Tried to change identifier from 'x' to 'stuff'. Changing identifiers is not allowed."
)
})
}
test("it should resolve refs during creation, when using path", () => {
const values: number[] = []
const Book = types.model({
id: types.identifier,
price: types.number
})
const BookEntry = types
.model({
book: types.reference(Book)
})
.views(self => ({
get price() {
return self.book.price * 2
}
}))
const Store = types.model({
books: types.array(Book),
entries: types.optional(types.array(BookEntry), [])
})
const s = Store.create({
books: [{ id: "3", price: 2 }]
})
unprotect(s)
reaction(
() => s.entries.reduce((a, e) => a + e.price, 0),
v => values.push(v)
)
s.entries.push({ book: castToReferenceSnapshot(s.books[0]) })
expect(s.entries[0].price).toBe(4)
expect(s.entries.reduce((a, e) => a + e.price, 0)).toBe(4)
const entry = BookEntry.create({ book: castToReferenceSnapshot(s.books[0]) }) // N.B. ref is initially not resolvable!
s.entries.push(entry)
expect(s.entries[1].price).toBe(4)
expect(s.entries.reduce((a, e) => a + e.price, 0)).toBe(8)
expect(values).toEqual([4, 8])
})
test("it should resolve refs over late types", () => {
const Book = types.model({
id: types.identifier,
price: types.number
})
const BookEntry = types
.model({
book: types.reference(types.late(() => Book))
})
.views(self => ({
get price() {
return self.book.price * 2
}
}))
const Store = types.model({
books: types.array(Book),
entries: types.array(BookEntry)
})
const s = Store.create({
books: [{ id: "3", price: 2 }]
})
unprotect(s)
s.entries.push({ book: castToReferenceSnapshot(s.books[0]) })
expect(s.entries[0].price).toBe(4)
expect(s.entries.reduce((a, e) => a + e.price, 0)).toBe(4)
})
test("it should resolve refs during creation, when using generic reference", () => {
const values: number[] = []
const Book = types.model({
id: types.identifier,
price: types.number
})
const BookEntry = types
.model({
book: types.reference(Book)
})
.views(self => ({
get price() {
return self.book.price * 2
}
}))
const Store = types.model({
books: types.array(Book),
entries: types.optional(types.array(BookEntry), [])
})
const s = Store.create({
books: [{ id: "3", price: 2 }]
})
unprotect(s)
reaction(
() => s.entries.reduce((a, e) => a + e.price, 0),
v => values.push(v)
)
s.entries.push({ book: castToReferenceSnapshot(s.books[0]) })
expect(s.entries[0].price).toBe(4)
expect(s.entries.reduce((a, e) => a + e.price, 0)).toBe(4)
const entry = BookEntry.create({ book: castToReferenceSnapshot(s.books[0]) }) // can refer to book, even when not part of tree yet
expect(getSnapshot(entry)).toEqual({ book: "3" })
s.entries.push(entry)
expect(values).toEqual([4, 8])
})
test("identifiers should support subtypes of types.string and types.number", () => {
const M = types.model({
id: types.refinement(types.identifierNumber, n => n > 5)
})
expect(M.is({})).toBe(false)
expect(M.is({ id: "test" })).toBe(false)
expect(M.is({ id: "6" })).toBe(false)
expect(M.is({ id: "4" })).toBe(false)
expect(M.is({ id: 6 })).toBe(true)
expect(M.is({ id: 4 })).toBe(false)
const S = types.model({
mies: types.map(M),
ref: types.reference(M)
})
const s = S.create({ mies: { "7": { id: 7 } }, ref: "7" })
expect(s.mies.get("7")).toBeTruthy()
expect(s.ref).toBe(s.mies.get("7")!)
})
test("string identifiers should not accept numbers", () => {
const F = types.model({
id: types.identifier
})
expect(F.is({ id: "4" })).toBe(true)
expect(F.is({ id: 4 })).toBe(false)
const F2 = types.model({
id: types.identifier
})
expect(F2.is({ id: "4" })).toBe(true)
expect(F2.is({ id: 4 })).toBe(false)
})
test("122 - identifiers should support numbers as well", () => {
const F = types.model({
id: types.identifierNumber
})
expect(
F.create({
id: 3
}).id
).toBe(3)
expect(F.is({ id: 4 })).toBe(true)
expect(F.is({ id: "4" })).toBe(false)
expect(F.is({ id: "bla" })).toBe(false)
})
test("self reference with a late type", () => {
const Book = types.model("Book", {
id: types.identifier,
genre: types.string,
reference: types.reference(types.late((): IAnyModelType => Book))
})
const Store = types
.model("Store", {
books: types.array(Book)
})
.actions(self => {
function addBook(book: SnapshotOrInstance) {
self.books.push(book)
}
return {
addBook
}
})
const s = Store.create({
books: [{ id: "1", genre: "thriller", reference: "" }]
})
const book2 = Book.create({
id: "2",
genre: "romance",
reference: castToReferenceSnapshot(s.books[0])
})
s.addBook(book2)
expect((s.books[1].reference as Instance).genre).toBe("thriller")
})
test("when applying a snapshot, reference should resolve correctly if value added after", () => {
const Box = types.model({
id: types.identifierNumber,
name: types.string
})
const Factory = types.model({
selected: types.reference(Box),
boxes: types.array(Box)
})
expect(() =>
Factory.create({
selected: 1,
boxes: [
{ id: 1, name: "hello" },
{ id: 2, name: "world" }
]
})
).not.toThrow()
})
test("it should fail when reference snapshot is ambiguous", () => {
const Box = types.model("Box", {
id: types.identifierNumber,
name: types.string
})
const Arrow = types.model("Arrow", {
id: types.identifierNumber,
name: types.string
})
const BoxOrArrow = types.union(Box, Arrow)
const Factory = types.model({
selected: types.reference(BoxOrArrow),
boxes: types.array(Box),
arrows: types.array(Arrow)
})
const store = Factory.create({
selected: 2,
boxes: [
{ id: 1, name: "hello" },
{ id: 2, name: "world" }
],
arrows: [{ id: 2, name: "arrow" }]
})
expect(() => {
// tslint:disable-next-line:no-unused-expression
store.selected // store.boxes[1] // throws because it can't know if you mean a box or an arrow!
}).toThrow(
"[mobx-state-tree] Cannot resolve a reference to type '(Box | Arrow)' with id: '2' unambigously, there are multiple candidates: /boxes/1, /arrows/0"
)
unprotect(store)
// first update the reference, than create a new matching item! Ref becomes ambigous now...
store.selected = 1 as any // valid assignment
expect(store.selected).toBe(store.boxes[0]) // unambigous identifier
let err!: Error
autorun(() => store.selected, {
onError(e) {
err = e
}
})
expect(store.selected).toBe(store.boxes[0]) // unambigous identifier
store.arrows.push({ id: 1, name: "oops" })
expect(err.message).toBe(
"[mobx-state-tree] Cannot resolve a reference to type '(Box | Arrow)' with id: '1' unambigously, there are multiple candidates: /boxes/0, /arrows/1"
)
})
test("it should support array of references", () => {
const Box = types.model({
id: types.identifierNumber,
name: types.string
})
const Factory = types.model({
selected: types.array(types.reference(Box)),
boxes: types.array(Box)
})
const store = Factory.create({
selected: [],
boxes: [
{ id: 1, name: "hello" },
{ id: 2, name: "world" }
]
})
unprotect(store)
expect(() => {
store.selected.push(store.boxes[0])
}).not.toThrow()
expect(getSnapshot(store.selected)).toEqual([1])
expect(() => {
store.selected.push(store.boxes[1])
}).not.toThrow()
expect(getSnapshot(store.selected)).toEqual([1, 2])
})
test("it should restore array of references from snapshot", () => {
const Box = types.model({
id: types.identifierNumber,
name: types.string
})
const Factory = types.model({
selected: types.array(types.reference(Box)),
boxes: types.array(Box)
})
const store = Factory.create({
selected: [1, 2],
boxes: [
{ id: 1, name: "hello" },
{ id: 2, name: "world" }
]
})
unprotect(store)
expect(store.selected[0] === store.boxes[0]).toEqual(true)
expect(store.selected[1] === store.boxes[1]).toEqual(true)
})
test("it should support map of references", () => {
const Box = types.model({
id: types.identifierNumber,
name: types.string
})
const Factory = types.model({
selected: types.map(types.reference(Box)),
boxes: types.array(Box)
})
const store = Factory.create({
selected: {},
boxes: [
{ id: 1, name: "hello" },
{ id: 2, name: "world" }
]
})
unprotect(store)
expect(() => {
store.selected.set("from", store.boxes[0])
}).not.toThrow()
expect(getSnapshot(store.selected)).toEqual({ from: 1 })
expect(() => {
store.selected.set("to", store.boxes[1])
}).not.toThrow()
expect(getSnapshot(store.selected)).toEqual({ from: 1, to: 2 })
})
test("it should restore map of references from snapshot", () => {
const Box = types.model({
id: types.identifierNumber,
name: types.string
})
const Factory = types.model({
selected: types.map(types.reference(Box)),
boxes: types.array(Box)
})
const store = Factory.create({
selected: { from: 1, to: 2 },
boxes: [
{ id: 1, name: "hello" },
{ id: 2, name: "world" }
]
})
unprotect(store)
expect(store.selected.get("from") === store.boxes[0]).toEqual(true)
expect(store.selected.get("to") === store.boxes[1]).toEqual(true)
})
test("it should support relative lookups", () => {
const Node = types.model({
id: types.identifierNumber,
children: types.optional(types.array(types.late((): IAnyModelType => Node)), [])
})
const root = Node.create({
id: 1,
children: [
{
id: 2,
children: [
{
id: 4
}
]
},
{
id: 3
}
]
})
unprotect(root)
expect(getSnapshot(root)).toEqual({
id: 1,
children: [
{ id: 2, children: [{ id: 4, children: [] }] },
{ id: 3, children: [] }
]
})
expect(resolveIdentifier(Node, root, 1)).toBe(root)
expect(resolveIdentifier(Node, root, 4)).toBe(root.children[0].children[0])
expect(resolveIdentifier(Node, root.children[0].children[0], 3)).toBe(root.children[1])
const n2 = detach(root.children[0])
unprotect(n2)
expect(resolveIdentifier(Node, n2, 2)).toBe(n2)
expect(resolveIdentifier(Node, root, 2)).toBeUndefined()
expect(resolveIdentifier(Node, root, 4)).toBeUndefined()
expect(resolveIdentifier(Node, n2, 3)).toBeUndefined()
expect(resolveIdentifier(Node, n2, 4)).toBe(n2.children[0])
expect(resolveIdentifier(Node, n2.children[0], 2)).toBe(n2)
const n5 = Node.create({ id: 5 })
expect(resolveIdentifier(Node, n5, 4)).toBeUndefined()
n2.children.push(n5)
expect(resolveIdentifier(Node, n5, 4)).toBe(n2.children[0])
expect(resolveIdentifier(Node, n2.children[0], 5)).toBe(n5)
})
test("References are non-nullable by default", () => {
const Todo = types.model({
id: types.identifierNumber
})
const Store = types.model({
todo: types.maybe(Todo),
ref: types.reference(Todo),
maybeRef: types.maybe(types.reference(Todo))
})
expect(Store.is({})).toBe(false)
expect(Store.is({ ref: 3 })).toBe(true)
expect(Store.is({ ref: null })).toBe(false)
expect(Store.is({ ref: undefined })).toBe(false)
expect(Store.is({ ref: 3, maybeRef: 3 })).toBe(true)
expect(Store.is({ ref: 3, maybeRef: undefined })).toBe(true)
let store = Store.create({
todo: { id: 3 },
ref: 3
})
expect(store.ref).toBe(store.todo!)
expect(store.maybeRef).toBeUndefined()
store = Store.create({
todo: { id: 3 },
ref: 4
})
unprotect(store)
if (process.env.NODE_ENV !== "production") {
expect(store.maybeRef).toBeUndefined()
expect(() => store.ref).toThrow(
"[mobx-state-tree] Failed to resolve reference '4' to type 'AnonymousModel' (from node: /ref)"
)
store.maybeRef = 3 as any // valid assignment
expect(store.maybeRef).toBe(store.todo!)
store.maybeRef = 4 as any // valid assignment
expect(() => store.maybeRef).toThrow(
"[mobx-state-tree] Failed to resolve reference '4' to type 'AnonymousModel' (from node: /maybeRef)"
)
store.maybeRef = undefined
expect(store.maybeRef).toBe(undefined)
expect(() => ((store as any).ref = undefined)).toThrow(/Error while converting/)
}
})
test("References are described properly", () => {
const Todo = types.model({
id: types.identifierNumber
})
const Store = types.model({
todo: types.maybe(Todo),
ref: types.reference(Todo),
maybeRef: types.maybe(types.reference(Todo))
})
expect(Store.describe()).toBe(
"{ todo: ({ id: identifierNumber } | undefined?); ref: reference(AnonymousModel); maybeRef: (reference(AnonymousModel) | undefined?) }"
)
})
test("References in recursive structures", () => {
const Folder = types.model("Folder", {
id: types.identifierNumber,
name: types.string,
files: types.array(types.string)
})
const Tree = types
.model("Tree", {
// sadly, this becomes any, and further untypeable...
children: types.array(types.late((): IAnyModelType => Tree)),
data: types.maybeNull(types.reference(Folder))
})
.actions(self => {
function addFolder(data: SnapshotOrInstance) {
const folder3 = Folder.create(data)
getRoot(self).putFolderHelper(folder3)
self.children.push(
Tree.create({ data: castToReferenceSnapshot(folder3), children: [] })
)
}
return { addFolder }
})
const Storage = types
.model("Storage", {
objects: types.map(Folder),
tree: Tree
})
.actions(self => ({
putFolderHelper(aFolder: SnapshotOrInstance) {
self.objects.put(aFolder)
}
}))
const store = Storage.create({ objects: {}, tree: { children: [], data: null } })
const folder = { id: 1, name: "Folder 1", files: ["a.jpg", "b.jpg"] }
store.tree.addFolder(folder)
expect(getSnapshot(store)).toEqual({
objects: {
"1": {
files: ["a.jpg", "b.jpg"],
id: 1,
name: "Folder 1"
}
},
tree: {
children: [
{
children: [],
data: 1
}
],
data: null
}
})
expect(store.objects.get("1")).toBe(store.tree.children[0].data)
const folder2 = { id: 2, name: "Folder 2", files: ["c.jpg", "d.jpg"] }
store.tree.children[0].addFolder(folder2)
expect(getSnapshot(store)).toEqual({
objects: {
"1": {
files: ["a.jpg", "b.jpg"],
id: 1,
name: "Folder 1"
},
"2": {
files: ["c.jpg", "d.jpg"],
id: 2,
name: "Folder 2"
}
},
tree: {
children: [
{
children: [
{
children: [],
data: 2
}
],
data: 1
}
],
data: null
}
})
expect(store.objects.get("1")).toBe(store.tree.children[0].data)
expect(store.objects.get("2")).toBe(store.tree.children[0].children[0].data)
})
test("it should applyPatch references in array", () => {
const Item = types.model("Item", {
id: types.identifier,
name: types.string
})
const Folder = types
.model("Folder", {
id: types.identifier,
objects: types.map(Item),
hovers: types.array(types.reference(Item))
})
.actions(self => {
function addObject(anItem: typeof Item.Type) {
self.objects.put(anItem)
}
function addHover(anItem: typeof Item.Type) {
self.hovers.push(anItem)
}
function removeHover(anItem: typeof Item.Type) {
self.hovers.remove(anItem)
}
return {
addObject,
addHover,
removeHover
}
})
const folder = Folder.create({ id: "folder 1", objects: {}, hovers: [] })
folder.addObject({ id: "item 1", name: "item name 1" })
const item = folder.objects.get("item 1")!
const snapshot = getSnapshot(folder)
const newStore = Folder.create(snapshot)
onPatch(folder, data => {
applyPatch(newStore, data)
})
folder.addHover(item)
expect(getSnapshot(newStore)).toEqual({
id: "folder 1",
objects: {
"item 1": {
id: "item 1",
name: "item name 1"
}
},
hovers: ["item 1"]
})
folder.removeHover(item)
expect(getSnapshot(newStore)).toEqual({
id: "folder 1",
objects: {
"item 1": {
id: "item 1",
name: "item name 1"
}
},
hovers: []
})
})
test("it should applySnapshot references in array", () => {
const Item = types.model("Item", {
id: types.identifier,
name: types.string
})
const Folder = types.model("Folder", {
id: types.identifier,
objects: types.map(Item),
hovers: types.array(types.reference(Item))
})
const folder = Folder.create({
id: "folder 1",
objects: {
"item 1": {
id: "item 1",
name: "item name 1"
}
},
hovers: ["item 1"]
})
const snapshot = JSON.parse(JSON.stringify(getSnapshot(folder)))
expect(snapshot).toEqual({
id: "folder 1",
objects: {
"item 1": {
id: "item 1",
name: "item name 1"
}
},
hovers: ["item 1"]
})
snapshot.hovers = []
applySnapshot(folder, snapshot)
expect(getSnapshot(folder)).toEqual({
id: "folder 1",
objects: {
"item 1": {
id: "item 1",
name: "item name 1"
}
},
hovers: []
})
snapshot.hovers = ["item 1"]
applySnapshot(folder, snapshot)
expect(getSnapshot(folder)).toEqual({
id: "folder 1",
objects: {
"item 1": {
id: "item 1",
name: "item name 1"
}
},
hovers: ["item 1"]
})
})
test("array of references should work fine", () => {
const B = types.model("Block", { id: types.identifier })
const S = types
.model("Store", {
blocks: types.array(B),
blockRefs: types.array(types.reference(B))
})
.actions(self => {
return {
order() {
const res = self.blockRefs.slice()
self.blockRefs.replace([res[1], res[0]])
}
}
})
const a = S.create({ blocks: [{ id: "1" }, { id: "2" }], blockRefs: ["1", "2"] })
a.order()
expect(a.blocks[0].id).toBe("1")
expect(a.blockRefs[0].id).toBe("2")
})
test("should serialize references correctly", () => {
const M = types.model({
id: types.identifierNumber
})
const S = types.model({
mies: types.map(M),
ref: types.maybe(types.reference(M))
})
const s = S.create({
mies: {
7: {
id: 7
}
}
})
unprotect(s)
expect(Array.from(s.mies.keys())).toEqual(["7"])
expect(s.mies.get("7")!.id).toBe(7)
expect(s.mies.get(7 as any)).toBe(s.mies.get("7")!) // maps automatically normalizes the key
s.mies.put({
id: 8
})
expect(Array.from(s.mies.keys())).toEqual(["7", "8"])
s.ref = 8 as any
expect(s.ref!.id).toBe(8) // resolved from number
expect(getSnapshot(s).ref).toBe(8) // ref serialized as number
s.ref = "7" as any // resolved from string
expect(s.ref!.id).toBe(7) // resolved from string
expect(getSnapshot(s).ref).toBe("7") // ref serialized as string (number would be ok as well)
s.ref = s.mies.get("8")!
expect(s.ref.id).toBe(8) // resolved from instance
expect(getSnapshot(s).ref).toBe(8) // ref serialized as number
s.ref = "9" as any // unresolvable
expect(getSnapshot(s).ref).toBe("9") // snapshot preserved as it was unresolvable
s.mies.set(9 as any, {
id: 9
})
expect(Array.from(s.mies.keys())).toEqual(["7", "8", "9"])
expect(s.mies.get("9")!.id).toBe(9)
expect(getSnapshot(s).ref).toBe("9") // ref serialized as string (number would be ok as well)
})
test("#1052 - Reference returns destroyed model after subtree replacing", () => {
const Todo = types.model("Todo", {
id: types.identifierNumber,
title: types.string
})
const Todos = types.model("Todos", {
items: types.array(Todo)
})
const Store = types
.model("Store", {
todos: Todos,
last: types.maybe(types.reference(Todo)),
lastWithId: types.maybe(types.reference(Todo)),
counter: -1
})
.actions(self => ({
load() {
self.counter++
self.todos = Todos.create({
items: [
{ id: 1, title: "Get Coffee " + self.counter },
{ id: 2, title: "Write simpler code " + self.counter }
]
})
},
select(todo: Instance) {
self.last = todo
self.lastWithId = todo.id as any
}
}))
const store = Store.create({ todos: {} })
store.load()
expect(store.last).toBeUndefined()
expect(store.lastWithId).toBeUndefined()
const reactionFn = jest.fn()
const reactionDisposer = reaction(() => store.last, reactionFn)
const reactionFn2 = jest.fn()
const reactionDisposer2 = reaction(() => store.lastWithId, reactionFn2)
try {
store.select(store.todos.items[0])
expect(isAlive(store.last!)).toBe(true)
expect(isObservable(store.last)).toBe(true)
expect(reactionFn).toHaveBeenCalledTimes(1)
expect(store.last!.title).toBe("Get Coffee 0")
expect(isAlive(store.lastWithId!)).toBe(true)
expect(isObservable(store.lastWithId)).toBe(true)
expect(reactionFn2).toHaveBeenCalledTimes(1)
expect(store.lastWithId!.title).toBe("Get Coffee 0")
store.load()
expect(isAlive(store.last!)).toBe(true)
expect(isObservable(store.last)).toBe(true)
expect(reactionFn).toHaveBeenCalledTimes(2)
expect(store.last!.title).toBe("Get Coffee 1")
expect(isAlive(store.lastWithId!)).toBe(true)
expect(isObservable(store.lastWithId)).toBe(true)
expect(reactionFn2).toHaveBeenCalledTimes(2)
expect(store.lastWithId!.title).toBe("Get Coffee 1")
} finally {
reactionDisposer()
reactionDisposer2()
}
})
test("#1080 - does not crash trying to resolve a reference to a destroyed+recreated model", () => {
configure({
useProxies: "never"
})
const Branch = types.model("Branch", {
id: types.identifierNumber,
name: types.string
})
const User = types.model("User", {
id: types.identifierNumber,
email: types.maybeNull(types.string),
branches: types.maybeNull(types.array(Branch))
})
const BranchStore = types
.model("BranchStore", {
activeBranch: types.maybeNull(types.reference(Branch))
})
.actions(self => ({
setActiveBranch(branchId: any) {
self.activeBranch = branchId
}
}))
const RootStore = types
.model("RootStore", {
user: types.maybeNull(User),
branchStore: types.maybeNull(BranchStore)
})
.actions(self => ({
setUser(snapshot: typeof userSnapshot) {
self.user = cast(snapshot)
},
setBranchStore(snapshot: typeof branchStoreSnapshot) {
self.branchStore = cast(snapshot)
},
destroyUser() {
destroy(self.user!)
},
destroyBranchStore() {
destroy(self.branchStore!)
}
}))
const userSnapshot = {
id: 1,
email: "test@test.com",
branches: [
{
id: 1,
name: "Branch 1"
},
{
id: 2,
name: "Branch 2"
}
]
}
const branchStoreSnapshot = {}
const rootStore = RootStore.create({ user: userSnapshot, branchStore: branchStoreSnapshot })
rootStore.branchStore!.setActiveBranch(1)
expect(rootStore.branchStore!.activeBranch).toEqual({
id: 1,
name: "Branch 1"
})
rootStore.destroyUser()
rootStore.destroyBranchStore()
rootStore.setUser(userSnapshot)
rootStore.setBranchStore(branchStoreSnapshot)
rootStore.branchStore!.setActiveBranch(2)
expect(rootStore.branchStore!.activeBranch).toEqual({
id: 2,
name: "Branch 2"
})
})
test("tryReference / isValidReference", () => {
const Todo = types.model({ id: types.identifier })
const TodoStore = types
.model({
todos: types.array(Todo),
ref1: types.maybe(types.reference(Todo)),
ref2: types.maybeNull(types.reference(Todo)),
ref3: types.maybe(types.reference(Todo))
})
.actions(self => ({
clearRef3() {
self.ref3 = undefined
},
afterCreate() {
addDisposer(
self,
reaction(
() => isValidReference(() => self.ref3),
valid => {
if (!valid) {
this.clearRef3()
}
},
{ fireImmediately: true }
)
)
}
}))
const store = TodoStore.create({
todos: [{ id: "1" }, { id: "2" }, { id: "3" }]
})
expect(tryReference(() => store.ref1)).toBeUndefined()
expect(tryReference(() => store.ref2)).toBeUndefined()
expect(isValidReference(() => store.ref1)).toBe(false)
expect(isValidReference(() => store.ref2)).toBe(false)
unprotect(store)
store.ref1 = store.todos[0]
store.ref2 = store.todos[1]
store.ref3 = store.todos[2]
expect(isStateTreeNode(store.ref1)).toBe(true)
expect(isStateTreeNode(store.ref2)).toBe(true)
expect(tryReference(() => store.ref1)).toBeDefined()
expect(tryReference(() => store.ref2)).toBeDefined()
expect(isValidReference(() => store.ref1)).toBe(true)
expect(isValidReference(() => store.ref2)).toBe(true)
store.todos = cast([])
expect(tryReference(() => store.ref1)).toBeUndefined()
expect(tryReference(() => store.ref2)).toBeUndefined()
expect(isValidReference(() => store.ref1)).toBe(false)
expect(isValidReference(() => store.ref2)).toBe(false)
// the reaction should have triggered and set this to undefined
expect(store.ref3).toBeUndefined()
expect(() => tryReference(() => 5 as any)).toThrow(
"The reference to be checked is not one of node, null or undefined"
)
expect(() => isValidReference(() => 5 as any)).toThrow(
"The reference to be checked is not one of node, null or undefined"
)
})
test("#1162 - reference to union", () => {
const M1 = types.model({ id: types.identifier, type: types.string, sum: types.string })
const M2 = types.model({
id: types.identifier,
type: types.string,
data: types.string
})
const AnyModel = types.union(
{
dispatcher(snapshot) {
switch (snapshot.type) {
case "type1":
return M1
case "type2":
return M2
default:
throw new Error()
}
}
},
M1,
M2
)
const Store = types.model({
arr: types.array(AnyModel),
selected: types.reference(AnyModel)
})
const s = Store.create({
selected: "num1",
arr: [
{ id: "num1", type: "type1", sum: "1" },
{ id: "num2", type: "type1", sum: "2" },
{ id: "num3", type: "type2", data: "3" }
]
})
unprotect(s)
expect(s.selected.id).toBe("num1")
expect(s.selected.type).toBe("type1")
expect((s.selected as Instance).sum).toBe("1")
s.selected = "num2" as any
expect(s.selected.id).toBe("num2")
expect(s.selected.type).toBe("type1")
expect((s.selected as Instance).sum).toBe("2")
s.selected = "num3" as any
expect(s.selected.id).toBe("num3")
expect(s.selected.type).toBe("type2")
expect((s.selected as Instance).data).toBe("3")
})
================================================
FILE: __tests__/core/refinement.test.ts
================================================
import { getSnapshot, types } from "../../src"
import { expect, test } from "bun:test"
test("it should allow if type and predicate is correct", () => {
const Factory = types.model({
number: types.refinement(
"positive number",
types.optional(types.number, 0),
s => typeof s === "number" && s >= 0
)
})
const doc = Factory.create({ number: 42 })
expect(getSnapshot(doc)).toEqual({ number: 42 })
})
if (process.env.NODE_ENV !== "production") {
test("it should throw if a correct type with failing predicate is given", () => {
const Factory = types.model({
number: types.refinement(
"positive number",
types.optional(types.number, 0),
s => typeof s === "number" && s >= 0
)
})
expect(() => {
Factory.create({ number: "givenStringInstead" } as any)
}).toThrow(
`[mobx-state-tree] Error while converting \`{\"number\":\"givenStringInstead\"}\` to \`AnonymousModel\`:\n\n at path \"/number\" value \`\"givenStringInstead\"\` is not assignable to type: \`positive number\` (Value is not a number).`
)
expect(() => {
Factory.create({ number: -4 })
}).toThrow(
`[mobx-state-tree] Error while converting \`{\"number\":-4}\` to \`AnonymousModel\`:\n\n at path \"/number\" value \`-4\` is not assignable to type: \`positive number\` (Value does not respect the refinement predicate).`
)
})
test("it should throw custom error message with failing predicate is given", () => {
const Factory = types.model({
number: types.refinement(
types.optional(types.number, 0),
s => typeof s === "number" && s >= 0,
s => "A positive number was expected"
)
})
expect(() => {
Factory.create({ number: "givenStringInstead" } as any)
}).toThrow(
`[mobx-state-tree] Error while converting \`{\"number\":\"givenStringInstead\"}\` to \`AnonymousModel\`:\n\n at path \"/number\" value \`\"givenStringInstead\"\` is not assignable to type: \`number\` (Value is not a number).`
)
expect(() => {
Factory.create({ number: -4 })
}).toThrow(
`[mobx-state-tree] Error while converting \`{\"number\":-4}\` to \`AnonymousModel\`:\n\n at path "/number" value \`-4\` is not assignable to type: \`number\` (A positive number was expected).`
)
})
}
================================================
FILE: __tests__/core/reflection.test.ts
================================================
import {
types,
getMembers,
getPropertyMembers,
IAnyStateTreeNode,
getType,
IAnyModelType,
IModelReflectionData,
IModelReflectionPropertiesData,
flow
} from "../../src"
import { expect, test } from "bun:test"
const User = types.model("User", {
id: types.identifier,
name: types.string
})
const Model = types
.model({
isPerson: false,
users: types.optional(types.map(User), {}),
dogs: types.array(User),
user: types.maybe(types.late(() => User))
})
.volatile(self => ({
volatileProperty: { propName: "halo" }
}))
.actions(self => {
function actionName() {
return 1
}
return {
actionName,
generatorAction: flow(function* generatorAction() {
const promise = new Promise(resolve => {
resolve(true)
})
yield promise
})
}
})
.views(self => ({
get viewName() {
return 1
}
}))
function expectPropertyMembersToMatchMembers(
propertyMembers: IModelReflectionPropertiesData,
members: IModelReflectionData
) {
expect(propertyMembers).toEqual({
name: members.name,
properties: members.properties
})
}
test("reflection - model", () => {
const node = Model.create()
const reflection = getMembers(node)
expect(reflection.name).toBe("AnonymousModel")
expect(reflection.actions.includes("actionName")).toBe(true)
expect(reflection.actions.includes("generatorAction")).toBe(true)
expect(reflection.flowActions.includes("generatorAction")).toBe(true)
expect(reflection.flowActions.includes("actionName")).toBe(false)
expect(reflection.views.includes("viewName")).toBe(true)
expect(reflection.views.includes("actionName")).toBe(false)
expect(reflection.volatile.includes("volatileProperty")).toBe(true)
expect(!!reflection.properties.users).toBe(true)
expect(!!reflection.properties.isPerson).toBe(true)
const typeReflection = getPropertyMembers(Model)
expectPropertyMembersToMatchMembers(typeReflection, reflection)
const reflection2 = getPropertyMembers(node)
expectPropertyMembersToMatchMembers(reflection2, reflection)
})
test("reflection - map", () => {
const node = Model.create({
users: { "1": { id: "1", name: "Test" } }
})
const node2 = node.users.get("1")!
const reflection = getMembers(node2)
expect(reflection.name).toBe("User")
expect(!!reflection.properties.id).toBe(true)
expect(!!reflection.properties.name).toBe(true)
const typeReflection = getPropertyMembers(getType(node2) as IAnyModelType)
expectPropertyMembersToMatchMembers(typeReflection, reflection)
const reflection2 = getPropertyMembers(node2)
expectPropertyMembersToMatchMembers(reflection2, reflection)
})
test("reflection - array", () => {
const node = Model.create({
dogs: [{ id: "1", name: "Test" }]
})
const node2 = node.dogs[0]
const reflection = getMembers(node2)
expect(!!reflection.properties.id).toBe(true)
expect(!!reflection.properties.name).toBe(true)
const typeReflection = getPropertyMembers(getType(node2) as IAnyModelType)
expectPropertyMembersToMatchMembers(typeReflection, reflection)
const reflection2 = getPropertyMembers(node2)
expectPropertyMembersToMatchMembers(reflection2, reflection)
})
test("reflection - late", () => {
const node = Model.create({
user: { id: "5", name: "Test" }
})
const empty: IAnyStateTreeNode = {}
const reflection = getMembers(node.user || empty)
const keys = Object.keys(reflection.properties || {})
expect(keys.includes("name")).toBe(true)
expect(reflection.properties.name.describe()).toBe("string")
})
if (process.env.NODE_ENV !== "production") {
test("reflection - throw on non model node for getMembers", () => {
const node = Model.create({
users: { "1": { id: "1", name: "Test" } }
})
expect(() => (node.users ? getMembers(node.users) : {})).toThrow()
})
test("reflection - throw on non model type/node for getMembers", () => {
expect(() => getPropertyMembers(types.array(types.number) as any)).toThrow()
const node = Model.create({
users: { "1": { id: "1", name: "Test" } }
})
expect(() => getPropertyMembers(node.users)).toThrow()
})
}
test("reflection - can retrieve property names", () => {
const node = Model.create()
const reflection = getMembers(node)
const keys = Object.keys(reflection.properties)
expect(keys.includes("users")).toBe(true)
expect(keys.includes("isPerson")).toBe(true)
})
test("reflection - property contains type", () => {
const TestModel = types.model({
string: types.string,
optional: false
})
const node = TestModel.create({
string: "hello"
})
const reflection = getMembers(node)
expect(reflection.properties.string).toBe(types.string)
expect(reflection.properties.optional).toMatchObject(types.optional(types.boolean, false))
})
test("reflection - members chained", () => {
const ChainedModel = types
.model({
isPerson: false
})
.actions(self => {
return {
actionName() {
return 1
}
}
})
.actions(self => {
return {
anotherAction() {
return 1
}
}
})
.actions(self => {
function flowActionName() {
return 1
}
return {
flowActionName,
generatorAction: flow(function* generatorAction() {
const promise = new Promise(resolve => {
resolve(true)
})
yield promise
})
}
})
.views(self => ({
get viewName() {
return 1
}
}))
.views(self => ({
anotherView(prop: string) {
return 1
}
}))
const node = ChainedModel.create()
const reflection = getMembers(node)
const keys = Object.keys(reflection.properties || {})
expect(keys.includes("isPerson")).toBe(true)
expect(reflection.actions.includes("actionName")).toBe(true)
expect(reflection.actions.includes("anotherAction")).toBe(true)
expect(reflection.actions.includes("flowActionName")).toBe(true)
expect(reflection.actions.includes("generatorAction")).toBe(true)
expect(reflection.flowActions.includes("generatorAction")).toBe(true)
expect(reflection.flowActions.includes("flowActionName")).toBe(false)
expect(reflection.views.includes("viewName")).toBe(true)
expect(reflection.views.includes("anotherView")).toBe(true)
expect(reflection.views.includes("actionName")).toBe(false)
expect(reflection.views.includes("anotherAction")).toBe(false)
expect(reflection.views.includes("flowActionName")).toBe(false)
})
test("reflection - conditionals respected", () => {
let swap = true
const ConditionalModel = types
.model({
isPerson: false
})
.actions(self => ({
actionName0() {
return 1
}
}))
.actions((self): { actionName1(): number } | { actionName2(): number } => {
if (swap) {
return {
actionName1() {
return 1
}
}
} else {
return {
actionName2() {
return 1
}
}
}
})
.views(self => {
if (swap) {
return {
get view1() {
return 1
}
}
} else {
return {
get view2() {
return 1
}
}
}
})
// swap true
const node = ConditionalModel.create()
const reflection = getMembers(node)
expect(reflection.actions.includes("actionName0")).toBe(true)
expect(reflection.actions.includes("actionName1")).toBe(true)
expect(reflection.actions.includes("actionName2")).toBe(false)
expect(reflection.views.includes("view1")).toBe(true)
expect(reflection.views.includes("view2")).toBe(false)
swap = false
const node2 = ConditionalModel.create()
const reflection2 = getMembers(node2)
expect(reflection.actions.includes("actionName0")).toBe(true)
expect(reflection2.actions.includes("actionName1")).toBe(false)
expect(reflection2.actions.includes("actionName2")).toBe(true)
expect(reflection2.views.includes("view1")).toBe(false)
expect(reflection2.views.includes("view2")).toBe(true)
})
================================================
FILE: __tests__/core/snapshotProcessor.test.ts
================================================
import { observable } from "mobx"
import {
types,
getSnapshot,
unprotect,
cast,
detach,
clone,
SnapshotIn,
getNodeId,
Instance,
onSnapshot
} from "../../src"
import { describe, expect, jest, test } from "bun:test"
describe("snapshotProcessor", () => {
describe("over a model type", () => {
const M = types.model({
x: types.string
})
test("no processors", () => {
const Model = types.model({
m: types.snapshotProcessor(M, {})
})
const model = Model.create({ m: { x: "hi" } })
unprotect(model)
expect(model.m.x).toBe("hi")
expect(getSnapshot(model).m.x).toBe("hi")
// reconciliation
model.m = { x: "ho" }
expect(model.m.x).toBe("ho")
expect(getSnapshot(model).m.x).toBe("ho")
})
test("pre processor", () => {
const Model = types.model({
m: types.snapshotProcessor(M, {
preProcessor(sn: { x: number }) {
return {
...sn,
x: String(sn.x)
}
}
})
})
const model = Model.create({ m: { x: 5 } })
unprotect(model)
expect(model.m.x).toBe("5")
expect(getSnapshot(model).m.x).toBe("5")
// reconciliation
model.m = cast({ x: 6 })
expect(model.m.x).toBe("6")
expect(getSnapshot(model).m.x).toBe("6")
})
test("post processor", () => {
let model: Instance
const Model = types.model({
m: types.snapshotProcessor(M, {
postProcessor(sn, node): { x: number; val?: string } {
expect(node).toBeTruthy()
return {
...sn,
x: Number(sn.x),
val: node.x
}
}
})
})
model = Model.create({
m: { x: "5" }
})
unprotect(model)
expect(model.m.x).toBe("5")
expect(getSnapshot(model).m.x).toBe(5)
expect(getSnapshot(model).m.val).toBe("5")
// reconciliation
model.m = cast({ x: "6" })
expect(model.m.x).toBe("6")
expect(getSnapshot(model).m.x).toBe(6)
expect(getSnapshot(model).m.val).toBe("6")
})
test("post processor that observes other observables recomputes when they change", () => {
let model: Instance
const atom = observable.box("foo")
const Model = types.model({
m: types.snapshotProcessor(M, {
postProcessor(sn, node): { x: number; val: string } {
return {
...sn,
x: Number(sn.x),
val: atom.get()
}
}
})
})
model = Model.create({
m: { x: "5" }
})
const newSnapshot = jest.fn()
onSnapshot(model, newSnapshot)
expect(getSnapshot(model).m.val).toBe("foo")
atom.set("bar")
expect(getSnapshot(model).m.val).toBe("bar")
expect(newSnapshot).toHaveBeenCalledTimes(1)
})
test("pre and post processor", () => {
const Model = types.model({
m: types.snapshotProcessor(M, {
preProcessor(sn: { x: number }) {
return {
...sn,
x: String(sn.x)
}
},
postProcessor(sn): { x: number } {
return {
...sn,
x: Number(sn.x)
}
}
})
})
const model = Model.create({
m: { x: 5 }
})
unprotect(model)
expect(model.m.x).toBe("5")
expect(getSnapshot(model).m.x).toBe(5)
// reconciliation
model.m = cast({ x: 6 })
expect(model.m.x).toBe("6")
expect(getSnapshot(model).m.x).toBe(6)
// cloning
expect(getSnapshot(clone(model.m)).x).toBe(6)
})
})
describe("over a literal type", () => {
const M = types.string
test("no processors", () => {
const Model = types.model({
m: types.snapshotProcessor(M, {})
})
const model = Model.create({ m: "hi" })
unprotect(model)
expect(model.m).toBe("hi")
expect(getSnapshot(model).m).toBe("hi")
// reconciliation
model.m = "ho"
expect(model.m).toBe("ho")
expect(getSnapshot(model).m).toBe("ho")
})
test("pre processor", () => {
const Model = types.model({
m: types.snapshotProcessor(M, {
preProcessor(sn: number) {
return String(sn)
}
})
})
const model = Model.create({ m: 5 })
unprotect(model)
expect(model.m).toBe("5")
expect(getSnapshot(model).m).toBe("5")
// reconciliation
model.m = 6 as any
expect(model.m).toBe("6")
expect(getSnapshot(model).m).toBe("6")
})
test("post processor", () => {
const Model = types.model({
m: types.snapshotProcessor(M, {
postProcessor(sn, node): number {
expect(node).toMatch(/5|6/)
return Number(sn)
}
})
})
const model = Model.create({
m: "5"
})
unprotect(model)
expect(model.m).toBe("5")
expect(getSnapshot(model).m).toBe(5)
// reconciliation
model.m = "6"
expect(model.m).toBe("6")
expect(getSnapshot(model).m).toBe(6)
})
test("pre and post processor", () => {
const Model = types.model({
m: types.snapshotProcessor(M, {
preProcessor(sn: number) {
return String(sn)
},
postProcessor(sn): number {
return Number(sn)
}
})
})
const model = Model.create({
m: 5
})
unprotect(model)
expect(model.m).toBe("5")
expect(getSnapshot(model).m).toBe(5)
// reconciliation
model.m = "6"
expect(model.m).toBe("6")
expect(getSnapshot(model).m).toBe(6)
// cloning
expect(getSnapshot(clone(model)).m).toBe(6)
})
})
describe("over an array type", () => {
const M = types.array(types.string)
test("no processors", () => {
const Model = types.model({
m: types.snapshotProcessor(M, {})
})
const model = Model.create({ m: ["hi"] })
unprotect(model)
expect(model.m[0]).toBe("hi")
expect(getSnapshot(model).m[0]).toBe("hi")
// reconciliation
model.m = cast(["ho"])
expect(model.m[0]).toBe("ho")
expect(getSnapshot(model).m[0]).toBe("ho")
})
test("pre processor", () => {
const Model = types.model({
m: types.snapshotProcessor(M, {
preProcessor(sn: number[]) {
return sn.map(n => String(n))
}
})
})
const model = Model.create({ m: [5] })
unprotect(model)
expect(model.m[0]).toBe("5")
expect(getSnapshot(model).m[0]).toBe("5")
// reconciliation
model.m = cast([6])
expect(model.m[0]).toBe("6")
expect(getSnapshot(model).m[0]).toBe("6")
})
test("post processor", () => {
const Model = types.model({
m: types.snapshotProcessor(M, {
postProcessor(sn, node): number[] {
expect(node).toBeDefined()
expect(node.length).toEqual(1)
return sn.map(n => Number(n))
}
})
})
const model = Model.create({
m: ["5"]
})
unprotect(model)
expect(model.m[0]).toBe("5")
expect(getSnapshot(model).m[0]).toBe(5)
// reconciliation
model.m = cast(["6"])
expect(model.m[0]).toBe("6")
expect(getSnapshot(model).m[0]).toBe(6)
})
test("pre and post processor", () => {
const Model = types.model({
m: types.snapshotProcessor(M, {
preProcessor(sn: number[]) {
return sn.map(n => String(n))
},
postProcessor(sn): number[] {
return sn.map(n => Number(n))
}
})
})
const model = Model.create({
m: [5]
})
unprotect(model)
expect(model.m[0]).toBe("5")
expect(getSnapshot(model).m[0]).toBe(5)
// reconciliation
model.m = cast([6])
expect(model.m[0]).toBe("6")
expect(getSnapshot(model).m[0]).toBe(6)
// cloning
expect(getSnapshot(clone(model.m))[0]).toBe(6)
})
})
describe("over a map type", () => {
const M = types.map(types.string)
test("no processors", () => {
const Model = types.model({
m: types.snapshotProcessor(M, {})
})
const model = Model.create({ m: { x: "hi" } })
unprotect(model)
expect(model.m.get("x")).toBe("hi")
expect(getSnapshot(model).m.x).toBe("hi")
// reconciliation
model.m.set("x", "ho")
expect(model.m.get("x")).toBe("ho")
expect(getSnapshot(model).m.x).toBe("ho")
})
test("pre processor", () => {
const Model = types.model({
m: types.snapshotProcessor(M, {
preProcessor(sn: { x: number }) {
return {
...sn,
x: String(sn.x)
}
}
})
})
const model = Model.create({ m: { x: 5 } })
unprotect(model)
expect(model.m.get("x")).toBe("5")
expect(getSnapshot(model).m.x).toBe("5")
// reconciliation
model.m = cast({ x: 6 })
expect(model.m.get("x")).toBe("6")
expect(getSnapshot(model).m.x).toBe("6")
})
test("post processor", () => {
const Model = types.model({
m: types.snapshotProcessor(M, {
postProcessor(sn, node): { x: number } {
expect(node.size).toBe(1)
return {
...sn,
x: Number(sn.x)
}
}
})
})
const model = Model.create({
m: { x: "5" }
})
unprotect(model)
expect(model.m.get("x")).toBe("5")
expect(getSnapshot(model).m.x).toBe(5)
// reconciliation
model.m = cast({ x: "6" })
expect(model.m.get("x")).toBe("6")
expect(getSnapshot(model).m.x).toBe(6)
})
test("pre and post processor", () => {
const Model = types.model({
m: types.snapshotProcessor(M, {
preProcessor(sn: { x: number }) {
return {
...sn,
x: String(sn.x)
}
},
postProcessor(sn): { x: number } {
return {
...sn,
x: Number(sn.x)
}
}
})
})
const model = Model.create({
m: { x: 5 }
})
unprotect(model)
expect(model.m.get("x")).toBe("5")
expect(getSnapshot(model).m.x).toBe(5)
// reconciliation
model.m = cast({ x: 6 })
expect(model.m.get("x")).toBe("6")
expect(getSnapshot(model).m.x).toBe(6)
// cloning
expect(getSnapshot(clone(model.m)).x).toBe(6)
})
})
test("chained transforms", () => {
const TL = types.snapshotProcessor(types.string, {
preProcessor(sn: string) {
return sn.trimLeft()
},
postProcessor(sn): string {
return "_" + sn
}
})
const TB = types.snapshotProcessor(TL, {
preProcessor(sn: string) {
return sn.trimRight()
},
postProcessor(sn): string {
return sn + "_"
}
})
const M = types.model({
name: TB
})
const t = TB.create(" hello ")
expect(t).toBe("hello")
const m = M.create({
name: " hello "
})
expect(m.name).toBe("hello")
expect(getSnapshot(m).name).toBe("_hello_")
})
describe("moving nodes around with a pre-processor", () => {
const Task = types.model("Task", { x: types.number })
const Store = types.model({
a: types.array(
types.snapshotProcessor(
Task,
{
preProcessor(sn: { x: string }) {
return {
x: Number(sn.x)
}
}
},
"PTask"
)
),
b: types.array(Task)
})
test("moving from a to b", () => {
const s = Store.create({
a: [{ x: "1" }]
})
unprotect(s)
const n = s.a[0]
detach(n)
expect(s.a.length).toBe(0)
expect(getSnapshot(n)).toEqual({ x: 1 })
s.b.push(n)
expect(s.b.length).toBe(1)
expect(getSnapshot(s.b)).toEqual([{ x: 1 }])
})
test("moving from b to a", () => {
const s = Store.create({
b: [{ x: 1 }]
})
unprotect(s)
const n = s.b[0]
detach(n)
expect(s.b.length).toBe(0)
expect(getSnapshot(n)).toEqual({ x: 1 })
s.a.push(n)
expect(s.a.length).toBe(1)
expect(getSnapshot(s.a)).toEqual([{ x: 1 }])
})
})
describe("moving nodes around with a post-processor", () => {
const Task = types.model({ x: types.number })
const Store = types.model({
a: types.array(
types.snapshotProcessor(Task, {
postProcessor(sn): { x: string } {
return {
x: String(sn.x)
}
}
})
),
b: types.array(Task)
})
test("moving from a to b", () => {
const s = Store.create({
a: [{ x: 1 }]
})
unprotect(s)
const n = s.a[0]
detach(n)
expect(s.a.length).toBe(0)
expect(getSnapshot(n)).toEqual({ x: "1" })
s.b.push(n)
expect(s.b.length).toBe(1)
// @ts-expect-error - post processor gets applied here and messes up the types
expect(getSnapshot(s.b)).toEqual([{ x: "1" }])
})
test("moving from b to a", () => {
const s = Store.create({
b: [{ x: 1 }]
})
unprotect(s)
const n = s.b[0]
detach(n)
expect(s.b.length).toBe(0)
expect(getSnapshot(n)).toEqual({ x: 1 })
s.a.push(n)
expect(s.a.length).toBe(1)
expect(getSnapshot(s.a)).toEqual([{ x: "1" }])
})
})
describe("assigning instances works", () => {
const Todo = types.model("Todo", {
id: types.identifier
})
const TodoWithProcessor = types.snapshotProcessor(Todo, {
preProcessor(snapshot: { id: string }) {
return snapshot
}
})
const Store = types
.model("TodoStore", {
todos: types.map(TodoWithProcessor),
instance: types.optional(TodoWithProcessor, { id: "new" })
})
.actions(self => ({
addTodo(todo: { id: string }) {
self.todos.put(todo)
},
setInstance(next: { id: string }) {
self.instance = next
}
}))
test("using instances in maps work", () => {
const store = Store.create()
const todo = TodoWithProcessor.create({ id: "map" })
store.addTodo(todo)
expect(store.todos.size).toBe(1)
expect(getSnapshot(store.todos)).toEqual({ map: { id: "map" } })
})
test("using instances as values works", () => {
const store = Store.create()
const todo = TodoWithProcessor.create({ id: "map" })
store.setInstance(todo)
expect(store.instance).toBe(todo)
})
test("using the non processed type in place of the processed one works", () => {
const store = Store.create()
const todo = Todo.create({ id: "map" })
store.setInstance(todo)
expect(store.instance).toBe(todo)
})
test("using the processed type in place of the non processed one works", () => {
const store = types
.model("Store", { instance: Todo })
.actions(self => ({
setInstance(next: { id: string }) {
self.instance = next
}
}))
.create({ instance: { id: "new" } })
const todo = TodoWithProcessor.create({ id: "map" })
store.setInstance(todo)
expect(store.instance).toBe(todo)
})
})
test("cached initial snapshots are ok", () => {
const M2 = types.snapshotProcessor(types.model({ x: types.number }), {
preProcessor(sn: { x: number }) {
return { ...sn, x: 0 }
}
})
const M1 = types.model({ m2: M2 })
const M = types.model({ m1: M1 })
const m = M.create({ m1: { m2: { x: 10 } } })
expect(getSnapshot(m)).toEqual({
m1: { m2: { x: 0 } }
})
})
test("works with IType.is", () => {
const Model = types.model({ x: types.number })
const model = Model.create({ x: 1 })
expect(Model.is(model)).toBe(true)
expect(Model.is({ x: 1 })).toBe(true)
const ProcessedModel = types.snapshotProcessor(Model, {
preProcessor(sn: { y: number }) {
const copy = { ...sn, x: sn.y }
// @ts-ignore
delete copy.y
return copy
},
postProcessor(sn: { x: number }) {
const copy = { ...sn, y: sn.x }
// @ts-ignore
delete copy.x
return copy
}
})
const processedModel = ProcessedModel.create({ y: 1 })
expect(ProcessedModel.is(processedModel)).toBe(true)
expect(ProcessedModel.is({ y: 1 })).toBe(true)
expect(ProcessedModel.is(Model)).toBe(false)
})
test(".is checks instances against the underlying type", () => {
const ModelA = types.model({ x: types.number })
const ModelB = types.model({ x: types.number })
const modelA = ModelA.create({ x: 1 })
const modelB = ModelB.create({ x: 2 })
// despite having the same snapshot type, .is is false for the instance of one against the other because they are not the same type
expect(ModelA.is(modelA)).toBe(true)
expect(ModelB.is(modelA)).toBe(false)
expect(ModelA.is(modelB)).toBe(false)
expect(ModelB.is(modelB)).toBe(true)
const ProcessedModel = types.snapshotProcessor(ModelA, {})
const processedModel = ProcessedModel.create({ x: 3 })
expect(ProcessedModel.is(processedModel)).toBe(true)
expect(ModelA.is(processedModel)).toBe(true)
expect(ModelB.is(processedModel)).toBe(false)
expect(ProcessedModel.is(modelA)).toBe(true)
expect(ProcessedModel.is(modelB)).toBe(false)
})
describe("1776 - reconciliation in an array", () => {
test("model with transformed property is reconciled", () => {
const SP = types.snapshotProcessor(
types.model({
id: types.identifier,
x: types.number
}),
{
preProcessor(sn: { id: string; y: number }) {
if ("x" in sn) {
// Ensure snapshot don't run through preprocessor twice
throw new Error("sn has already been preprocessed")
}
return { id: sn.id, x: sn.y }
}
}
)
const Store = types.model({ items: types.array(SP) }).actions(self => ({
setItems(items: SnapshotIn[]) {
self.items = cast(items)
}
}))
const store = Store.create({ items: [{ id: "1", y: 0 }] })
const oldNodeId = getNodeId(store.items[0])
store.setItems([{ id: "1", y: 1 }])
expect(getNodeId(store.items[0])).toBe(oldNodeId)
})
test("model with transformed identifier attribute is reconciled", () => {
const SP = types.snapshotProcessor(
types.model({
id: types.identifier
}),
{
preProcessor(sn: { foo: string }) {
return { id: sn.foo }
}
}
)
const Store = types.model({ items: types.array(SP) }).actions(self => ({
setItems(items: SnapshotIn[]) {
self.items = cast(items)
}
}))
const store = Store.create({ items: [{ foo: "1" }] })
const oldNodeId = getNodeId(store.items[0])
store.setItems([{ foo: "1" }])
expect(getNodeId(store.items[0])).toBe(oldNodeId)
})
})
describe("single node reconcilication", () => {
test("model with transformed property is reconciled", () => {
const SP = types.snapshotProcessor(
types.model({
id: types.identifier,
x: types.number
}),
{
preProcessor(sn: { id: string; y: number }) {
if ("x" in sn) {
// Ensure snapshot don't run through preprocessor twice
throw new Error("sn has already been preprocessed")
}
return { id: sn.id, x: sn.y }
}
}
)
const Store = types.model({ item: SP }).actions(self => ({
setItem(item: SnapshotIn) {
self.item = cast(item)
}
}))
const store = Store.create({ item: { id: "1", y: 0 } })
const oldNodeId = getNodeId(store.item)
store.setItem({ id: "1", y: 1 })
expect(getNodeId(store.item)).toBe(oldNodeId)
expect(store.item.x).toBe(1)
})
test("model with transformed identifier property is reconciled", () => {
const SP = types.snapshotProcessor(
types.model({
id: types.identifier
}),
{
preProcessor(sn: { foo: string }) {
return { id: sn.foo }
}
}
)
const Store = types.model({ item: SP }).actions(self => ({
setItem(item: SnapshotIn) {
self.item = cast(item)
}
}))
const store = Store.create({ item: { foo: "1" } })
const oldNodeId = getNodeId(store.item)
store.setItem({ foo: "1" })
expect(getNodeId(store.item)).toBe(oldNodeId)
expect(store.item.id).toBe("1")
})
test("1791 - model wrapped with maybe is reconciled", () => {
const SP = types.snapshotProcessor(
types.model({
id: types.identifier,
x: types.number
}),
{
preProcessor(sn: { id: string; y: number }) {
return { id: sn.id, x: sn.y }
}
}
)
const Store = types.model({ item: types.maybe(SP) }).actions(self => ({
setItem(item: SnapshotIn) {
self.item = cast(item)
}
}))
const store = Store.create({ item: { id: "1", y: 0 } })
const oldNodeId = getNodeId(store.item!)
store.setItem({ id: "1", y: 1 })
expect(getNodeId(store.item!)).toBe(oldNodeId)
expect(store.item?.x).toBe(1)
})
test("model wrapped with optional is reconciled", () => {
const SP = types.snapshotProcessor(
types.model({
id: types.identifier,
x: types.number
}),
{
preProcessor(sn: { id: string; y: number }) {
return { id: sn.id, x: sn.y }
}
}
)
const Store = types
.model({ item: types.optional(SP, { id: "1", y: 0 }) })
.actions(self => ({
setItem(item?: SnapshotIn) {
self.item = cast(item)
}
}))
const store = Store.create()
const oldNodeId = getNodeId(store.item!)
expect(store.item?.x).toBe(0)
store.setItem({ id: "1", y: 1 })
expect(getNodeId(store.item!)).toBe(oldNodeId)
expect(store.item?.x).toBe(1)
store.setItem(undefined)
expect(getNodeId(store.item!)).toBe(oldNodeId)
expect(store.item?.x).toBe(0)
})
})
test("1777 - preProcessor wrapped in maybe accepts undefined", () => {
const SP = types.snapshotProcessor(
types.model({
id: types.identifier,
x: types.number
}),
{
preProcessor(sn: { id: string; y: number }) {
return { id: sn.id, x: sn.y }
}
}
)
const Store = types.model({ item: types.maybe(SP) }).actions(self => ({
setItem(item?: SnapshotIn) {
self.item = cast(item)
}
}))
const store = Store.create()
expect(store.item).toBeUndefined()
store.setItem({ id: "1", y: 1 })
expect(store.item?.x).toBe(1)
store.setItem(undefined)
expect(store.item).toBeUndefined()
})
test("1849 - Wrapped unions don't cause infinite recursion", () => {
const Store = types
.model({
prop: types.optional(
types.snapshotProcessor(
types.union(types.literal("a"), types.literal("b")),
{}
),
"a"
)
})
.actions(self => ({
setProp(prop: typeof self.prop) {
self.prop = prop
}
}))
const store = Store.create()
expect(store.prop).toBe("a")
expect(() => store.setProp("b")).not.toThrow()
expect(store.prop).toBe("b")
})
if (process.env.NODE_ENV !== "production") {
test("it should fail if given incorrect processor", () => {
expect(() => {
types.model({
m: types.snapshotProcessor(types.number, {
postProcessor: {} as any
})
})
}).toThrow("[mobx-state-tree] postSnapshotProcessor must be a function")
})
}
})
================================================
FILE: __tests__/core/string.test.ts
================================================
import { types } from "../../src"
import { Hook, NodeLifeCycle } from "../../src/internal"
import { describe, expect, it, test } from "bun:test"
describe("types.string", () => {
describe("methods", () => {
describe("create", () => {
describe("with no arguments", () => {
if (process.env.NODE_ENV !== "production") {
it("should throw an error in development", () => {
expect(() => {
types.string.create()
}).toThrow()
})
}
})
describe("with a string argument", () => {
it("should return a string", () => {
const s = types.string.create("foo")
expect(typeof s).toBe("string")
})
})
describe("with argument of different types", () => {
const testCases = [
null,
undefined,
1,
true,
[],
function () {},
new Date(),
/a/,
new Map(),
new Set(),
Symbol(),
new Error(),
NaN,
Infinity
]
if (process.env.NODE_ENV !== "production") {
testCases.forEach(testCase => {
it(`should throw an error when passed ${JSON.stringify(testCase)}`, () => {
expect(() => {
types.string.create(testCase as any)
}).toThrow()
})
})
}
})
})
describe("describe", () => {
it("should return the value 'string'", () => {
const description = types.string.describe()
expect(description).toBe("string")
})
})
describe("getSnapshot", () => {
it("should return the value passed in", () => {
const s = types.string.instantiate(null, "", {}, "foo")
const snapshot = types.string.getSnapshot(s)
expect(snapshot).toBe("foo")
})
})
describe("getSubtype", () => {
it("should return null", () => {
const subtype = types.string.getSubTypes()
expect(subtype).toBe(null)
})
})
describe("instantiate", () => {
if (process.env.NODE_ENV !== "production") {
describe("with invalid arguments", () => {
it("should not throw an error", () => {
expect(() => {
// @ts-ignore
types.string.instantiate()
}).not.toThrow()
})
})
}
describe("with a string argument", () => {
it("should return an object", () => {
const s = types.string.instantiate(null, "", {}, "foo")
expect(typeof s).toBe("object")
})
})
})
describe("is", () => {
describe("with a string argument", () => {
it("should return true", () => {
const result = types.string.is("foo")
expect(result).toBe(true)
})
})
describe("with argument of different types", () => {
const testCases = [
null,
undefined,
1,
true,
[],
function () {},
new Date(),
/a/,
new Map(),
new Set(),
Symbol(),
new Error(),
NaN,
Infinity
]
testCases.forEach(testCase => {
it(`should return false when passed ${JSON.stringify(testCase)}`, () => {
const result = types.string.is(testCase as any)
expect(result).toBe(false)
})
})
})
})
describe("isAssignableFrom", () => {
describe("with a string argument", () => {
it("should return true", () => {
const result = types.string.isAssignableFrom(types.string)
expect(result).toBe(true)
})
})
describe("with argument of different types", () => {
const testCases = [
types.Date,
types.boolean,
types.finite,
types.float,
types.identifier,
types.identifierNumber,
types.integer,
types.null,
types.number,
types.undefined
]
testCases.forEach(testCase => {
it(`should return false when passed ${JSON.stringify(testCase)}`, () => {
const result = types.string.isAssignableFrom(testCase as any)
expect(result).toBe(false)
})
})
})
})
// TODO: we need to test this, but to be honest I'm not sure what the expected behavior is on single string nodes.
describe.skip("reconcile", () => {})
describe("validate", () => {
describe("with a string argument", () => {
it("should return with no validation errors", () => {
const result = types.string.validate("foo", [])
expect(result).toEqual([])
})
})
describe("with argument of different types", () => {
const testCases = [
null,
undefined,
1,
true,
[],
function () {},
new Date(),
/a/,
new Map(),
new Set(),
Symbol(),
new Error(),
NaN,
Infinity
]
testCases.forEach(testCase => {
it(`should return with a validation error when passed ${JSON.stringify(
testCase
)}`, () => {
const result = types.string.validate(testCase as any, [])
expect(result).toEqual([
{
context: [],
message: "Value is not a string",
value: testCase
}
])
})
})
})
})
})
describe("properties", () => {
describe("flags", () => {
test("return the correct value", () => {
const flags = types.string.flags
expect(flags).toBe(1)
})
})
describe("identifierAttribute", () => {
// We don't have a way to set the identifierAttribute on a primitive type, so this should return undefined.
test("returns undefined", () => {
const identifierAttribute = types.string.identifierAttribute
expect(identifierAttribute).toBeUndefined()
})
})
describe("isType", () => {
test("returns true", () => {
const isType = types.string.isType
expect(isType).toBe(true)
})
})
describe("name", () => {
test('returns "string"', () => {
const name = types.string.name
expect(name).toBe("string")
})
})
})
describe("instance", () => {
describe("methods", () => {
describe("aboutToDie", () => {
it("calls the beforeDetach hook", () => {
const s = types.string.instantiate(null, "", {}, "foo")
let called = false
s.registerHook(Hook.beforeDestroy, () => {
called = true
})
s.aboutToDie()
expect(called).toBe(true)
})
})
describe("die", () => {
it("kills the node", () => {
const s = types.string.instantiate(null, "", {}, "foo")
s.die()
expect(s.isAlive).toBe(false)
})
it("should mark the node as dead", () => {
const s = types.string.instantiate(null, "", {}, "foo")
s.die()
expect(s.state).toBe(NodeLifeCycle.DEAD)
})
})
describe("finalizeCreation", () => {
it("should mark the node as finalized", () => {
const s = types.string.instantiate(null, "", {}, "foo")
s.finalizeCreation()
expect(s.state).toBe(NodeLifeCycle.FINALIZED)
})
})
describe("finalizeDeath", () => {
it("should mark the node as dead", () => {
const s = types.string.instantiate(null, "", {}, "foo")
s.finalizeDeath()
expect(s.state).toBe(NodeLifeCycle.DEAD)
})
})
describe("getReconciliationType", () => {
it("should return the correct type", () => {
const s = types.string.instantiate(null, "", {}, "foo")
const type = s.getReconciliationType()
expect(type).toBe(types.string)
})
})
describe("getSnapshot", () => {
it("should return the value passed in", () => {
const s = types.string.instantiate(null, "", {}, "foo")
const snapshot = s.getSnapshot()
expect(snapshot).toBe("foo")
})
})
describe("registerHook", () => {
it("should register a hook and call it", () => {
const s = types.string.instantiate(null, "", {}, "foo")
let called = false
s.registerHook(Hook.beforeDestroy, () => {
called = true
})
s.die()
expect(called).toBe(true)
})
})
describe("setParent", () => {
if (process.env.NODE_ENV !== "production") {
describe("with null", () => {
it("should throw an error", () => {
const s = types.string.instantiate(null, "", {}, "foo")
expect(() => {
s.setParent(null, "foo")
}).toThrow()
})
})
describe("with a parent object", () => {
it("should throw an error", () => {
const Parent = types.model({
child: types.string
})
const parent = Parent.create({ child: "foo" })
const s = types.string.instantiate(null, "", {}, "bar")
expect(() => {
// @ts-ignore
s.setParent(parent, "bar")
}).toThrow(
"[mobx-state-tree] assertion failed: scalar nodes cannot change their parent"
)
})
})
}
})
})
})
})
================================================
FILE: __tests__/core/this.test.ts
================================================
import { types } from "../../src"
import { isObservableProp, isComputedProp } from "mobx"
import { expect, test } from "bun:test"
// MWE: disabled test, `this` isn't supposed to work, and afaik nowhere advertised
test.skip("this support", () => {
const M = types
.model({ x: 5 })
.views(self => ({
get x2() {
return self.x * 2
},
get x4() {
return this.x2 * 2
},
boundTo() {
return this
},
innerBoundTo() {
return () => this
},
isThisObservable() {
return (
isObservableProp(this, "x2") &&
isObservableProp(this, "x4") &&
isObservableProp(this, "localState") &&
isComputedProp(this, "x2")
)
}
}))
.volatile(self => ({
localState: 3,
getLocalState() {
return this.localState
},
getLocalState2() {
return this.getLocalState() * 2
}
}))
.actions(self => {
return {
xBy(by: number) {
return self.x * by
},
setX(x: number) {
self.x = x
},
setThisX(x: number) {
;(this as any).x = x // this should not affect self.x
},
setXBy(x: number) {
this.setX(this.xBy(x))
},
setLocalState(x: number) {
self.localState = x
}
}
})
const mi = M.create()
expect(mi.isThisObservable()).toBe(true)
expect(mi.boundTo()).toBe(mi)
expect(mi.innerBoundTo()()).toBe(mi)
expect(mi.x).toBe(5)
mi.setX(6)
expect(mi.x).toBe(6)
mi.setXBy(2)
expect(mi.x).toBe(12)
expect(mi.x2).toBe(12 * 2)
expect(mi.x4).toBe(12 * 4)
expect(mi.xBy(2)).toBe(24)
expect(mi.localState).toBe(3)
expect(mi.getLocalState()).toBe(3)
expect(mi.getLocalState2()).toBe(3 * 2)
mi.setLocalState(6)
expect(mi.localState).toBe(6)
expect(mi.getLocalState()).toBe(6)
expect(mi.getLocalState2()).toBe(6 * 2)
mi.setLocalState(7)
expect(mi.localState).toBe(7)
// make sure attempts to modify this (as long as it is not an action) doesn't affect self
const oldX = mi.x
mi.setThisX(oldX + 1)
expect(mi.x).toBe(oldX)
})
================================================
FILE: __tests__/core/type-system.test.ts
================================================
import { describe, expect, test } from "bun:test"
import {
IAnyType,
IType,
Instance,
ModelPrimitive,
ModelPropertiesDeclaration,
SnapshotIn,
SnapshotOrInstance,
SnapshotOut,
TypeOfValue,
cast,
castToSnapshot,
getParent,
getRoot,
getSnapshot,
isArrayType,
isFrozenType,
isIdentifierType,
isLateType,
isLiteralType,
isMapType,
isModelType,
isOptionalType,
isPrimitiveType,
isReferenceType,
isRefinementType,
isStateTreeNode,
isUnionType,
types,
unprotect,
type IOptionalIType,
type ISimpleType
} from "../../src"
import type {
DatePrimitive,
IAnyComplexType,
IAnyModelType,
IArrayType,
IMapType,
IReferenceType,
IUnionType
} from "../../src/internal"
type DifferingKeys = {
[K in keyof ActualT | keyof ExpectedT]: K extends keyof ActualT
? K extends keyof ExpectedT
? Exact extends never
? K
: never
: K
: K
}[keyof ActualT | keyof ExpectedT] &
string
type NotExactErrorMessage =
ActualT extends Record
? ExpectedT extends Record
? `Mismatched property: ${DifferingKeys}`
: "Expected a non-object type, but received an object"
: ExpectedT extends Record
? "Expected an object type, but received a non-object type"
: "Types are not exactly equal"
type Exact = [A] extends [B] ? ([B] extends [A] ? A : never) : never
const assertTypesEqual = (
t: ActualT,
u: Exact extends never
? NotExactErrorMessage
: ExpectedT
): [ActualT, ExpectedT] => [t, u] as any
const _: unknown = undefined
const createTestFactories = () => {
const Box = types.model({
width: 0,
height: 0
})
const Square = types.model({
width: 0,
height: 0
})
const Cube = types.model({
width: 0,
height: 0,
depth: 0
})
return { Box, Square, Cube }
}
test("it should recognize a valid snapshot", () => {
const { Box } = createTestFactories()
expect(Box.is({ width: 1, height: 2 })).toEqual(true)
expect(Box.is({ width: 1, height: 2, depth: 3 })).toEqual(true)
})
test("it should recognize an invalid snapshot", () => {
const { Box } = createTestFactories()
expect(Box.is({ width: "1", height: "2" })).toEqual(false)
})
test("it should check valid nodes as well", () => {
const { Box } = createTestFactories()
const doc = Box.create()
expect(Box.is(doc)).toEqual(true)
})
test("it should check invalid nodes as well", () => {
const { Box } = createTestFactories()
const doc = Box.create()
expect(
types
.model({
anotherAttr: types.number
})
.is(doc)
).toEqual(false)
})
test("it should do typescript type inference correctly", () => {
const A = types
.model({
x: types.number,
y: types.maybeNull(types.string)
})
.views(self => ({
get z(): string {
return "hi"
}
}))
.actions(self => {
function method() {
const x: string = self.z + self.x + self.y
anotherMethod(x)
}
function anotherMethod(x: string) {}
return {
method,
anotherMethod
}
})
// factory is invokable
const a = A.create({ x: 2, y: "7" })
unprotect(a)
// property can be used as proper type
const z: number = a.x
// property can be assigned to correctly
a.x = 7
// wrong type cannot be assigned
// MANUAL TEST: not ok: a.x = "stuff"
// sub factories work
const B = types.model({
sub: types.maybe(A)
})
const b = B.create()
unprotect(b)
// sub fields can be reassigned
b.sub = A.create({
// MANUAL TEST not ok: z: 4,
x: 3
})
// sub fields have proper type
b.sub.x = 4
const d: string | null = b.sub.y
a.y = null
const zz: string = a.z
// Manual test not assignable:
// a.z = "test"
b.sub.method()
expect(true).toBe(true) // suppress no asserts warning
// snapshots are of the proper type
const snapshot = getSnapshot(a)
const sx: number = snapshot.x
const sy: string | null = snapshot.y
expect(sx).toBe(7)
expect(sy).toBe(null)
})
test("#66 - it should accept superfluous fields", () => {
const Item = types.model({
id: types.number,
name: types.string
})
expect(Item.is({})).toBe(false)
expect(Item.is({ id: 3 })).toBe(false)
expect(Item.is({ id: 3, name: "" })).toBe(true)
expect(Item.is({ id: 3, name: "", description: "" })).toBe(true)
const a = Item.create({ id: 3, name: "", description: "bla" } as any)
expect((a as any).description).toBe(undefined)
})
test("#66 - it should not require defaulted fields", () => {
const Item = types.model({
id: types.number,
name: types.optional(types.string, "boo")
})
expect(Item.is({})).toBe(false)
expect(Item.is({ id: 3 })).toBe(true)
expect(Item.is({ id: 3, name: "" })).toBe(true)
expect(Item.is({ id: 3, name: "", description: "" })).toBe(true)
const a = Item.create({ id: 3, description: "bla" } as any)
expect((a as any).description).toBe(undefined)
expect(a.name).toBe("boo")
})
test("#66 - it should be possible to omit defaulted fields", () => {
const Item = types.model({
id: types.number,
name: "boo"
})
expect(Item.is({})).toBe(false)
expect(Item.is({ id: 3 })).toBe(true)
expect(Item.is({ id: 3, name: "" })).toBe(true)
expect(Item.is({ id: 3, name: "", description: "" })).toBe(true)
const a = Item.create({ id: 3, description: "bla" } as any)
expect((a as any).description).toBe(undefined)
expect(a.name).toBe("boo")
})
test("#66 - it should pick the correct type of defaulted fields", () => {
const Item = types.model({
id: types.number,
name: "boo"
})
const a = Item.create({ id: 3 })
unprotect(a)
expect(a.name).toBe("boo")
if (process.env.NODE_ENV !== "production") {
expect(() => ((a as any).name = 3)).toThrow(
`[mobx-state-tree] Error while converting \`3\` to \`string\`:\n\n value \`3\` is not assignable to type: \`string\` (Value is not a string).`
)
}
})
test("cannot create factories with null values", () => {
expect(() =>
types.model({
x: null
} as any)
).toThrow()
})
test("can create factories with maybe primitives", () => {
const F = types.model({
x: types.maybeNull(types.string)
})
expect(F.is(undefined)).toBe(false)
expect(F.is({})).toBe(true)
expect(F.is({ x: null })).toBe(true)
expect(F.is({ x: "test" })).toBe(true)
expect(F.is({ x: 3 })).toBe(false)
expect(F.create().x).toBe(null)
expect(F.create({ x: undefined }).x).toBe(null)
expect(F.create({ x: "" }).x).toBe("")
expect(F.create({ x: "3" }).x).toBe("3")
})
test("it is possible to refer to a type", () => {
const Todo = types
.model({
title: types.string
})
.actions(self => {
function setTitle(v: string) {}
return {
setTitle
}
})
function x(): typeof Todo.Type {
return Todo.create({ title: "test" })
}
const z = x()
unprotect(z)
z.setTitle("bla")
z.title = "bla"
// z.title = 3 // Test manual: should give compile error
expect(true).toBe(true) // suppress no asserts warning
})
test(".Type should not be callable", () => {
const Todo = types
.model({
title: types.string
})
.actions(self => {
function setTitle(v: string) {}
return {
setTitle
}
})
expect(() => Todo.Type).toThrow()
})
test(".SnapshotType should not be callable", () => {
const Todo = types
.model({
title: types.string
})
.actions(self => {
function setTitle(v: string) {}
return {
setTitle
}
})
expect(() => Todo.SnapshotType).toThrow()
})
test("types instances with compatible snapshots should not be interchangeable", () => {
const A = types.model("A", {}).actions(self => {
function doA() {}
return {
doA
}
})
const B = types.model("B", {}).actions(self => {
function doB() {}
return {
doB
}
})
const C = types.model("C", {
x: types.maybe(A)
})
expect(A.is({})).toBe(true)
expect(A.is(B.create())).toBe(false) // if this yielded true, then `B.create().doA()` should work!
expect(A.is(getSnapshot(B.create()))).toBe(true)
const c = C.create()
unprotect(c)
expect(() => {
c.x = undefined
}).not.toThrow()
expect(() => {
c.x = cast({})
}).not.toThrow()
expect(() => {
c.x = A.create()
}).not.toThrow()
expect(() => {
;(c as any).x = B.create()
}).toThrow()
})
test("it handles complex types correctly", () => {
const Todo = types
.model({
title: types.string
})
.actions(self => {
function setTitle(v: string) {}
return {
setTitle
}
})
const Store = types
.model({
todos: types.map(Todo)
})
.views(self => {
function getActualAmount() {
return self.todos.size
}
return {
get amount() {
return getActualAmount()
},
getAmount(): number {
return self.todos.size + getActualAmount()
}
}
})
.actions(self => {
function setAmount() {
const x: number = self.todos.size + self.amount + self.getAmount()
}
return {
setAmount
}
})
expect(true).toBe(true) // suppress no asserts warning
})
if (process.env.NODE_ENV !== "production") {
test("it should provide detailed reasons why the value is not applicable", () => {
const Todo = types
.model({
title: types.string
})
.actions(self => {
function setTitle(v: string) {}
return {
setTitle
}
})
const Store = types
.model({
todos: types.map(Todo)
})
.views(self => ({
get amount() {
return self.todos.size
},
getAmount(): number {
return self.todos.size + self.todos.size
}
}))
.actions(self => {
function setAmount() {
const x: number = self.todos.size + self.amount + self.getAmount()
}
return {
setAmount
}
})
expect(() =>
Store.create({
todos: { "1": { title: true, setTitle: "hello" } },
amount: 1,
getAmount: "hello"
} as any)
).toThrow(
// MWE: TODO: Ideally (like in MST =< 0.9):
// at path "/todos/1/setTitle" value \`"hello"\` is not assignable (Action properties should not be provided in the snapshot).
// at path "/amount" value \`1\` is not assignable (Computed properties should not be provided in the snapshot).
// at path "/getAmount" value \`"hello"\` is not assignable (View properties should not be provided in the snapshot).`
`[mobx-state-tree] Error while converting \`{"todos":{"1":{"title":true,"setTitle":"hello"}},"amount":1,"getAmount":"hello"}\` to \`AnonymousModel\`:
at path "/todos/1/title" value \`true\` is not assignable to type: \`string\` (Value is not a string).`
)
})
}
test("it should type compose correctly", () => {
const Car = types
.model({
wheels: 3
})
.actions(self => {
let connection = null as any as Promise
function drive() {}
function afterCreate() {
connection = Promise.resolve(true)
}
return {
drive,
afterCreate
}
})
const Logger = types
.model({
logNode: "test"
})
.actions(self => {
function log(msg: string) {}
return {
log
}
})
const LoggableCar = types.compose(Car, Logger)
const x = LoggableCar.create({ wheels: 3, logNode: "test" /* compile error: x: 7 */ })
// x.test() // compile error
x.drive()
x.log("z")
})
test("it should extend {pre,post}ProcessSnapshot on compose", () => {
const CompositionTracker = types
.model({
composedOf: types.array(types.string),
composedWith: types.array(types.string)
})
.preProcessSnapshot(snapshot => ({
...snapshot,
composedOf: (snapshot.composedOf || []).concat("CompositionTracker")
}))
.postProcessSnapshot(snapshot => ({
...snapshot,
composedWith: (snapshot.composedWith || []).concat("WagonTracker")
}))
const Car = types
.model({})
.preProcessSnapshot(snapshot => ({
...snapshot,
composedOf: ((snapshot as any).composedOf || []).concat("Car")
}))
.postProcessSnapshot(snapshot => ({
...snapshot,
composedWith: ((snapshot as any).composedWith || []).concat("Wagon")
}))
const Logger = types
.model({})
.preProcessSnapshot(snapshot => ({
...snapshot,
composedOf: ((snapshot as any).composedOf || []).concat("CarLogger")
}))
.postProcessSnapshot(snapshot => ({
...snapshot,
composedWith: ((snapshot as any).composedWith || []).concat("WagonLogger")
}))
const LoggableCar = types.compose(CompositionTracker, Car, Logger).props({
composedOf: types.array(types.string),
composedWith: types.array(types.string)
})
const x = LoggableCar.create({})
expect(x.composedOf).toContain("CompositionTracker")
expect(x.composedOf).toContain("Car")
expect(x.composedOf).toContain("CarLogger")
expect(x.composedOf.toJSON()).toEqual(["CompositionTracker", "Car", "CarLogger"])
expect(getSnapshot(x).composedWith).toContain("WagonTracker")
expect(getSnapshot(x).composedWith).toContain("Wagon")
expect(getSnapshot(x).composedWith).toContain("WagonLogger")
expect(getSnapshot(x).composedWith).toEqual(["WagonTracker", "Wagon", "WagonLogger"])
})
test("it should extend types correctly", () => {
const Car = types
.model({
wheels: 3
})
.actions(self => {
function drive() {}
return {
drive
}
})
const Logger = types
.model("Logger")
.props({
logNode: "test"
})
.actions(self => {
let connection: Promise
return {
log(msg: string) {},
afterCreate() {
connection = Promise.resolve(true)
}
}
})
const LoggableCar = types.compose("LoggableCar", Car, Logger)
const x = LoggableCar.create({ wheels: 3, logNode: "test" /* compile error: x: 7 */ })
// x.test() // compile error
x.drive()
x.log("z")
})
test("self referring views", () => {
const Car = types.model({ x: 3 }).views(self => {
const views = {
get triple() {
return self.x + views.double
},
get double() {
return self.x * 2
}
}
return views
})
expect(Car.create().triple).toBe(9)
})
test("#922", () => {
expect(() => {
const Stateable = types.model("Statable", {
state: types.optional(
types.enumeration("state", ["initalized", "pending", "done", "error"]),
"initalized"
)
})
const Client = types.model("Client", {
id: types.identifierNumber,
name: types.string
})
const UserClientList = types.compose(
"UserClientList",
Stateable,
types.model({
items: types.array(Client),
month: types.optional(types.Date, () => {
return new Date()
})
})
)
const NonExtendedUserClientList = types.model("NonExtendedUserClientList", {
items: types.array(Client),
month: types.optional(types.Date, () => {
return new Date()
}),
state: types.optional(
types.enumeration("state", ["initalized", "pending", "done", "error"]),
"initalized"
)
})
const User = types.model("User", {
name: types.string,
clients: types.optional(UserClientList, () => UserClientList.create({}))
})
const NonExtendedUser = types.model("User", {
name: types.string,
clients: types.optional(NonExtendedUserClientList, () =>
NonExtendedUserClientList.create({})
)
})
const you = NonExtendedUser.create({
name: "you"
})
const me = User.create({
name: "me"
})
}).not.toThrow()
})
test("#922 - 2", () => {
expect(() => {
types.optional(types.enumeration("state", ["init", "pending", "done", "error"]), "init")
}).not.toThrow()
})
test("#932", () => {
interface MyInterface {
test: string
}
const MyModel = types.model("MyModel", {
myField: types.array(types.frozen())
})
const x = MyModel.create({ myField: [{ test: "stuff" }] })
const a: string = x.myField[0].test
})
test("932 - 2", () => {
type MyType = string
const ModelA = types.model("ModelA", {
myField: types.maybe(types.frozen())
})
const x = ModelA.create({})
const y = x.myField // y is string | undefined
const ModelA2 = types.model("ModelA", {
myField: types.frozen()
})
const x2 = ModelA2.create({
myField: "test" // mandatory
})
const y2: string = x2.myField // string only
})
test("#923", () => {
const Foo = types.model("Foo", {
name: types.optional(types.string, "")
})
const Bar = types.model("Bar", {
foos: types.optional(types.array(Foo), [])
})
types.optional(types.map(Bar), {}) // Should have no compile error!
})
test("snapshot type of reference must be string | number", () => {
const M = types.model({ id: types.identifier, a: "bar" })
const R = types.reference(M)
const S = types.model({ realM: M, refM: R })
const s = S.create({
realM: { id: "5" },
refM: "5"
})
const sn: string | number = getSnapshot(s.refM)
})
test("#951", () => {
const C = types.model({ a: 123 })
// model as root
const ModelWithC = types.model({ c: C })
const modelInstance = ModelWithC.create({ c: C.create() })
// getRoot
const modelRoot1 = getRoot(modelInstance.c)
const modelCR1: Instance = modelRoot1.c
const modelRoot2 = getRoot>(modelInstance.c)
const modelCR2: Instance = modelRoot2.c
// getParent
const modelParent1 = getParent(modelInstance.c)
const modelCP1: Instance = modelParent1
const modelParent2 = getParent>(modelInstance.c)
const modelCP2: Instance = modelParent2
// array as root
const ArrayOfC = types.array(C)
const arrayInstance = ArrayOfC.create([C.create()])
// getRoot
const arrayRoot1 = getRoot(arrayInstance[0])
const arrayCR1: Instance = arrayRoot1[0]
// getParent
const arrayParent1 = getParent(arrayInstance[0])
const arrayCP1: Instance = arrayParent1
// map as root
const MapOfC = types.map(C)
const mapInstance = MapOfC.create({ a: C.create() })
// getRoot
const mapRoot1 = getRoot(mapInstance.get("a")!)
const mapC1: Instance = mapRoot1.get("a")!
// getParent
const mapParent1 = getRoot(mapInstance.get("a")!)
const mapCP1: Instance = mapParent1
})
test("cast and SnapshotOrInstance", () => {
const NumberArray = types.array(types.number)
const NumberMap = types.map(types.number)
const A = types
.model({ n: 123, n2: types.number, arr: NumberArray, map: NumberMap })
.actions(self => ({
// for primitives (although not needed)
setN(nn: SnapshotOrInstance) {
self.n = cast(nn)
},
setN2(nn: SnapshotOrInstance) {
self.n = cast(nn)
},
setN3(nn: SnapshotOrInstance) {
self.n = cast(nn)
},
setN4(nn: number) {
self.n = cast(nn)
},
setN5() {
self.n = cast(5)
},
// for arrays
setArr(nn: SnapshotOrInstance) {
self.arr = cast(nn)
},
setArr2(nn: SnapshotOrInstance) {
self.arr = cast(nn)
},
setArr3(nn: SnapshotIn) {
self.arr = cast(nn)
},
setArr31(nn: number[]) {
self.arr = cast(nn)
},
setArr4() {
// it works even without specifying the target type, magic!
self.arr = cast([2, 3, 4])
self.arr = cast(NumberArray.create([2, 3, 4]))
},
// for maps
setMap(nn: SnapshotOrInstance) {
self.map = cast(nn)
},
setMap2(nn: SnapshotOrInstance) {
self.map = cast(nn)
},
setMap3(nn: SnapshotIn) {
self.map = cast(nn)
},
setMap31(nn: { [k: string]: number }) {
self.map = cast(nn)
},
setMap4() {
// it works even without specifying the target type, magic!
self.map = cast({ a: 2, b: 3 })
self.map = cast(NumberMap.create({ a: 2, b: 3 }))
}
}))
const C = types
.model({ a: A, maybeA: types.maybe(A), maybeNullA: types.maybeNull(A) })
.actions(self => ({
// for submodels, using typeof self.var
setA(na: SnapshotOrInstance) {
self.a = cast(na)
// we just want to check it compiles
if (0 !== 0) {
self.maybeA = cast(na)
self.maybeNullA = cast(na)
}
},
// for submodels, using the type directly
setA2(na: SnapshotOrInstance) {
self.a = cast(na)
// we just want to check it compiles
if (0 !== 0) {
self.maybeA = cast(na)
self.maybeNullA = cast(na)
}
},
setA3(na: SnapshotIn) {
self.a = cast(na)
// we just want to check it compiles
if (0 !== 0) {
self.maybeA = cast(na)
self.maybeNullA = cast(na)
}
},
setA4(na: Instance) {
self.a = cast(na)
// we just want to check it compiles
if (0 !== 0) {
self.maybeA = cast(na)
self.maybeNullA = cast(na)
}
},
setA5() {
// it works even without specifying the target type, magic!
self.a = cast({ n2: 5 })
self.a = cast(A.create({ n2: 5 }))
// we just want to check it compiles
if (0 !== 0) {
self.maybeA = cast({ n2: 5 })
self.maybeA = cast(A.create({ n2: 5 }))
self.maybeNullA = cast({ n2: 5 })
self.maybeNullA = cast(A.create({ n2: 5 }))
}
}
}))
const c = C.create({ a: { n2: 5 } })
unprotect(c)
// all below works
c.setA({ n2: 5 })
c.setA(A.create({ n2: 5 }))
c.setA2({ n2: 5 })
c.setA2(A.create({ n2: 5 }))
c.setA3({ n2: 5 })
// c.setA3(A.create({ n2: 5 })) // this one doesn't work (as expected, it wants the creation type)
// c.setA4({n2: 5}) // this one doesn't work (as expected, it wants the instance type)
c.setA4(A.create({ n2: 5 }))
c.setA5()
c.a.setN(1)
c.a.setN2(1)
c.a.setN3(1)
c.a.setN4(1)
c.a.setN5()
c.a.setArr([])
c.a.setArr(NumberArray.create([]))
c.a.setArr2([])
c.a.setArr2(NumberArray.create([]))
c.a.setArr3([])
c.a.setArr3(NumberArray.create([]))
c.a.setArr4()
c.a.setMap({ a: 2, b: 3 })
c.a.setMap(NumberMap.create({ a: 2, b: 3 }))
c.a.setMap2({ a: 2, b: 3 })
c.a.setMap2(NumberMap.create({ a: 2, b: 3 }))
c.a.setMap3({ a: 2, b: 3 })
// c.a.setMap3(NumberMap.create({ a: 2, b: 3 })) // doesn't work (as expected, wants a plain object)
c.a.setMap4()
const arr = types.array(A).create()
unprotect(arr)
arr[0] = cast({ n2: 5 })
const map = types.map(A).create()
unprotect(map)
map.set("a", cast({ n2: 5 })) // not really needed in this case, but whatever :)
// this does not compile, yay!
/*
cast([])
cast({ a: 5 })
cast(NumberArray.create([]))
cast(A.create({ n2: 5 }))
cast({ a: 2, b: 5 })
cast(NumberMap.create({ a: 2, b: 3 }))
*/
})
test("#994", () => {
const Cinema = types.model("Cinema", {
id: types.identifier,
name: types.maybe(types.string)
})
const ref = types.reference(Cinema) // should compile ok on TS3
})
test("castToSnapshot", () => {
const firstModel = types.model({ brew1: types.map(types.number) })
const secondModel = types.model({ brew2: types.map(firstModel) }).actions(self => ({ do() {} }))
const appMod = types.model({ aaa: secondModel })
const storeSnapshot: SnapshotIn = {
brew2: { outside: { brew1: { inner: 222 } } }
}
const storeInstance = secondModel.create(storeSnapshot)
const storeSnapshotOrInstance1: SnapshotOrInstance =
secondModel.create(storeSnapshot)
const storeSnapshotOrInstance2: SnapshotOrInstance = storeSnapshot
appMod.create({ aaa: castToSnapshot(storeInstance) })
appMod.create({ aaa: castToSnapshot(storeSnapshot) })
appMod.create({ aaa: castToSnapshot(storeSnapshotOrInstance1) })
appMod.create({ aaa: castToSnapshot(storeSnapshotOrInstance2) })
// appMod.create({ aaa: castToSnapshot(5) }) // should not compile
})
test("create correctly chooses if the snapshot is needed or not - #920", () => {
const X = types.model({
test: types.string
})
const T = types.model({
test: types.refinement(X, s => s.test.length > 5)
})
// T.create() // manual test: expects compilation error
// T.create({}) // manual test: expects compilation error
T.create({
test: { test: "hellothere" }
})
const T2 = types.model({
test: types.maybe(X)
})
T2.create() // ok
T2.create({}) // ok
const A = types.model({
test: "bla"
})
A.create() // ok
A.create({}) // ok
const B = types.array(types.string)
B.create() // ok
B.create(["hi"]) // ok
const C = types.map(types.string)
C.create() // ok
C.create({ hi: "hi" }) // ok
const D = types.number
// D.create() // manual test: expects compilation error
D.create(5) // ok
const E = types.optional(types.number, 5)
E.create() // ok
E.create(6) // ok
const F = types.frozen()
// F.create() // manual test: compilation error
F.create(6) // ok
const FF = types.frozen()
FF.create() // ok
FF.create(undefined) // ok
const G = types.frozen(5)
G.create() // ok
G.create(6) // ok
const H = types.frozen(5)
H.create() // ok
H.create(6) // ok
const I = types.optional(types.frozen(), 6)
I.create()
I.create(7)
})
test("#1117", () => {
const Failsafe = (
t: IType,
handleProblem: (
value: C,
validationError: ReturnType["validate"]>
) => void = (value, error) => {
console.error("Skipping value: typecheck error on", value)
console.error(error)
}
) =>
types.custom({
name: `Failsafe<${t.name}>`,
fromSnapshot(snapshot: C) {
try {
return t.create(snapshot) // this should compile
} catch (e) {
handleProblem(snapshot, e as any)
return null
}
},
toSnapshot(x) {
if (isStateTreeNode(x)) return getSnapshot(x)
return x as any as C
},
isTargetType(v): v is T | null {
if (isFrozenType(t)) {
return t.is(v)
}
return false
},
getValidationMessage() {
return ""
}
})
})
test("MST array type should be assignable to plain array type", () => {
{
const Todo = types
.model({
done: false,
name: types.string
})
.actions(self => ({
toggleDone() {
self.done = !self.done
}
}))
const TodoArray = types.array(Todo)
const todoArray = TodoArray.create([{ done: true, name: "todo1" }, { name: "todo2" }])
unprotect(todoArray)
const otherTodoArray: Array> = todoArray
otherTodoArray.push(cast({ done: false, name: "todo2" }))
}
{
const T = types.model({
a: types.optional(types.array(types.number), [])
})
const arr: Array = T.create().a
}
{
const T = types.model({
a: types.optional(types.array(types.number), [], [5])
})
const arr: Array = T.create({
a: 5
}).a
}
})
test("can get snapshot from submodel (submodel is IStateNodeTree", () => {
const T = types.model({
a: types.model({ x: 5 })
})
const t = T.create({ a: {} })
const sn = getSnapshot(t.a).x
})
test("can extract type from complex objects", () => {
const T = types.maybe(
types.model({
a: types.model({
x: 5
})
})
)
const t = T.create({
a: {}
})!
type OriginalType = TypeOfValue
const T2: OriginalType = T
})
test("#1268", () => {
const Book = types.model({
id: types.identifier
})
const BooksStore = types.model({
books: types.array(types.reference(Book))
})
const RootStore = types.model({
booksStore: BooksStore
})
const booksStore = BooksStore.create({ books: [] })
const rootStore = RootStore.create({ booksStore: castToSnapshot(booksStore) })
})
test("#1307 optional can be omitted in .create", () => {
const Model1 = types.model({ name: types.optional(types.string, "") })
const model1 = Model1.create({})
assertTypesEqual(model1.name, _ as string)
const Model2 = types.model({ name: "" })
const model2 = Model2.create({})
assertTypesEqual(model2.name, _ as string)
})
test("#1307 custom types failing", () => {
const createCustomType = ({
CustomType
}: {
CustomType: ICustomType
}) => {
return types
.model("Example", {
someProp: types.boolean,
someType: CustomType
})
.views(self => ({
get isSomePropTrue(): boolean {
return self.someProp
}
}))
}
})
test("#1343", () => {
function createTypeA(t: T) {
return types.model("TypeA", t).views(self => ({
get someView() {
return null
}
}))
}
function createTypeB(t: T) {
return types
.model("TypeB", {
a: createTypeA(t)
})
.views(self => ({
get someViewFromA() {
return self.a.someView
}
}))
}
})
test("#1330", () => {
const ChildStore = types
.model("ChildStore", {
foo: types.string,
bar: types.boolean
})
.views(self => ({
get root(): IRootStore {
return getRoot(self)
}
}))
.actions(self => ({
test() {
const { childStore } = self.root
// childStore and childStore.foo is properly inferred in TS 3.4 but not in 3.5
console.log(childStore.foo)
}
}))
interface IRootStore extends Instance {}
const RootStore = types.model("RootStore", {
childStore: ChildStore,
test: ""
})
assertTypesEqual(
RootStore.create({
childStore: {
foo: "a",
bar: true
}
}).childStore.root.test,
_ as string
)
})
test("maybe / optional type inference verification", () => {
const T = types.model({
a: types.string,
b: "test",
c: types.maybe(types.string),
d: types.maybeNull(types.string),
e: types.optional(types.string, "test")
})
interface ITC extends SnapshotIn {}
interface ITS extends SnapshotOut {}
assertTypesEqual(
_ as ITC,
_ as {
a: string
b?: string
c?: string | undefined
d?: string | null
e?: string
}
)
assertTypesEqual(
_ as ITS,
_ as {
a: string
b: string
c: string | undefined
d: string | null
e: string
}
)
})
test("object creation with no props", () => {
const MyType = types.model().views(_self => ({
get test() {
return 5
}
}))
MyType.create()
MyType.create({})
// Instances can be created with their own instance type
MyType.create(MyType.create())
// Instances can be created with a snapshot of themselves
true || MyType.create(getSnapshot(MyType.create()))
// TODO @ts-expect-error -- symbols aren't props (but may be one day)
// This currently is allowed, because excess property checking doesn't happen against symbols.
// See https://github.com/microsoft/TypeScript/issues/44794
true || MyType.create({ [Symbol("test")]: 5 })
// @ts-expect-error -- this is a view, not a prop
true || MyType.create({ test: 5 })
// @ts-expect-error -- unknown prop
true || MyType.create({ another: 5 })
})
test("object creation when composing with a model with no props", () => {
const EmptyType = types.model().views(_self => ({
get test() {
return 5
}
}))
const NonEmptyType = types.model({
value: types.optional(types.number, 0),
negate: types.boolean
})
const Composed = types.compose(EmptyType, NonEmptyType)
Composed.create({ negate: true })
Composed.create({ negate: false })
Composed.create({ value: 5, negate: true })
// Instances can be created with their own instance type
Composed.create(Composed.create({ negate: true }))
// @ts-expect-error -- symbols aren't props (but may be one day)
true || Composed.create({ [Symbol("test")]: 5 })
// @ts-expect-error -- this is a view, not a prop
true || Composed.create({ test: 5 })
// @ts-expect-error -- unknown prop
true || Composed.create({ another: 5 })
})
test("union type inference verification for small number of types", () => {
const T = types.union(types.boolean, types.literal("test"), types.maybe(types.number))
type ITC = SnapshotIn
type ITS = SnapshotOut
assertTypesEqual(_ as ITC, _ as boolean | "test" | number | undefined)
assertTypesEqual(_ as ITS, _ as boolean | "test" | number | undefined)
})
test("union type inference verification for a large number of types", () => {
const T = types.union(
types.literal("a"),
types.literal("b"),
types.literal("c"),
types.literal("d"),
types.literal("e"),
types.literal("f"),
types.literal("g"),
types.literal("h"),
types.literal("i"),
types.literal("j")
)
type ITC = SnapshotIn
type ITS = SnapshotOut
assertTypesEqual(_ as ITC, _ as "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j")
assertTypesEqual(_ as ITS, _ as "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j")
})
test("#2186 substitutability type verification for model types extending a common base", () => {
const BaseType = types.model()
const SubTypeOptional = BaseType.props({ a: "" })
const SubTypeRequired = BaseType.props({ a: types.string })
const SubTypeRequiredWithAnotherOptional = SubTypeRequired.props({ a: types.string, b: 5 })
true || ((t: typeof BaseType) => t.create())(SubTypeOptional)
true ||
((t: typeof SubTypeRequired) => t.create({ a: "123" }))(SubTypeRequiredWithAnotherOptional)
true ||
((t: typeof BaseType) => t.create())(
// @ts-expect-error -- a is required
SubTypeRequired
)
})
test("#2184 - type narrowing functions should narrow to the expected type", () => {
const type: unknown = null
if (isOptionalType(type)) {
assertTypesEqual(type, _ as IOptionalIType)
} else if (isUnionType(type)) {
assertTypesEqual(type, _ as IUnionType)
} else if (isFrozenType(type)) {
assertTypesEqual(type, _ as ISimpleType)
} else if (isMapType(type)) {
assertTypesEqual(type, _ as IMapType)
} else if (isArrayType(type)) {
assertTypesEqual(type, _ as IArrayType)
} else if (isModelType(type)) {
assertTypesEqual(type, _ as IAnyModelType)
} else if (isLiteralType(type)) {
assertTypesEqual(type, _ as ISimpleType)
} else if (isPrimitiveType(type)) {
assertTypesEqual(
type,
_ as
| ISimpleType
| ISimpleType
| ISimpleType
| typeof types.bigint
| typeof DatePrimitive
)
} else if (isReferenceType(type)) {
assertTypesEqual(type, _ as IReferenceType)
} else if (isIdentifierType(type)) {
assertTypesEqual(type, _ as ISimpleType | ISimpleType)
} else if (isRefinementType(type)) {
assertTypesEqual(type, _ as IAnyType)
} else if (isLateType(type)) {
assertTypesEqual(type, _ as IAnyType)
}
})
describe("for snapshotProcessor", () => {
const Model = types.model({ name: types.optional(types.string, "string") })
test("produces the right types when not customized", () => {
const Processor = types.snapshotProcessor(Model, {
preProcessor(snapshot) {
assertTypesEqual(snapshot, _ as SnapshotIn)
return snapshot
},
postProcessor(snapshot) {
assertTypesEqual(snapshot, _ as { name: string })
return snapshot
}
})
type ITC = SnapshotIn
type ITS = SnapshotOut
assertTypesEqual(_ as ITC, _ as SnapshotIn)
assertTypesEqual(_ as ITS, _ as SnapshotOut)
})
test("produces the right types when customized", () => {
const Processor = types.snapshotProcessor(Model, {
preProcessor(snapshot) {
assertTypesEqual(snapshot, _ as string)
return Model.create({ name: snapshot })
},
postProcessor(snapshot) {
assertTypesEqual(snapshot, _ as SnapshotOut)
return snapshot.name.length
}
})
type ITC = SnapshotIn
type ITS = SnapshotOut
assertTypesEqual(_ as ITC, _ as string)
assertTypesEqual(_ as ITS, _ as number)
})
})
test("#1627 - union dispatch function is typed", () => {
const model = types.model({ a: types.string })
const _union = types.union(
{
dispatcher(snapshot) {
assertTypesEqual(
snapshot,
_ as SnapshotIn | SnapshotIn
)
return snapshot?.a ? model : types.null
}
},
model,
types.null
)
const _brokenUnion = types.union(
{
// @ts-expect-error -- types.string isn't a valid type for the union
dispatcher(snapshot) {
assertTypesEqual(
snapshot,
_ as SnapshotIn | SnapshotIn
)
return snapshot?.a ? model : types.string
}
},
model,
types.null
)
})
================================================
FILE: __tests__/core/union.test.ts
================================================
import { configure } from "mobx"
import {
types,
hasParent,
tryResolve,
getSnapshot,
applySnapshot,
getType,
setLivelinessChecking,
SnapshotIn,
Instance,
IAnyType
} from "../../src"
import { describe, expect, it, test, beforeEach } from "bun:test"
const createTestFactories = () => {
const Box = types.model("Box", {
width: types.number,
height: types.number
})
const Square = types.model("Square", {
width: types.number
})
const Cube = types.model("Cube", {
width: types.number,
height: types.number,
depth: types.number
})
const Plane = types.union(Square, Box)
const Heighed = types.union(Box, Cube)
const DispatchPlane = types.union(
{ dispatcher: snapshot => (snapshot && "height" in snapshot ? Box : Square) },
Box,
Square
)
const Block = types.model("Block", {
list: types.array(Heighed)
})
return { Box, Square, Cube, Plane, DispatchPlane, Heighed, Block }
}
const createLiteralTestFactories = () => {
const Man = types.model("Man", { type: types.literal("M") })
const Woman = types.model("Woman", { type: types.literal("W") })
const All = types.model("All", { type: types.string })
const ManWomanOrAll = types.union(Man, Woman, All)
return { Man, Woman, All, ManWomanOrAll }
}
if (process.env.NODE_ENV !== "production") {
test("it should complain about multiple applicable types no dispatch method", () => {
const { Box, Square } = createTestFactories()
const PlaneNotEager = types.union({ eager: false }, Square, Box)
expect(() => {
PlaneNotEager.create({ width: 2, height: 2 })
}).toThrow(/Error while converting/)
})
}
test("it should have parent whenever creating or applying from a complex data structure to a model which has Union typed children", () => {
const { Block, Heighed } = createTestFactories()
const block = Block.create({
list: [{ width: 2, height: 2 }]
})
const child = tryResolve(block, "./list/0")
expect(hasParent(child)).toBe(true)
})
if (process.env.NODE_ENV !== "production") {
test("it should complain about no applicable types", () => {
const { Heighed } = createTestFactories()
expect(() => {
Heighed.create({ height: 2 } as any)
}).toThrow(/Error while converting/)
})
}
test("it should be smart enough to discriminate by keys", () => {
const { Box, Plane, Square } = createTestFactories()
const doc = types.union(Square, Box).create({ width: 2 })
expect(Box.is(doc)).toEqual(false)
expect(Square.is(doc)).toEqual(true)
})
test("it should discriminate by value type", () => {
const Size = types.model("Size", {
width: 0,
height: 0
})
const Picture = types.model("Picture", {
url: "",
size: Size
})
const Square = types.model("Square", {
size: 0
})
const PictureOrSquare = types.union(Picture, Square)
const doc = PictureOrSquare.create({ size: { width: 0, height: 0 } })
expect(Picture.is(doc)).toEqual(true)
expect(Square.is(doc)).toEqual(false)
})
test("it should compute exact union types", () => {
const { Box, Plane, Square } = createTestFactories()
expect(Plane.is(Box.create({ width: 3, height: 2 }))).toEqual(true)
expect(Plane.is(Square.create({ width: 3 }))).toEqual(true)
})
test("it should compute exact union types - 2", () => {
const { Box, DispatchPlane, Square } = createTestFactories()
expect(DispatchPlane.is(Box.create({ width: 3, height: 2 }))).toEqual(true)
expect(
DispatchPlane.is(
Square.create({ width: 3, height: 2 } as any /* incorrect type, superfluous attr!*/)
)
).toEqual(true)
})
test("it should use dispatch to discriminate", () => {
const { Box, DispatchPlane, Square } = createTestFactories()
const a = DispatchPlane.create({ width: 3 })
expect(getSnapshot(a)).toEqual({ width: 3 })
})
test("it should eagerly match by ambiguos value", () => {
const { ManWomanOrAll, All, Man } = createLiteralTestFactories()
const person = ManWomanOrAll.create({ type: "Z" })
expect(All.is(person)).toEqual(true)
expect(Man.is(person)).toEqual(false)
})
test("it should eagerly match by ambiguos value - 2", () => {
const { All, Man } = createLiteralTestFactories()
const person = types.union(All, Man).create({ type: "M" })
expect(All.is(person)).toEqual(true)
expect(Man.is(person)).toEqual(false) // not matched, All grabbed everything!
})
test("it should eagerly match by value literal", () => {
const { ManWomanOrAll, All, Man } = createLiteralTestFactories()
const person = ManWomanOrAll.create({ type: "M" })
expect(All.is(person)).toEqual(false)
expect(Man.is(person)).toEqual(true)
})
test("dispatch", () => {
const Odd = types
.model({
value: types.number
})
.actions(self => ({
isOdd() {
return true
},
isEven() {
return false
}
}))
const Even = types.model({ value: types.number }).actions(self => ({
isOdd() {
return false
},
isEven() {
return true
}
}))
const Num = types.union(
{ dispatcher: snapshot => (snapshot.value % 2 === 0 ? Even : Odd) },
Even,
Odd
)
expect(Num.create({ value: 3 }).isOdd()).toBe(true)
expect(Num.create({ value: 3 }).isEven()).toBe(false)
expect(Num.create({ value: 4 }).isOdd()).toBe(false)
expect(Num.create({ value: 4 }).isEven()).toBe(true)
if (process.env.NODE_ENV !== "production") {
expect(() => {
types.union(
((snapshot: any) => (snapshot.value % 2 === 0 ? Even : Odd)) as any, // { dispatcher: snapshot => (snapshot.value % 2 === 0 ? Even : Odd) },
Even,
Odd
)
}).toThrow("expected object")
}
})
test("961 - apply snapshot to union should not throw when union keeps models with different properties and snapshot is got by getSnapshot", () => {
const Foo = types.model({ foo: 1 })
const Bar = types.model({ bar: 1 })
const U = types.union(Foo, Bar)
const u = U.create({ foo: 1 })
applySnapshot(u, getSnapshot(Bar.create()))
})
describe("1045 - secondary union types with applySnapshot and ids", () => {
function initTest(
useSnapshot: boolean,
useCreate: boolean,
submodel1First: boolean,
type: number
) {
setLivelinessChecking("error")
const Submodel1NoSP = types.model("Submodel1", {
id: types.identifier,
extraField1: types.string,
extraField2: types.maybe(types.string)
})
const Submodel1SP = types.snapshotProcessor(Submodel1NoSP, {
preProcessor(sn: SnapshotIn>) {
const { id, extraField1, extraField2 } = sn
return {
id,
extraField1: extraField1.toUpperCase(),
extraField2: extraField2?.toUpperCase()
}
}
})
const Submodel2NoSP = types.model("Submodel2", {
id: types.identifier,
extraField1: types.maybe(types.string),
extraField2: types.string
})
const Submodel2SP = types.snapshotProcessor(Submodel2NoSP, {
preProcessor(sn: SnapshotIn>) {
const { id, extraField1, extraField2 } = sn
return {
id,
extraField1: extraField1?.toUpperCase(),
extraField2: extraField2.toUpperCase()
}
}
})
const Submodel1 = useSnapshot ? Submodel1SP : Submodel1NoSP
const Submodel2 = useSnapshot ? Submodel2SP : Submodel2NoSP
const Submodel = submodel1First
? types.union(Submodel1, Submodel2)
: types.union(Submodel2, Submodel1)
const Model = types.array(Submodel)
const store = Model.create([{ id: "id1", extraField1: "extraField1" }])
return {
store,
applySn: function () {
const sn1 = {
id: "id1",
extraField1: "new extraField1",
extraField2: "some value"
}
const sn2 = {
id: "id1",
extraField1: undefined,
extraField2: "some value"
}
const sn = type === 1 ? sn1 : sn2
const submodel = type === 1 ? Submodel1 : Submodel2
const expected = useSnapshot
? {
id: sn.id,
extraField1: sn.extraField1?.toUpperCase(),
extraField2: sn.extraField2?.toUpperCase()
}
: sn
applySnapshot(store, [useCreate ? (submodel as any).create(sn) : sn])
expect(store.length).toBe(1)
expect(store[0]).toEqual(expected)
expect(getType(store[0])).toBe(
useSnapshot ? (submodel.getSubTypes() as IAnyType) : submodel
)
}
}
}
for (const useSnapshot of [false, true]) {
describe(useSnapshot ? "with snapshotProcessor" : "without snapshotProcessor", () => {
for (const submodel1First of [true, false]) {
describe(submodel1First ? "submodel1 first" : "submodel2 first", () => {
for (const useCreate of [false, true]) {
describe(useCreate ? "using create" : "not using create", () => {
for (const type of [2, 1]) {
describe(`snapshot is of type Submodel${type}`, () => {
beforeEach(() => {
configure({
useProxies: "never"
})
})
it(`apply snapshot works when the node is not touched`, () => {
const t = initTest(
useSnapshot,
useCreate,
submodel1First,
type
)
t.applySn()
})
it(`apply snapshot works when the node is touched`, () => {
const t = initTest(
useSnapshot,
useCreate,
submodel1First,
type
)
// tslint:disable-next-line:no-unused-expression
t.store[0]
t.applySn()
})
})
}
})
}
})
}
})
}
})
================================================
FILE: __tests__/core/volatile.test.ts
================================================
import { types, getSnapshot, recordPatches, unprotect } from "../../src"
import { reaction, isObservableProp, isObservable, autorun, observable } from "mobx"
import { expect, test } from "bun:test"
const Todo = types
.model({
done: false
})
.volatile(self => ({
state: Promise.resolve(1)
}))
.actions(self => ({
toggle() {
self.done = !self.done
},
reload() {
self.state = Promise.resolve(2)
}
}))
test("Properties should be readable and writable", () => {
const i = Todo.create()
expect(i.state instanceof Promise).toBe(true)
i.reload()
expect(i.state instanceof Promise).toBe(true)
})
test("VS should not show up in snapshots", () => {
expect(getSnapshot(Todo.create())).toEqual({ done: false })
})
test("VS should not show up in patches", () => {
const i = Todo.create()
const r = recordPatches(i)
i.reload()
i.toggle()
r.stop()
expect(r.patches).toEqual([{ op: "replace", path: "/done", value: true }])
})
test("VS be observable", () => {
const promises: Promise[] = []
const i = Todo.create()
const d = reaction(
() => i.state,
p => promises.push(p)
)
i.reload()
i.reload()
expect(promises.length).toBe(2)
d()
})
test("VS should not be deeply observable", () => {
const i = types
.model({})
.volatile(self => ({
x: { a: 1 }
}))
.create()
unprotect(i)
expect(isObservableProp(i, "x")).toBe(true)
expect(isObservable(i.x)).toBe(false)
expect(isObservableProp(i.x, "a")).toBe(false)
i.x = { a: 2 }
expect(isObservableProp(i, "x")).toBe(true)
expect(isObservable(i.x)).toBe(false)
expect(isObservableProp(i.x, "a")).toBe(false)
})
test("VS should not be strongly typed observable", () => {
const i = Todo.create()
// TEST: type error i.state = 7
i.state.then(() => {}) // it's a promise
// TEST: not available on snapshot: getSnapshot(i).state
expect(true).toBe(true)
})
test("VS should not be modifiable without action", () => {
const i = Todo.create()
expect(() => {
i.state = Promise.resolve(4)
}).toThrow(/the object is protected and can only be modified by using an action/)
})
test("VS should expect a function as an argument", () => {
expect(() => {
const t = types
.model({})
// @ts-ignore
.volatile({ state: 1 })
.create()
}).toThrow(`You passed an object to volatile state as an argument, when function is expected`)
})
test("VS should not be modifiable when unprotected", () => {
const i = Todo.create()
unprotect(i)
const p = Promise.resolve(7)
expect(() => {
i.state = p
}).not.toThrow()
expect(i.state === p).toBe(true)
})
test("VS sample from the docs should work (1)", () => {
const T = types.model({}).extend(self => {
const localState = observable.box(3)
return {
views: {
get x() {
return localState.get()
}
},
actions: {
setX(value: number) {
localState.set(value)
}
}
}
})
const t = T.create()
expect(t.x).toBe(3)
t.setX(5)
expect(t.x).toBe(5)
// now observe it
const observed: number[] = []
const dispose = autorun(() => {
observed.push(t.x)
})
t.setX(7)
expect(t.x).toBe(7)
expect(observed).toEqual([5, 7])
dispose()
})
test("VS sample from the docs should work (2)", () => {
const T = types.model({}).extend(self => {
let localState = 3
return {
views: {
getX() {
return localState
}
},
actions: {
setX(value: number) {
localState = value
}
}
}
})
const t = T.create()
expect(t.getX()).toBe(3)
t.setX(5)
expect(t.getX()).toBe(5)
// now observe it (should not be observable)
const observed: number[] = []
const dispose = autorun(() => {
observed.push(t.getX())
})
t.setX(7)
expect(t.getX()).toBe(7)
expect(observed).toEqual([5])
dispose()
})
================================================
FILE: __tests__/perf/fixture-data.skip.ts
================================================
import { rando, createHeros, createMonsters, createTreasure } from "./fixtures/fixture-data"
import { Hero, Monster, Treasure } from "./fixtures/fixture-models"
import { expect, test } from "bun:test"
test("createHeros", () => {
const data = createHeros(10)
expect(data.length).toBe(10)
const hero = Hero.create(data[0])
expect(hero.descriptionLength > 1).toBe(true)
})
test("createMonsters", () => {
const data = createMonsters(10, 10, 10)
expect(data.length).toBe(10)
expect(data[1].treasures.length).toBe(10)
expect(data[0].eatenHeroes.length).toBe(10)
const monster = Monster.create(data[0])
expect(monster.eatenHeroes && monster.eatenHeroes.length === 10).toBe(true)
expect(monster.treasures.length === 10).toBe(true)
})
test("createTreasure", () => {
const data = createTreasure(10)
expect(data.length).toBe(10)
const treasure = Treasure.create(data[1])
expect(treasure.gold > 0).toBe(true)
})
test("rando sorting", () => {
// i'm going straight to hell for this test... must get coverage to 100%.... no matter the cost.
let foundTrue = false
let foundFalse = false
let result
do {
result = rando()
if (result) {
foundTrue = true
} else {
foundFalse = true
}
} while (!foundTrue || !foundFalse)
expect(foundTrue).toBe(true)
expect(foundFalse).toBe(true)
})
================================================
FILE: __tests__/perf/fixture-models.skip.ts
================================================
import { Hero, Monster, Treasure } from "./fixtures/fixture-models"
import { expect, test } from "bun:test"
const mst = require("../../dist/mobx-state-tree.umd")
const { unprotect } = mst
const SAMPLE_HERO = {
id: 1,
name: "jimmy",
level: 1,
role: "cleric",
description: "hi"
}
test("Hero computed fields", () => {
const hero = Hero.create(SAMPLE_HERO)
expect(hero.descriptionLength).toBe(2)
})
test("Tresure", () => {
const treasure = Treasure.create({ gold: 1, trapped: true })
expect(treasure.trapped).toBe(true)
expect(treasure.gold).toBe(1)
})
test("Monster computed fields", () => {
const monster = Monster.create({
id: "foo",
level: 1,
maxHp: 3,
hp: 1,
warning: "boo!",
createdAt: new Date(),
treasures: [
{ gold: 2, trapped: true },
{ gold: 3, trapped: true }
],
eatenHeroes: [SAMPLE_HERO],
hasFangs: true,
hasClaws: true,
hasWings: true,
hasGrowl: true,
freestyle: null
})
expect(monster.isAlive).toBe(true)
expect(monster.isFlashingRed).toBe(true)
unprotect(monster)
expect(monster.weight).toBe(2)
monster.level = 0
monster.hasFangs = false
monster.hasWings = false
monster.eatenHeroes = null
expect(monster.weight).toBe(1)
monster.hp = 0
expect(monster.isFlashingRed).toBe(false)
expect(monster.isAlive).toBe(false)
})
================================================
FILE: __tests__/perf/fixtures/fixture-data.ts
================================================
import { HeroRoles } from "./fixture-models"
/**
* Creates data containing very few fields.
*
* @param count The number of items to create.
*/
export function createTreasure(count: number) {
const data = []
let i = 0
do {
data.push({
trapped: i % 2 === 0,
gold: ((count % 10) + 1) * 10
})
i++
} while (i < count)
return data
}
// why yes i DID graduate high school, why do you ask?
export const rando = () => (Math.random() > 0.5 ? 1 : 0)
const titles = ["Sir", "Lady", "Baron von", "Baroness", "Captain", "Dread", "Fancy"].sort(rando)
const givenNames = ["Abe", "Beth", "Chuck", "Dora", "Ernie", "Fran", "Gary", "Haily"].sort(rando)
const epicNames = ["Amazing", "Brauny", "Chafed", "Dapper", "Egomaniac", "Foul"].sort(rando)
const wtf = `Daenerys Stormborn of the House Targaryen, First of Her Name, the Unburnt,
Queen of the Andals and the First Men, Khaleesi of the Great Grass Sea, Breaker of Chains,
and Mother of Dragons. `
/**
* Creates data with a medium number of fields and data.
*
* @param count The number of items to create.
*/
export function createHeros(count: number) {
const data = []
let i = 0
let even = true
let n1
let n2
let n3
do {
n1 = titles[i % titles.length]
n2 = givenNames[i % givenNames.length]
n3 = epicNames[i % epicNames.length]
data.push({
id: i,
name: `${n1} ${n2} the ${n3}`,
level: (count % 100) + 1,
role: HeroRoles[i % HeroRoles.length],
description: `${wtf} ${wtf} ${wtf}`
})
even = !even
i++
} while (i < count)
return data
}
/**
* Creates data with a large number of fields and data.
*
* @param count The number of items to create.
* @param treasureCount The number of small children to create.
* @param heroCount The number of medium children to create.
*/
export function createMonsters(count: number, treasureCount: number, heroCount: number) {
const data = []
let i = 0
let even = true
do {
const treasures = createTreasure(treasureCount)
const eatenHeroes = createHeros(heroCount)
data.push({
id: `omg-${i}-run!`,
freestyle: `${wtf} ${wtf} ${wtf}${wtf} ${wtf} ${wtf}`,
level: (count % 100) + 1,
hp: i % 2 === 0 ? 1 : 5 * i,
maxHp: 5 * i,
warning: "!!!!!!",
createdAt: new Date(),
hasFangs: even,
hasClaws: even,
hasWings: !even,
hasGrowl: !even,
fearsFire: even,
fearsWater: !even,
fearsWarriors: even,
fearsClerics: !even,
fearsMages: even,
fearsThieves: !even,
stenchLevel: i % 5,
treasures,
eatenHeroes
})
even = !even
i++
} while (i < count)
return data
}
================================================
FILE: __tests__/perf/fixtures/fixture-models.ts
================================================
const mst = require("../../../dist/mobx-state-tree.umd")
const { types } = mst
// tiny
export const Treasure = types.model("Treasure", {
trapped: types.boolean,
gold: types.optional(types.number, 0)
})
// medium
export const HeroRoles = ["warrior", "wizard", "cleric", "thief"]
export const Hero = types
.model("Hero", {
id: types.identifierNumber,
name: types.string,
description: types.string,
level: types.optional(types.number, 1),
role: types.union(...exports.HeroRoles.map(types.literal))
})
.views((self: any) => ({
get descriptionLength() {
return self.description.length
}
}))
// large
export const Monster = types
.model("Monster", {
id: types.identifier,
freestyle: types.frozen(),
level: types.number,
maxHp: types.number,
hp: types.number,
warning: types.maybeNull(types.string),
createdAt: types.maybeNull(types.Date),
treasures: types.optional(types.array(exports.Treasure), []),
eatenHeroes: types.maybeNull(types.array(exports.Hero)),
hasFangs: types.optional(types.boolean, false),
hasClaws: types.optional(types.boolean, false),
hasWings: types.optional(types.boolean, false),
hasGrowl: types.optional(types.boolean, false),
stenchLevel: types.optional(types.number, 0),
fearsFire: types.optional(types.boolean, false),
fearsWater: types.optional(types.boolean, false),
fearsWarriors: types.optional(types.boolean, false),
fearsClerics: types.optional(types.boolean, false),
fearsMages: types.optional(types.boolean, false),
fearsThieves: types.optional(types.boolean, false),
fearsProgrammers: types.optional(types.boolean, true)
})
.views((self: any) => ({
get isAlive() {
return self.hp > 0
},
get isFlashingRed() {
return self.hp > 0 && self.hp < self.maxHp && self.hp === 1
},
get weight() {
const victimWeight = self.eatenHeroes ? self.eatenHeroes.length : 0
const fangWeight = self.hasFangs ? 10 : 5
const wingWeight = self.hasWings ? 12 : 4
return (victimWeight + fangWeight + wingWeight) * self.level > 5 ? 2 : 1
}
}))
================================================
FILE: __tests__/perf/perf.skip.ts
================================================
import { smallScenario, mediumScenario, largeScenario } from "./scenarios"
import { start } from "./timer"
import { expect, test } from "bun:test"
// TODO: Not sure how this should work. This feels super fragile.
const TOO_SLOW_MS = 10000
test("performs well on small scenario", () => {
expect(smallScenario(10).elapsed < TOO_SLOW_MS).toBe(true)
})
test("performs well on medium scenario", () => {
expect(mediumScenario(10).elapsed < TOO_SLOW_MS).toBe(true)
})
test("performs well on large scenario", () => {
expect(largeScenario(10, 0, 0).elapsed < TOO_SLOW_MS).toBe(true)
expect(largeScenario(10, 10, 0).elapsed < TOO_SLOW_MS).toBe(true)
expect(largeScenario(10, 0, 10).elapsed < TOO_SLOW_MS).toBe(true)
expect(largeScenario(10, 10, 10).elapsed < TOO_SLOW_MS).toBe(true)
})
test("timer", async () => {
const go = start()
await new Promise(resolve => setTimeout(resolve, 2))
const lap = go(true)
await new Promise(resolve => setTimeout(resolve, 2))
const d = go()
expect(lap).not.toBe(0)
expect(d).not.toBe(0)
expect(lap).not.toBe(d)
})
================================================
FILE: __tests__/perf/report.ts
================================================
import { smallScenario, mediumScenario, largeScenario } from "./scenarios"
// here's what we'll be testing
const plan = [
"-----------",
"Small Model",
"-----------",
() => smallScenario(100),
() => smallScenario(1000),
() => smallScenario(10000),
() => smallScenario(1000),
() => smallScenario(100),
"",
"------------",
"Medium Model",
"------------",
() => mediumScenario(100),
() => mediumScenario(1000),
() => mediumScenario(10000),
() => mediumScenario(1000),
() => mediumScenario(100),
"",
"------------------------",
"Large Model - 0 children",
"------------------------",
() => largeScenario(100, 0, 0),
() => largeScenario(1000, 0, 0),
() => largeScenario(100, 0, 0),
"",
"-------------------------------------------",
"Large Model - 10 small & 10 medium children",
"-------------------------------------------",
() => largeScenario(50, 10, 10),
() => largeScenario(250, 10, 10),
() => largeScenario(50, 10, 10),
"",
"-------------------------------------------",
"Large Model - 100 small & 0 medium children",
"-------------------------------------------",
() => largeScenario(50, 100, 0),
() => largeScenario(250, 100, 0),
() => largeScenario(50, 100, 0),
"",
"-------------------------------------------",
"Large Model - 0 small & 100 medium children",
"-------------------------------------------",
() => largeScenario(50, 0, 100),
() => largeScenario(250, 0, 100),
() => largeScenario(50, 0, 100)
]
// burn a few to get the juices flowing
smallScenario(1000)
mediumScenario(500)
largeScenario(100, 10, 10)
// remember when this broke the internet?
function leftPad(value: string, length: number, char = " "): string {
return value.toString().length < length ? leftPad(char + value, length) : value
}
// let's start
plan.forEach(fn => {
// strings get printed, i guess.
if (typeof fn === "string") {
console.log(fn)
return
}
// trigger awkward gc up front if we can
if (global.gc) {
global.gc()
}
// run the report
const result = fn()
// calculate some fields
const seconds = leftPad((result.elapsed / 1.0).toLocaleString(), 8)
const times = leftPad(`x ${result.count.toLocaleString()}`, 10)
const avg = leftPad((result.elapsed / result.count).toFixed(1), 4)
// print
console.log(`${seconds}ms | ${times} | ${avg}ms avg`)
})
console.log("")
================================================
FILE: __tests__/perf/scenarios.ts
================================================
import { start } from "./timer"
import { Treasure, Hero, Monster } from "./fixtures/fixture-models"
import { createTreasure, createHeros, createMonsters } from "./fixtures/fixture-data"
/**
* Covers models with a trivial number of fields.
*
* @param count The number of records to create.
*/
export function smallScenario(count: number) {
const data = createTreasure(count) // ready?
const time = start()
const converted = data.map(d => Treasure.create(d)) // go
const elapsed = time()
const sanity = converted.length === count
return { count, elapsed, sanity }
}
/**
* Covers models with a moderate number of fields + 1 computed field.
*
* @param count The number of records to create.
*/
export function mediumScenario(count: number) {
const data = createHeros(count) // ready?
const time = start()
const converted = data.map(d => Hero.create(d)) // go
const elapsed = time()
const sanity = converted.length === count
return { count, elapsed, sanity }
}
/**
* Covers models with a large number of fields.
*
* @param count The number of records to create.
* @param smallChildren The number of small children contained within.
* @param mediumChildren The number of medium children contained within.
*/
export function largeScenario(count: number, smallChildren: number, mediumChildren: number) {
const data = createMonsters(count, smallChildren, mediumChildren) // ready?
const time = start()
const converted = data.map(d => Monster.create(d)) // go
const elapsed = time()
const sanity = converted.length === count
return { count, elapsed, sanity }
}
================================================
FILE: __tests__/perf/timer.ts
================================================
/**
* Start a timer which return a function, which when called show the
* number of milliseconds since it started.
*
* Passing true will give the current lap time.
*
* Example:
* ```ts
* const time = start()
* // 1 second later
* time() // 1.00
* // 1 more second later
* time() // 2.00
* time(true) // 1.00
* ```
*/
export const start = () => {
const started = process.hrtime()
let last: [number, number] = [started[0], started[1]]
return (lapTime = false) => {
const final = process.hrtime(lapTime ? last : started)
return Math.round((final[0] * 1e9 + final[1]) / 1e6)
}
}
================================================
FILE: __tests__/setup.ts
================================================
import { beforeEach, afterEach, mock } from "bun:test"
import { resetNextActionId, setLivelinessChecking } from "../src/internal"
import { configure } from "mobx"
beforeEach(() => {
setLivelinessChecking("warn")
resetNextActionId()
})
afterEach(() => {
mock.restore()
// Some tests turn off proxy support, so ensure it's always turned back on
configure({
useProxies: "always"
})
})
================================================
FILE: __tests__/tsconfig.json
================================================
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "../",
"module": "commonjs",
"lib": ["es2017", "es2020.bigint", "dom"],
"target": "es6",
"sourceMap": false,
"noEmit": true
},
"include": ["**/*.ts"]
}
================================================
FILE: __tests__/utils.test.ts
================================================
import { MstError } from "../src/utils"
import { describe, expect, test } from "bun:test"
describe("MstError custom error class", () => {
test("with default message", () => {
const error = new MstError()
expect(error.message).toBe("[mobx-state-tree] Illegal state")
})
test("with custom message", () => {
const customMessage = "custom error message"
const error = new MstError(customMessage)
expect(error.message).toBe(`[mobx-state-tree] ${customMessage}`)
})
test("instance of MstError", () => {
const error = new MstError()
expect(error).toBeInstanceOf(MstError)
expect(error).toBeInstanceOf(Error)
})
})
================================================
FILE: bunfig.toml
================================================
[test]
root = "./__tests__"
preload = ["./__tests__/setup.ts"]
================================================
FILE: changelog.md
================================================
The manually-updated changelog has been discontinued. For versions > 4.0.0, go here to view changes:
[https://github.com/mobxjs/mobx-state-tree/releases](https://github.com/mobxjs/mobx-state-tree/releases)
# 4.0.0
[BREAKING CHANGE] MST 4.0 requires MobX 6
# 3.17.3
Add onValidated support to safeReference [#1540 by @orlovcs](https://github.com/mobxjs/mobx-state-tree/pull/1540)
# 3.17.2
Fix incorrect access to global `fail` symbol [#1549](https://github.com/mobxjs/mobx-state-tree/pull/1549)
# 3.17.1
Re-release 3.17.0
# 3.17.0
- Added experimental helpers toGenerator and toGeneratorFunction. [#1543](https://github.com/mobxjs/mobx-state-tree/pull/1543) by [@fruitraccoon](https://github.com/fruitraccoon)
# 3.16.0
- Added search field to the docs
- Custom types can now receive environments as second argument of the `fromSnapshot` option. [#1410](https://github.com/mobxjs/mobx-state-tree/pull/1410) by [@k-g-a](https://github.com/k-g-a)
- Added option `maxHistoryLength` to the `UndoManager`, implements [#1417](https://github.com/mobxjs/mobx-state-tree/issues/1417) through [#1426](https://github.com/mobxjs/mobx-state-tree/pull/1426) by [@tibotiber](https://github.com/tibotiber).
- Improved TypeScript typings of `flow`, fixes [#1378](https://github.com/mobxjs/mobx-state-tree/pull/1378) through [#1409](https://github.com/mobxjs/mobx-state-tree/pull/1409) by [@nulladdict](https://github.com/nulladdict)
- Fixed that calling `createObservableInstanceIfNeeded` would execute an action, even if the function immediately returned. (significant since the extraneous actions would pollute the mobx dev-tools on mere accesses, eg. by ComplexType.prototype.getValue) Fixes [#1421](https://github.com/mobxjs/mobx-state-tree/issues/1421) trough [#1422](https://github.com/mobxjs/mobx-state-tree/pull/1422) by [@Venryx](https://github.com/Venryx)
- Fix issue where `snapshotProcessor.is` does not correctly handle model instances. Fixes [#1494](https://github.com/mobxjs/mobx-state-tree/issues/1494) through [#1495](https://github.com/mobxjs/mobx-state-tree/pull/1495) by [@KevinSjoberg](https://github.com/KevinSjoberg)
- Make sure that MST no longer requires `setImmediate` to be present, but fallback to other solutions. [#1501](https://github.com/mobxjs/mobx-state-tree/pull/1501) by [@isaachinman](https://github.com/isaachinman)
# 3.15.0
- Fix for flow typings. This means that now using flows requires at least TypeScript v3.6 and that `castFlowReturn` becomes deprecated.
- Fix for empty models / models with all properties set to optional being able to take any value in TypeScript through [#1269](https://github.com/mobxjs/mobx-state-tree/pull/1269) by [@xaviergonz](https://github.com/xaviergonz).
# 3.14.1
- Made it possible to force full run-time type-checking (for better error messages) in production builds by setting `ENABLE_TYPE_CHECK=true` as environment variable. Fixes [#1332](https://github.com/mobxjs/mobx-state-tree/pull/1332) through [#1337](https://github.com/mobxjs/mobx-state-tree/pull/1337) by [@OverseePublic](https://github.com/OverseePublic)
- Fixed an issue where `Type.is` doesn't behave correctly for types that has snapshot processors. Fixes [#1321](https://github.com/mobxjs/mobx-state-tree/issues/1321) through [#1323](https://github.com/mobxjs/mobx-state-tree/pull/1323) by [@Tucker-Eric](https://github.com/Tucker-Eric)
- Changed the implementation of the internal `STNValue` type, to fix TS 3.5.3 compatibility. If somebody notices regressions in the TypeScript integration, please report. Fixes [#1343](https://github.com/mobxjs/mobx-state-tree/issues/1343), [#1307](https://github.com/mobxjs/mobx-state-tree/issues/1307)
- Added `acceptsUndefined` as option for `safeReference` so it is more suitable to be used inside arrays/maps, through [#1245](https://github.com/mobxjs/mobx-state-tree/pull/1245) by [@xaviergonz](https://github.com/xaviergonz).
# 3.14.0
- Fixed a regression with `atomic` middleware with async flows [#1250](https://github.com/mobxjs/mobx-state-tree/issues/1250).
- Added filter function to `recordActions` to filter out recording some actions. Also added `recording` and `resume` methods.
- Added `getRunningActionContext()` to get the currently executing MST action context (if any). Also added the action context helper functions `isActionContextChildOf()` and `isActionContextThisOrChildOf`.
- Reduced type nesting to avoid Typescript 3.4 errors about infinite types. Sadly due to this change `types.create` is no longer smart enough in TS to know if skipping the snapshot parameter is valid or not. Through [#1251](https://github.com/mobxjs/mobx-state-tree/pull/1251) by [@xaviergonz](https://github.com/xaviergonz).
# 3.13.0
- Fixed `Instance` not giving the proper type in Typescript when the type included both objects and primitives.
- Through PR [#1196](https://github.com/mobxjs/mobx-state-tree/pull/1196) by [@xaviergonz](https://github.com/xaviergonz)
- Added `createActionTrackerMiddleware2`, a more easy to use version of the first one, which makes creating middlewares for both sync and async actions more universal.
- Added an optional filter to `recordPatches` to be able to skip recording certain patches.
- `atomic` now uses the new `createActionTrackerMiddleware2`.
- `UndoManager` fixes and improvements:
- Uses the new `createActionTrackerMiddleware2`.
- Added `clearUndo` and `clearRedo` to only clear those.
- Added `undoLevels` and `redoLevels` to know how many undo/redo actions are available.
- Made undo manager actions atomic, so they won't actually do any partial changes if for some reason they fail.
- Fix for `withoutUndo` so it will only skip recording what is inside, not the whole action - fixes [#1195](https://github.com/mobxjs/mobx-state-tree/issues/1195).
# 3.12.2
- Added more output formats for the library (common-js minified version and umd minified version). Note that now the umd version will be the development version while the new umd.min version will be the production version. This change is to keep it in sync with the parent mobx package. Also the npm package is now leaner since it mistakenly included separatedly compiled js files and source maps.
# 3.12.1
- Fixed a regression with `getEnv` sometimes not returning the proper environment.
- Fixed an issue where `map.put` would not work with snapshots of types with an optional id [#1131](https://github.com/mobxjs/mobx-state-tree/issues/1131) through [#1226](https://github.com/mobxjs/mobx-state-tree/pull/1226) by [@xaviergonz](https://github.com/xaviergonz).
# 3.12.0
- Added `TypeOfValue` to extract the type of a complex (non primitive) variable in Typescript.
- Fixed some Typescript issues with optional arrays [#1218](https://github.com/mobxjs/mobx-state-tree/issues/1218) through [#1229](https://github.com/mobxjs/mobx-state-tree/pull/1229) by [@xaviergonz](https://github.com/xaviergonz)
- Added `getNodeId` to get the internal unique node id for an instance [#1168](https://github.com/mobxjs/mobx-state-tree/issues/1168) through [#1225](https://github.com/mobxjs/mobx-state-tree/pull/1225) by [@xaviergonz](https://github.com/xaviergonz)
- Fixed nodes being `pop`/`shift`/`splice` from an array not getting properly destroyed through [#1205](https://github.com/mobxjs/mobx-state-tree/pull/1205) by [@xaviergonz](https://github.com/xaviergonz). Not that this means that in order to access the returned dead nodes data without getting a liveliness error/warning then the returned dead nodes have to be either cloned (`clone`) or their snapshots (`getSnapshot`) have to be used first.
# 3.11.0
- Added an optional third argument to `types.optional` that allows to set alternative optional values other than just `undefined` through [#1192](https://github.com/mobxjs/mobx-state-tree/pull/1192) by [@xaviergonz](https://github.com/xaviergonz)
- Fixed detaching arrays/maps killing their children [#1173](https://github.com/mobxjs/mobx-state-tree/issues/1173) through [#1175](https://github.com/mobxjs/mobx-state-tree/pull/1175) by [@xaviergonz](https://github.com/xaviergonz)
- Added `types.snapshotProcessor` [#947](https://github.com/mobxjs/mobx-state-tree/issues/947) through [#1165](https://github.com/mobxjs/mobx-state-tree/pull/1165) by [@xaviergonz](https://github.com/xaviergonz). This feature will eventually deprecate `postProcessSnapshot` and `preProcessSnapshot` from models in a next major version.
- Performance improvement for event handlers so they consume less RAM through [#1160](https://github.com/mobxjs/mobx-state-tree/pull/1160) by [@xaviergonz](https://github.com/xaviergonz)
- Make liveliness errors give more info to trace their cause [#1142](https://github.com/mobxjs/mobx-state-tree/issues/1142) through [#1147](https://github.com/mobxjs/mobx-state-tree/pull/1147) by [@xaviergonz](https://github.com/xaviergonz)
# 3.10.2
- Fixed a regression regarding json paths not being correctly rooted to the base [#1128](https://github.com/mobxjs/mobx-state-tree/issues/1128) through [#1146](https://github.com/mobxjs/mobx-state-tree/pull/1146) by [@xaviergonz](https://github.com/xaviergonz)
# 3.10.1
- Fixed mobx 5.9.0 compatibility [#1143](https://github.com/mobxjs/mobx-state-tree/issues/1143) through [#1144](https://github.com/mobxjs/mobx-state-tree/pull/1144) by [@xaviergonz](https://github.com/xaviergonz)
- Made liveliness checking in warn mode log an error so the stack trace can be seen [#1142](https://github.com/mobxjs/mobx-state-tree/issues/1142) through [#1145](https://github.com/mobxjs/mobx-state-tree/pull/1145) by [@xaviergonz](https://github.com/xaviergonz)
- Fixed JSON path escaping, where '/' and '~' were incorrectly being encoded/decoded as '~0' and '~1' rather than '~1' and '~0'. Also fixed empty keys not being handled correctly by JSON patches [#1128](https://github.com/mobxjs/mobx-state-tree/issues/1128). Fixed through [#1129](https://github.com/mobxjs/mobx-state-tree/pull/1129) by [@xaviergonz](https://github.com/xaviergonz)
# 3.10.0
- Fix for safeReference doesn't work when multiple nodes reference a single reference that gets deleted [#1115](https://github.com/mobxjs/mobx-state-tree/issues/1115) through [#1121](https://github.com/mobxjs/mobx-state-tree/pull/1121) by [@xaviergonz](https://github.com/xaviergonz)
- Little fix for `castFlowReturn` not typecasting the promise to its actual result.
- Made `isAlive(node)` reactive, so it can be reacted upon through [#1100](https://github.com/mobxjs/mobx-state-tree/pull/1100) by [@xaviergonz](https://github.com/xaviergonz)
- Fix for unaccessed nodes not unregistering their identifiers [#1112](https://github.com/mobxjs/mobx-state-tree/issues/1112) through [#1113](https://github.com/mobxjs/mobx-state-tree/pull/1113) by [@xaviergonz](https://github.com/xaviergonz)
- Added `clear()` to `UndoManager` middleware through [#1118](https://github.com/mobxjs/mobx-state-tree/pull/1118) by [@chemitaxis](https://github.com/chemitaxis)
# 3.9.0
- TypeScript 3.0 or later is now required when using TypeScript. This brings some improvements:
- `flow` arguments and return types are now correctly inferred automatically. One exception is when the last return of a `flow` is a `Promise`. In these cases `castFlowReturn(somePromise)` needs to be used so the return type can be inferred properly.
- `create` method is now smart enough to warn when no snapshot argument is provided on types that have some mandatory properties.
- Added `setLivelinessChecking` and `getLivelinessChecking`, the old `setLivelynessChecking` will eventually be deprecated.
- Added `onInvalidated` option for references and `types.safeReference` (see readme) through [#1091](https://github.com/mobxjs/mobx-state-tree/pull/1091) by [@xaviergonz](https://github.com/xaviergonz)
- Added `tryReference` and `isValidReference` to use references that might be no longer pointing to any nodes in a safe way through [#1087](https://github.com/mobxjs/mobx-state-tree/pull/1087) by [@xaviergonz](https://github.com/xaviergonz)
- Readded `IComplexType` for backwards compatibility.
# 3.8.1
- Fixed non-initialized nodes not being destroyed [#1080](https://github.com/mobxjs/mobx-state-tree/issues/1080) through [#1082](https://github.com/mobxjs/mobx-state-tree/pull/1082) by [@k-g-a](https://github.com/k-g-a)
- Fixed a memory leak in createActionTrackingMiddleware when using flow [#1083](https://github.com/mobxjs/mobx-state-tree/issues/1083) through [#1084](https://github.com/mobxjs/mobx-state-tree/pull/1084) by [@robinfehr](https://github.com/robinfehr)
# 3.8.0
- Added castToSnapshot/castToReferenceSnapshot methods for TypeScript and fixed some TypeScript typings not being properly detected when using SnapshotIn types through [#1074](https://github.com/mobxjs/mobx-state-tree/pull/1074) by [@xaviergonz](https://github.com/xaviergonz)
- Fixed redux middleware throwing an error when a flow is called before it is connected [#1065](https://github.com/mobxjs/mobx-state-tree/issues/1065) through [#1079](https://github.com/mobxjs/mobx-state-tree/pull/1079) by [@mkramb](https://github.com/mkramb) and [@xaviergonz](https://github.com/xaviergonz)
- Made `addDisposer` return the passed disposer through [#1059](https://github.com/mobxjs/mobx-state-tree/pull/1059) by [@xaviergonz](https://github.com/xaviergonz)
# 3.7.1
- Fixed references to nodes being broken after the node was replaced [#1052](https://github.com/mobxjs/mobx-state-tree/issues/1052), plus speed up of reference resolving when using IDs through [#1053](https://github.com/mobxjs/mobx-state-tree/pull/1053) by [@xaviergonz](https://github.com/xaviergonz)
# 3.7.0
- Middleware events now also contain `allParentIds` (chain of causing ids, from root until (excluding) current)
- Improved redux dev tools integration, now supporting flows and showing action chains through [#1035](https://github.com/mobxjs/mobx-state-tree/pull/1035) based on a fix by [@bourquep](https://github.com/bourquep)
# 3.6.0
- Made type Typescript compilation when 'declarations' is set to true + type completion faster thanks to some type optimizations through [#1043](https://github.com/mobxjs/mobx-state-tree/pull/1043) by [@xaviergonz](https://github.com/xaviergonz)
- Fix for array reconciliation of union types with ids [#1045](https://github.com/mobxjs/mobx-state-tree/issues/1045) through [#1047](https://github.com/mobxjs/mobx-state-tree/pull/1047) by [@xaviergonz](https://github.com/xaviergonz)
- Fixed bug where the eager option for the union type defaulted to true when no options argument was passed but false when it was passed. Now they both default to true when not specified. Fixed through [#1046](https://github.com/mobxjs/mobx-state-tree/pull/1046) by [@xaviergonz](https://github.com/xaviergonz)
# 3.5.0
- Fix for afterCreate/afterAttach sometimes throwing an exception when a node was created as part of a view/computed property [#967](https://github.com/mobxjs/mobx-state-tree/issues/967) through [#1026](https://github.com/mobxjs/mobx-state-tree/pull/1026) by [@xaviergonz](https://github.com/xaviergonz). Note that this fix will only work if your installed peer mobx version is >= 4.5.0 or >= 5.5.0
- Fix for cast method being broken in Typescript 3.1.1 through [#1028](https://github.com/mobxjs/mobx-state-tree/pull/1028) by [@xaviergonz](https://github.com/xaviergonz)
# 3.4.0
- Added getPropertyMembers(typeOrNode) through [#1016](https://github.com/mobxjs/mobx-state-tree/pull/1016) by [@xaviergonz](https://github.com/xaviergonz)
- Fix for preProcessSnapshot not copied on compose [#613](https://github.com/mobxjs/mobx-state-tree/issues/613) through [#1013](https://github.com/mobxjs/mobx-state-tree/pull/1013) by [@theRealScoobaSteve](https://github.com/theRealScoobaSteve)
- Fix for actions sometimes failing to resolve this to self through [#1014](https://github.com/mobxjs/mobx-state-tree/pull/1014) by [@xaviergonz](https://github.com/xaviergonz)
- Fix for preProcessSnapshot not copied on compose [#613](https://github.com/mobxjs/mobx-state-tree/issues/613) through [#1013](https://github.com/mobxjs/mobx-state-tree/pull/1013) by [@theRealScoobaSteve](https://github.com/theRealScoobaSteve)
- Improvements to the bookshop example through [#1009](https://github.com/mobxjs/mobx-state-tree/pull/1009) by [@programmer4web](https://github.com/programmer4web)
- Fix for a regression with optional identifiers [#1019](https://github.com/mobxjs/mobx-state-tree/issues/1019) through [#1020](https://github.com/mobxjs/mobx-state-tree/pull/1020) by [@xaviergonz](https://github.com/xaviergonz)
# 3.3.0
- Fix for references sometimes not intializing its parents [#993](https://github.com/mobxjs/mobx-state-tree/issues/993) through [#997](https://github.com/mobxjs/mobx-state-tree/pull/997) by [@xaviergonz](https://github.com/xaviergonz)
- Fix for TS3 issues with reference type [#994](https://github.com/mobxjs/mobx-state-tree/issues/994) through [#995](https://github.com/mobxjs/mobx-state-tree/pull/995) by [@xaviergonz](https://github.com/xaviergonz)
- types.optional will now throw if an instance is directly passed as default value [#1002](https://github.com/mobxjs/mobx-state-tree/issues/1002) through [#1003](https://github.com/mobxjs/mobx-state-tree/pull/1003) by [@xaviergonz](https://github.com/xaviergonz)
- Doc fixes and improvements by [@AjaxSolutions](https://github.com/AjaxSolutions) and [@agilgur5](https://github.com/agilgur5)
# 3.2.4
- Further improvements for Typescript support for enumeration by [@xaviergonz](https://github.com/xaviergonz)
- Smaller generated .d.ts files through [#990](https://github.com/mobxjs/mobx-state-tree/pull/990) by [@xaviergonz](https://github.com/xaviergonz)
- Fix for exception when destroying children of types.maybe through [#985](https://github.com/mobxjs/mobx-state-tree/pull/985) by [@dsabanin](https://github.com/dsabanin)
# 3.2.3
- Fixed incorrect typing generation for mst-middlewares [#979](https://github.com/mobxjs/mobx-state-tree/issues/979)
# 3.2.2
- Fixes for the reconciliation algorithm of arrays [#928](https://github.com/mobxjs/mobx-state-tree/issues/928) through [#960](https://github.com/mobxjs/mobx-state-tree/pull/960) by [@liuqiang1357](https://github.com/liuqiang1357)
- Better Typescript support for enumeration, compose, union, literal and references by [@xaviergonz](https://github.com/xaviergonz)
- Updated dependencies to latest versions by [@xaviergonz](https://github.com/xaviergonz)
- [Internal] Cleanup 'createNode' and related codepaths through [#962](https://github.com/mobxjs/mobx-state-tree/pull/962) by [@k-g-a](https://github.com/k-g-a)
# 3.2.1
- Fix for wrong generated TS import [#968](https://github.com/mobxjs/mobx-state-tree/issues/968) through [#969](https://github.com/mobxjs/mobx-state-tree/pull/969) by [@k-g-a](https://github.com/k-g-a)
# 3.2.0
- Made the internal CreationType/SnapshotType/Type official via the new [`SnapshotIn`, `SnapshotOut`, `Instance` and `SnapshotOrInstance`](README.md#typeScript-and-mst) by [@xaviergonz](https://github.com/xaviergonz)
- A new [`cast` method](README.md#snapshots-can-be-used-to-write-values) that makes automatic casts from instances/input snapshots for assignments by [@xaviergonz](https://github.com/xaviergonz)
# 3.1.1
- Fixed typings of `getParent` and `getRoot`. Fixes [#951](https://github.com/mobxjs/mobx-state-tree/issues/951) through [#953](https://github.com/mobxjs/mobx-state-tree/pull/953) by [@xaviergonz](https://github.com/xaviergonz)
# 3.1.0
- Fixed issue where snapshot post-processors where not always applied. Fixes [#926](https://github.com/mobxjs/mobx-state-tree/issues/926), [#961](https://github.com/mobxjs/mobx-state-tree/issues/961), through [#959](https://github.com/mobxjs/mobx-state-tree/pull/959) by [@k-g-a](https://github.com/k-g-a)
# 3.0.3
- Fixed re-adding the same objects to an array. Fixes [#928](https://github.com/mobxjs/mobx-state-tree/issues/928) through [#949](https://github.com/mobxjs/mobx-state-tree/pull/949) by [@Krivega](https://github.com/Krivega)
# 3.0.2
- Introduced `types.integer`! By [@jayarjo](https://github.com/jayarjo) through [#935](https://github.com/mobxjs/mobx-state-tree/pull/935)
- Improved typescript typings, several fixes to the type system. Awesome contribution by [@xaviergonz](https://github.com/xaviergonz) through [#937](https://github.com/mobxjs/mobx-state-tree/pull/937) and [#945](https://github.com/mobxjs/mobx-state-tree/pull/945). Fixes [#922](https://github.com/mobxjs/mobx-state-tree/issues/922), [#930](https://github.com/mobxjs/mobx-state-tree/issues/930), [#932](https://github.com/mobxjs/mobx-state-tree/issues/932), [#923](https://github.com/mobxjs/mobx-state-tree/issues/923)
- Improved handling of `types.late`
# 3.0.1 (retracted)
# 3.0.0
Welcome to MobX-state-tree! This version introduces some breaking changes, but nonetheless is an recommended upgrade as all changes should be pretty straight forward and there is no reason anymore to maintain the 2.x range (3.0 is still compatible with MobX 4)
## Most important changes
MST 3 is twice as fast in initializing trees with half the memory consumption compared to version 2:
Running `yarn speedtest` on Node 9.3:
| | MST 2 | MST 3 |
| --------------- | ------ | ------ |
| Time | 24sec | 12 sec |
| Mem | 315MB | 168MB |
| Size (min+gzip) | 14.1KB | 15.0KB |
Beyond that, MST 3 uses TypeScript 2.8, which results in more accurate TypeScript support.
The type system has been simplified and improved in several areas. Several open issues around maps and (numeric) keys have been resolved. The `frozen` type can now be fully typed. See below for the full details.
Also, the 'object has died' exception can be suppressed now. One should still address it, but at least it won't be a show-stopper from now on.
## Changes in the type system
- **[BREAKING]** `types.identifier` can no longer be parameterized with either `types.string` or `types.number`. So instead of `types.identifier()` use `types.identifier`. Identifiers are now always normalized to strings. This reflects what was already happening internally and solves a lot of edge cases. To use numbers as identifiers, `types.identifierNumber` (instead of `types.identifier(types.number)`) can be used, which serializes it's snapshot to a number, but will internally work like a string based identifier
- **[BREAKING]** `types.maybe` now serializes to / from `undefined` by default, as it is more and more the common best practice to don't use `null` at all and MST follows this practice. Use `types.maybeNull` for the old behavior (see [#830](https://github.com/mobxjs/mobx-state-tree/issues/830))
- **[BREAKING]** `types.frozen` is now a function, and can now be invoked in a few different ways:
1. `types.frozen()` - behaves the same as `types.frozen` in MST 2.
2. `types.frozen(SubType)` - provide a valid MST type and frozen will check if the provided data conforms the snapshot for that type. Note that the type will not actually be instantiated, so it can only be used to check the _shape_ of the data. Adding views or actions to `SubType` would be pointless.
3. `types.frozen(someDefaultValue)` - provide a primitive value, object or array, and MST will infer the type from that object, and also make it the default value for the field
4. `types.frozen()` - provide a typescript type, to help in strongly typing the field (design time only)
- It is no longer necessary to wrap `types.map` or `types.array` in `types.optional` when used in a `model` type, `map` and `array` are now optional by default when used as property type. See [#906](https://github.com/mobxjs/mobx-state-tree/issues/906)
- **[BREAKING]** `postProcessSnapshot` can no longer be declared as action, but, like `preProcessSnapshot`, needs to be defined on the type rather than on the instance.
- **[BREAKING]** `types.union` is now eager, which means that if multiple valid types for a value are encountered, the first valid type is picked, rather then throwing. #907 / #804, `dispatcher` param => option,
## Other improvements
- **[BREAKING]** MobX-state-tree now requires at least TypeScript 2.8 when using MST with typescript. The type system has been revamped, and should now be a lot more accurate, especially concerning snapshot types.
- **[BREAKING]** `map.put` will now return the inserted node, rather than the map itself. This makes it easier to find objects for which the identifier is not known upfront. See [#766](https://github.com/mobxjs/mobx-state-tree/issues/766) by [k-g-a](https://github.com/k-g-a)
- **[BREAKING]** The order of firing hooks when instantiating has slighlty changed, as the `afterCreate` hook will now only be fired upon instantiation of the tree node, which now happens lazily (on first read / action). The internal order in which hooks are fired within a single node has remained the same. See [#845](https://github.com/mobxjs/mobx-state-tree/issues/845) for details
- Significantly improved the performance of constructing MST trees. Significantly reduced the memory footprint of MST. Big shoutout to the relentless effort by [k-g-a](https://github.com/k-g-a) to optimize all the things! See [#845](https://github.com/mobxjs/mobx-state-tree/issues/845) for details.
- Introduced `setLivelynessChecking("warn" | "ignore" | "error")`, this can be used to customize how MST should act when one tries to read or write to a node that has already been removed from the tree. The default behavior is `warn`.
- Improved the overloads of `model.compose`, see [#892](https://github.com/mobxjs/mobx-state-tree/pull/892) by [t49tran](https://github.com/t49tran)
- Fixed issue where computed properties based on `getPath` could return stale results, fixes [#917](https://github.com/mobxjs/mobx-state-tree/issues/917)
- Fixed issue where onAction middleware threw on dead nodes when attachAfter option was used
- Fixed several issues with maps and numeric identifiers, such as [#884](https://github.com/mobxjs/mobx-state-tree/issues/884) and [#826](https://github.com/mobxjs/mobx-state-tree/issues/826)
## TL,DR Migration guide
- `types.identifier(types.number)` => `types.identifierNumber`
- `types.identifier()` and `types.identifier(types.string)` =>`types.identifier`
- `types.frozen` => `types.frozen()`
- `types.maybe(x)` => `types.maybeNull(x)`
- `postProcessSnapshot` should now be declared on the type instead of as action
# 2.2.0
- Added support for MobX 5. Initiative by [@jeffberry](https://github.com/jeffberry) through [#868](https://github.com/mobxjs/mobx-state-tree/pull/868/files). Please note that there are JavaScript engine restrictions for MobX 5 (no Internet Explorer, or React Native Android). If you need to target those versions please keep using MobX 4 as peer dependency (MST is compatible with both)
- Reduced memory footprint with ~10-20%, by [k-g-a](https://github.com/k-g-a) through [#872](https://github.com/mobxjs/mobx-state-tree/pull/872)
- Fixed issue where undo manager was not working correctly for non-root stores, by [marcofugaro](https://github.com/marcofugaro) trough [#875](https://github.com/mobxjs/mobx-state-tree/pull/875)
# 2.1.0
- Fixed issue where default values of `types.frozen` where not applied correctly after apply snapshot. [#842](https://github.com/mobxjs/mobx-state-tree/pull/842) by [SirbyAlive](https://github.com/SirbyAlive). Fixes [#643](https://github.com/mobxjs/mobx-state-tree/issues/634)
- Fixed issue where empty patch sets resulted in in unnecessary history items. [#838](https://github.com/mobxjs/mobx-state-tree/pull/838) by [chemitaxis](https://github.com/chemitaxis). Fixes [#837](https://github.com/mobxjs/mobx-state-tree/issues/837)
- `flow`s of destroyed nodes can no 'safely' resume. [#798](https://github.com/mobxjs/mobx-state-tree/pull/798/files) by [Bnaya](https://github.com/Bnaya). Fixes [#792](https://github.com/mobxjs/mobx-state-tree/issues/792)
- Made sure the type `Snapshot` is exposed. [#821](https://github.com/mobxjs/mobx-state-tree/pull/821) by [dsabanin](https://github.com/dsabanin)
- Fix: the function parameter was incorrectly typed as non-optional. [#851](https://github.com/mobxjs/mobx-state-tree/pull/851) by [abruzzihraig](https://github.com/abruzzihraig)
# 2.0.5
- It is now possible to get the snapshot of a node without triggering the `postProcessSnapshot` hook. See [#745](https://github.com/mobxjs/mobx-state-tree/pull/745) for details. By @robinfehr
- Introduced `getParentOfType` and `hasParentOfType`. See [#767](https://github.com/mobxjs/mobx-state-tree/pull/767) by @k-g-a
- Fixed issue where running `typeCheck` accidentally logged typecheck errors to the console. Fixes [#781](https://github.com/mobxjs/mobx-state-tree/issues/781)
# 2.0.4
- Removed accidental dependency on mobx
# 2.0.3
- Fixed issue where middleware that changed arguments wasn't properly picked up. See [#732](https://github.com/mobxjs/mobx-state-tree/pull/732) by @robinfehr. Fixes [#731](https://github.com/mobxjs/mobx-state-tree/issues/731)
- Fixed reassigning to a custom type from a different type in a union silently failing. See [#737](https://github.com/mobxjs/mobx-state-tree/pull/737) by @univerio. Fixes [#736](https://github.com/mobxjs/mobx-state-tree/issues/736)
- Fixed typings issue with TypeScript 2.8. See [#740](https://github.com/mobxjs/mobx-state-tree/pull/740) by @bnaya.
- Fixed undo manager apply grouped patches in the wrong order. See [#755](https://github.com/mobxjs/mobx-state-tree/pull/755) by @robinfehr. Fixes [#754](https://github.com/mobxjs/mobx-state-tree/issues/754)
# 2.0.2
- Fixed bidirectional references from nodes to nodes, see [#728](https://github.com/mobxjs/mobx-state-tree/pull/728) by @robinfehr
- `joinJsonPath` and `splitJsonPath` are now exposed as utilities, see [#724](https://github.com/mobxjs/mobx-state-tree/pull/724) by @jjrv
- Several documentation and example fixes
# 2.0.1
- Fixed typings for maps of maps [#704](https://github.com/mobxjs/mobx-state-tree/pull/704) by @xaviergonz
- Fixed dependency issue in `mst-middlewares` package
# 2.0.0
**Breaking changes**
- MobX-state-tree now requires MobX 4.0 or higher
- Identifiers are now internally always normalized to strings. This also means that adding an object with an number identifier to an observable map, it should still be requested back as string. In general, we recommend to always use string based identifiers to avoid confusion.
- Due to the changes in Mobx 4.0, `types.map(subType).keys()` will return `Iterator` instead of `ObservableArrays`. In order to address this issue, wrap the keys with `Array.from()`.
# 1.4.0
**Features**
- It is now possible to create [custom primitive(like) types](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/API/README.md#custom)! Implements [#673](https://github.com/mobxjs/mobx-state-tree/issues/673) through [#689](https://github.com/mobxjs/mobx-state-tree/pull/689)
- [`getIdentifier`](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/API/README.md#getidentifier) is now exposed as function, to get the identifier of a model instance (if any). Fixes [#674](https://github.com/mobxjs/mobx-state-tree/issues/674) through [#678](https://github.com/mobxjs/mobx-state-tree/pull/678) by TimHollies
- Writing [middleware](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/middleware.md) has slightly changed, to make it less error prone and more explicit whether a middleware chain should be aborted. For details, see [#675](https://github.com/mobxjs/mobx-state-tree/pull/675) by Robin Fehr
- It is now possible to configure whether [attached middleware](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/API/README.md#addmiddleware) should be triggered for the built-in hooks / operations. [#653](https://github.com/mobxjs/mobx-state-tree/pull/653) by Robin Fehr
- We exposed an [api](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/API/README.md#getmembers) to perform reflection on model instances. [#649](https://github.com/mobxjs/mobx-state-tree/pull/649) by Robin Fehr
**Fixes**
- Fixed a bug where items in maps where not properly reconciled when the `put` operation was used. Fixed [#683](https://github.com/mobxjs/mobx-state-tree/issues/683) and [#672](https://github.com/mobxjs/mobx-state-tree/issues/672) through [#693](https://github.com/mobxjs/mobx-state-tree/pull/693)
- Fixed issue where trying to resolve a path would throw exceptions. Fixed [#686](https://github.com/mobxjs/mobx-state-tree/issues/686) through [#692](https://github.com/mobxjs/mobx-state-tree/pull/692)
- In non production builds actions and views on models can now be replaced, to simplify mocking. Fixes [#646](https://github.com/mobxjs/mobx-state-tree/issues/646) through [#690](https://github.com/mobxjs/mobx-state-tree/pull/690)
- Fixed bug where `tryResolve` could leave a node in a corrupt state. [#668](https://github.com/mobxjs/mobx-state-tree/pull/668) by dnakov
- Fixed typings for TypeScript 2.7, through [#667](https://github.com/mobxjs/mobx-state-tree/pull/667) by Javier Gonzalez
- Several improvements to error messages
# 1.3.1
- Fixed bug where `flows` didn't properly batch their next ticks properly in actions, significantly slowing processes down. Fixes [#563](<[#563](https://github.com/mobxjs/mobx-state-tree/issues/563)>)
# 1.3.0
- Significantly improved the undo/redo manager. The undo manager now supports groups. See [#504](https://github.com/mobxjs/mobx-state-tree/pull/504) by @robinfehr! See the [updated docs](https://github.com/mobxjs/mobx-state-tree/blob/master/packages/mst-middlewares/README.md#undomanager) for more details.
- Significantly improved performance, improvements of 20% could be expected, but changes of course per case. See [#553](https://github.com/mobxjs/mobx-state-tree/pull/553)
- Implemented `actionLogger` middleware, which logs most events for async actions
- Slightly changed the order in which life cycle hooks are fired. `afterAttach` will no fire first on the parent, then on the children. So, unlike `afterCreate`, in `afterAttach` one can assume in `afterAttach that the parent has completely initialized.
# 1.2.1
- 1.2.0 didn't seem to be released correctly...
# 1.2.0
- Introduced customizable reference types. See the [reference and identifiers](https://github.com/mobxjs/mobx-state-tree#references-and-identifiers) section.
- Introduced `model.volatile` to more easily declare and reuse volatile instance state. Volatile state can contain arbitrary data, is shallowly observable and, like props, cannot be modified without actions. See [`model.volatile`](https://github.com/mobxjs/mobx-state-tree#model-volatile) for more details.
# 1.1.1
### Improvements
- Fixed an issue where nodes where not always created correctly, see #534. Should fix #513 and #531.
- All tests are now run in both PROD and non PROD configurations, after running into some bugs that only occurred in production builds.
- Some internal optimizations have been applied (and many more will follow). Like having internal leaner node for immutable data. See #474
- A lot of minor improvements on the docs
# 1.1.0
### Improvements
- The concept of process (asynchronous actions) has been renamed to flows. (Mainly to avoid issues with bundlers)
- We changed to a lerna setup which allows separately distributing middleware and testing examples with more ease
- Every MST middleware is now shipped in a separate package named `mst-middlewares`. They are now written in TypeScript and fully transpiled to ES5 to avoid problems with uglifyjs in create-react-app bundling.
- Introduced `createActionTrackingMiddleware`, this significantly simplifies writing middleware for common scenarios. Especially middleware that deals with asynchronous actions (flows)
- Renamed `process` to `flow`. Deprecated `process`.
- **BREAKING** As a result some middleware event names have also been changed. If you have custom middlewares this change might affect you. Rename middleware event type prefixes starting with `process` to now start with `flow`.
### Fixes
- Fixed nested maps + environments not working correctly, [#447](https://github.com/mobxjs/mobx-state-tree/pull/447) by @xaviergonz
- Improved typescript typings for enumerations, up to 50 values are now supported [#424](https://github.com/mobxjs/mobx-state-tree/pull/447) by @danielduwaer
# 1.0.2
- Introduced `modelType.extend` which allows creating views and actions with shared state.
# 1.0.1
### Features
- Added the middlewares `atomic` and types `TimeTraveller`, `UndoManager`. Check out the [docs](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/middleware.md)!
- Introduced `createActionTrackingMiddleware` to simplify the creation of middleware that support complex async processes
- exposed `typecheck(type, value)` as public api (will ignore environment flags)
### Improvements
- `getEnv` will return an empty object instead of throwing when a tree was initialized without environment
- Fixed issue where patches generated for nested maps were incorrect (#396)
- Fixed the escaping of (back)slashes in JSON paths (#405)
- Improved the algorithm that reconcile items in an array (#384)
- Assigning a node that has an environment to a parent is now allowed, as long as the environment is strictly the same (#387)
- Many minor documentation improvements. Thanks everybody who created a PR!
# 1.0.0
No changes
# 0.12.0
- **BREAKING** The redux utilities are no longer part of the core package, but need to be imported from `mobx-state-tree/middleware/redux`.
# 0.11.0
### Breaking changes
- **BREAKING** `onAction` middleware no longer throws when encountering unserializable arguments. Rather, it serializes a struct like `{ $MST_UNSERIALIZABLE: true, type: "someType" }`. MST Nodes are no longer automatically serialized. Rather, one should either pass 1: an id, 2: a (relative) path, 3: a snapshot
- **BREAKING** `revertPatch` has been dropped. `IReversableJsonPatch` is no longer exposed, instead use the inverse patches generated by `onPatch`
- **BREAKING** some middleware events have been renamed: `process_yield` -> `process_resume`, `process_yield_error` -> `process_resume_error`, to make it less confusing how these events relate to `yield` statements.
- **BREAKING** patchRecorder's field `patches` has been renamed to `rawPatches,`cleanPatches`to`patches`, and`inversePatches` was added.
### New features
- Introduced `decorate(middleware, action)` to easily attach middleware to a specific action
- Handlers passed to `onPatch(handler: (patch, inversePatch) => void)` now receive as second argument the inverse patch of the emitted patch
- `onAction` lister now supports an `attachAfter` parameter
- Middleware events now also contain `parentId` (id of the causing action, `0` if none) and `tree` (the root of context)
### Fixes
- ReduxDevTools connection is no longer one step behind [#287](https://github.com/mobxjs/mobx-state-tree/issues/287)
- Middleware is no longer run as part of the transaction of the targeted action
- Fixed representation of `union` types in error messages
# 0.10.3
- **BREAKISH** Redefining lifecycle hooks will now automatically compose them, implements [#252](https://github.com/mobxjs/mobx-state-tree/issues/252)
- Added dev-only checks, typecheck will be performed only in dev-mode and top-level API-calls will be checked.
- The internal types `IMiddleWareEvent`, `IMiddlewareEventType`, `ISerializedActionCall` are now exposed (fixes [#315](https://github.com/mobxjs/mobx-state-tree/issues/315))
# 0.10.2
- Object model instances no longer share a prototype.
# 0.10.1
- Removed accidental dependency on the codemod
# 0.10.0
- **BREAKING** the syntax to define model types has been updated. See the [updated docs](https://github.com/mobxjs/mobx-state-tree#creating-models) or the original proposal:[#282](https://github.com/mobxjs/mobx-state-tree/pull/286), but no worries, theres a codemod! :D
- **BREAKING** `preProcessSnapshot` hook is no longer a normal hook that can be defined as action. Instead, it should be defined on the type using `types.model(...).preProcessSnapshot(value => value)`
- **BREAKING** Asynchronous process should now be defined using `process`. See this [example](https://github.com/mobxjs/mobx-state-tree/blob/adba1943af263898678fe148a80d3d2b9f8dbe63/examples/bookshop/src/stores/BookStore.js#L25) or the [asynchronous action docs](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/async-actions.md).
**How to run the codemod?**
The codemod is provided as npm package command line tool. It has been written using the TypeScript parser, so it will successfully support either TS or regular JavaScript source files.
To run the codemod, you need to first install it globally by `npm install -g mst-codemod-to-0.10`.
After that, the `mst-codemod-to-0.10` command will be available in your command line.
To perform the codemod, you need to call in your command line `mst-codemod-to-0.10` followed by the filename you want to codemod. A `.bak` file with the original source will be created for backup purposes, and the file you provided will be updated to the new syntax! Have fun!
PS: You could also use `npx` instead of installing the codemod globally! :)
# 0.9.5
- Asynchronous actions are now a first class concept in mobx-state-tree. See the [docs](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/async-actions.md)
# 0.9.4
- Introduced `types.null` and `types.undefined`
- Introduced `types.enumeration(name?, options)`
# 0.9.3
- Fix `note that a snapshot is compatible` when assigning a type to an optional version of itself
- Fix error when deleting a non existing item from a map [#255](https://github.com/mobxjs/mobx-state-tree/issues/255)
- Now all required TypeScript interfaces are exported in the main mobx-state-tree package [#256](https://github.com/mobxjs/mobx-state-tree/issues/256)
# 0.9.2
Introduced the concept of reverse patches, see [#231](https://github.com/mobxjs/mobx-state-tree/pull/231/)
- Introduced the `revertPatch` operation, that takes a patch or list of patches, and reverse applies it to the target.
- `onPatch` now takes a second argument, `includeOldValue`, defaulting to `false`, which, if set to true, includes in the patch any value that is being overwritten as result of the patch. Setting this option to true produces patches that can be used with `revertPatch`
- `patchRecorder` now introduces additional fields / methods to be able to reverse apply changes: `patchRecorder.cleanPatches`, `patchRecorder.undo`
# 0.9.1
- Applying a snapshot or patches will now emit an action as well. The name of the emitted action will be `@APPLY_PATCHES`resp `@APPLY_SNAPSHOT`. See [#107](https://github.com/mobxjs/mobx-state-tree/issues/107)
- Fixed issue where same Date instance could'nt be used two times in the same state tree [#229](https://github.com/mobxjs/mobx-state-tree/issues/229)
- Fixed issue with reapplying snapshots to Date field resulting in snapshot typecheck error[#233](https://github.com/mobxjs/mobx-state-tree/issues/233)
- Declaring `types.maybe(types.frozen)` will now result into an error [#224](https://github.com/mobxjs/mobx-state-tree/issues/224)
- Added support for Mobx observable arrays in type checks [#221](https://github.com/mobxjs/mobx-state-tree/issues/221) (from [alessioscalici](https://github.com/alessioscalici))
# 0.9.0
- **BREAKING** Removed `applyPatches` and `applyActions`. Use `applyPatch` resp. `applyAction`, as both will now also accept an array as argument
- **BREAKING** `unprotect` and `protect` can only be applied at root nodes to avoid confusing scenarios Fixed [#180](https://github.com/mobxjs/mobx-state-tree/issues/180)
- Fixed [#141](https://github.com/mobxjs/mobx-state-tree/issues/141), actions / views are no longer wrapped in dynamically generated functions for a better debugging experience
- Small improvements to typings, fixed compilation issues with TypeScript 2.4.1.
- Fixed issues where `compose` couldn't overwrite getters. [#209](https://github.com/mobxjs/mobx-state-tree/issues/209), by @homura
- Fixed CDN links in readme
- Added TodoMVC to the examples section
# 0.8.2
- Fixed issue in rollup module bundle
# 0.8.1
- Fixed issue in release script, rendering 0.8.0 useless
# 0.8.0
- **BREAKING** Dropped `types.extend` in favor of `types.compose`. See [#192](https://github.com/mobxjs/mobx-state-tree/issues/192)
- Introduced the lifecycle hooks `preProcessSnapshot` and `postProcessSnapshot`. See [#203](https://github.com/mobxjs/mobx-state-tree/pull/203) / [#100](https://github.com/mobxjs/mobx-state-tree/issues/100)
- Use rollup as bundler [#196](https://github.com/mobxjs/mobx-state-tree/pull/196)
# 0.7.3
- Introduced the concept of volatile / local state in models. See [#168](https://github.com/mobxjs/mobx-state-tree/issues/168), or [docs](https://github.com/mobxjs/mobx-state-tree/tree/master#volatile-state)
- Fixed issue with types.map() with types.identifier(types.number) [#191](https://github.com/mobxjs/mobx-state-tree/issues/191) reported by @boatkorachal
- Fixed issue with reconciler that affected types.map when node already existed at that key reported by @boatkorachal [#191](https://github.com/mobxjs/mobx-state-tree/issues/191)
# 0.7.2
- Fixed `cannot read property resolve of undefined` thanks to @cpunion for reporting, now value of dead nodes will be undefined. [#186](https://github.com/mobxjs/mobx-state-tree/issues/186)
- Fixed `[LateType] is not defined` thanks to @amir-arad for reporting, when using late as model property type [#187](https://github.com/mobxjs/mobx-state-tree/issues/187)
- Fixed `Object.freeze can only be called on Object` thanks to @ds300 for reporting, when using MST on a ReactNative environment [#189](https://github.com/mobxjs/mobx-state-tree/issues/189)
- Now the entire codebase is prettier! :D [#187](https://github.com/mobxjs/mobx-state-tree/issues/187)
# 0.7.1
- Fixed `array.remove` not working
# 0.7.0
The type system and internal administration has been refactoring, making the internals both simpler and more flexible.
Things like references and identifiers are now first class types, making them much better composable. [#152](https://github.com/mobxjs/mobx-state-tree/issues/152)
- **BREAKING** References with a predefined lookup path are no longer supported. Instead of that, identifiers are now looked up in the entire tree. For that reasons identifiers now have to be unique in the entire tree, per type.
- **BREAKING** `resolve` is renamed to `resolvePath`
- Introduced `resolveIdentifier(type, tree, identifier)` to find objects by identifier
- **BREAKING** `types.reference` is by default non-nullable. For nullable identifiers, use `types.maybe(types.reference(X))`
- Many, many improvements. Related open issues will be updated.
- **BREAKING** `isMST` is renamed to `isStateTreeNode`
# 0.6.3
- Fixed issue with array/maps of union types @abruzzihraig [#151](https://github.com/mobxjs/mobx-state-tree/issues/151)
- Make types.extend support computed attributes @cpunion [#169](https://github.com/mobxjs/mobx-state-tree/issues/169)
- Fixed issue with map of primitive types and applySnapshot @pioh [#155](https://github.com/mobxjs/mobx-state-tree/issues/155)
- Better type declarations for union, up to 10 supported types
# 0.6.2
- Fixed issue where arrays where not properly serialized as action argument
# 0.6.1
- Improved reporting of Type.is(), now it returns a fine grained report of why the provided value is not applicable.
```
[mobx-state-tree] Error while converting [{}] to AnonymousModel[]:
at path "/name" snapshot undefined is not assignable to type: string.
at path "/quantity" snapshot undefined is not assignable to type: number.
```
- Fixed support for `types.reference` in combination with `types.late`, by @robinfehr
# 0.6.0
- **BREAKING** `types.withDefault` has been renamed to `types.optional`
- **BREAKING** Array and map types can no longer be left out of snapshots by default. Use `optional` to make them optional in the snapshot
- **BREAKING** Literals no longer have a default value by default (use optional + literal instead)
- **BREAKING** Disabled inlining type.model definitions as introduced in 0.5.1; to many subtle issues
- Improved identifier support, they are no properly propagated through utility types like `maybe`, `union` etc
- Fixed issue where fields where not referred back to default when a partial snapshot was provided
- Fixed #122: `types.identifier` now also accepts a subtype to override the default string type; e.g. `types.identifier(types.number)`
# 0.5.1
- Introduced support for lazy evaluating values in `withDefault`, useful to generate UUID's, timestamps or non-primitive default values
- ~~It is now possible to define something like~~ Removed in 0.6.0
```javascript
const Box = types.model({
point: {
x: 10,
y: 10
}
}
```
Where the type of `point` property is inferred to `point: types.withDefault(types.model({ x: 10, y: 10}), () => ({ x: 10, y: 10 }))`
# 0.5.0
- ** BREAKING ** protection is now enabled by default (#101)
- ** BREAKING ** it is no longer possible to read values from a dead object. Except through `getSnapshot` or `clone` (#102)
- ** BREAKING ** `types.recursive` has been removed in favor of `types.late`
- Introduced `unprotect`, to disable protection mode for a certain instance. Useful in `afterCreate` hooks
- Introduced `types.late`. Usage: `types.late(() => typeDefinition)`. Can be used for circular / recursive type definitions, even across files. See `test/circular(1|2).ts` for an example (#74)
# 0.4.0
**BREAKING** `types.model` no requires 2 parameters to define a model. The first parameter defines the properties, derived values and view functions. The second argument is used to define the actions. For example:
```javascript
const Todo = types.model("Todo", {
done: types.boolean,
toggle() {
this.done = !this.done
}
})
```
Now should be defined as:
```javascript
const Todo = types.model(
"Todo",
{
done: types.boolean
},
{
toggle() {
this.done = !this.done
}
}
)
```
It is still possible to define functions on the first object. However, those functions are not considered to be actions, but views. They are not allowed to modify values, but instead should produce a new value themselves.
# 0.3.3
- Introduced lifecycle hooks `afterCreate`, `afterAttach`, `beforeDetach`, `beforeDestroy`, implements #76
- Introduced the convenience method `addDisposer(this, cb)` that can be used to easily destruct reactions etc. which are set up in `afterCreate`. See #76
# 0.3.2
- Fix: actions where not bound automatically
- Improved and simplified the reconciliation mechanism, fixed many edge cases
- Improved the reference mechanism, fixed many edge cases
- Improved performance
# 0.3.1
- (re) introduced the concept of environments, which can be passed as second argument to `.create`, and picked up using `getEnv`
# 0.3.0
- Removed `primitive` type, use a more specific type instead
- Improved typescript typings of snapshots
- Added `depth` parameter to `getParent` and `hasParent`
- Separated the concepts of middleware and serializable actions. It is now possible to intercept, modify actions etc through `addMiddleWare`. `onAction` now uses middleware, if it is used, all parameters of actions should be serializable!
# 0.2.2
- Introduced the concept of liveliness; if nodes are removed from the the tree because they are replaced by some other value, they will be marked as "died". This should help to early signal when people hold on to references that are not part of the tree anymore. To explicitly remove an node from a tree, with the intent to spawn a new state tree from it, use `detach`.
- Introduced the convenience method `destroy` to remove a model from it's parent and mark it as dead.
- Introduced the concept of protected trees. If a tree is protected using `protect`, it can only be modified through action, and not by mutating it directly anymore.
# 0.2.1
- Introduced .Type and .SnapshotType to be used with TypeScript to get the type for a model
# 0.2.0
- Renamed `createFactory` to `types.model` (breaking!)
- Renamed `composeFactory` to `types.extend` (breaking!)
- Actions should now be declared as `name(params) { body }`, instead of `name: action(function (params) { body})` (breaking!)
- Models are no longer constructed by invoking the factory as function, but by calling `factory.create` (breaking!)
- Introduced `identifier`
- Introduced / improved `reference`
- Greatly improved typescript support, type inference etc. However there are still limitations as the full typesystem of MST cannot be expressed in TypeScript. Especially concerning the type of snapshots and the possibility to use snapshots as first class value.
================================================
FILE: docker-compose.yml
================================================
version: "3"
services:
docusaurus:
build: .
ports:
- 3000:3000
- 35729:35729
volumes:
- ./docs:/app/docs
- ./website/blog:/app/website/blog
- ./website/core:/app/website/core
- ./website/i18n:/app/website/i18n
- ./website/pages:/app/website/pages
- ./website/static:/app/website/static
- ./website/sidebars.json:/app/website/sidebars.json
- ./website/siteConfig.js:/app/website/siteConfig.js
working_dir: /app/website
================================================
FILE: docs/.gitattributes
================================================
/API/**/* -diff -merge
/API/**/* linguist-generated
================================================
FILE: docs/API/index.md
================================================
---
id: "index"
title: "mobx-state-tree - v7.0.2"
sidebar_label: "Globals"
---
[mobx-state-tree - v7.0.2](index.md)
## Index
### Interfaces
* [CustomTypeOptions](interfaces/customtypeoptions.md)
* [FunctionWithFlag](interfaces/functionwithflag.md)
* [IActionContext](interfaces/iactioncontext.md)
* [IActionRecorder](interfaces/iactionrecorder.md)
* [IActionTrackingMiddleware2Call](interfaces/iactiontrackingmiddleware2call.md)
* [IActionTrackingMiddleware2Hooks](interfaces/iactiontrackingmiddleware2hooks.md)
* [IActionTrackingMiddlewareHooks](interfaces/iactiontrackingmiddlewarehooks.md)
* [IAnyComplexType](interfaces/ianycomplextype.md)
* [IAnyModelType](interfaces/ianymodeltype.md)
* [IAnyType](interfaces/ianytype.md)
* [IHooks](interfaces/ihooks.md)
* [IJsonPatch](interfaces/ijsonpatch.md)
* [IMiddlewareEvent](interfaces/imiddlewareevent.md)
* [IModelReflectionData](interfaces/imodelreflectiondata.md)
* [IModelReflectionPropertiesData](interfaces/imodelreflectionpropertiesdata.md)
* [IModelType](interfaces/imodeltype.md)
* [IPatchRecorder](interfaces/ipatchrecorder.md)
* [IReversibleJsonPatch](interfaces/ireversiblejsonpatch.md)
* [ISerializedActionCall](interfaces/iserializedactioncall.md)
* [ISimpleType](interfaces/isimpletype.md)
* [ISnapshotProcessor](interfaces/isnapshotprocessor.md)
* [ISnapshotProcessors](interfaces/isnapshotprocessors.md)
* [IType](interfaces/itype.md)
* [IValidationContextEntry](interfaces/ivalidationcontextentry.md)
* [IValidationError](interfaces/ivalidationerror.md)
* [ReferenceOptionsGetSet](interfaces/referenceoptionsgetset.md)
* [ReferenceOptionsOnInvalidated](interfaces/referenceoptionsoninvalidated.md)
* [UnionOptions](interfaces/unionoptions.md)
### Type aliases
* [IDisposer](index.md#idisposer)
* [IHooksGetter](index.md#ihooksgetter)
* [IMiddlewareEventType](index.md#imiddlewareeventtype)
* [IMiddlewareHandler](index.md#imiddlewarehandler)
* [ITypeDispatcher](index.md#itypedispatcher)
* [IUnionType](index.md#iuniontype)
* [IValidationContext](index.md#ivalidationcontext)
* [IValidationResult](index.md#ivalidationresult)
* [Instance](index.md#instance)
* [LivelinessMode](index.md#livelinessmode)
* [OnReferenceInvalidated](index.md#onreferenceinvalidated)
* [OnReferenceInvalidatedEvent](index.md#onreferenceinvalidatedevent)
* [ReferenceIdentifier](index.md#referenceidentifier)
* [ReferenceOptions](index.md#referenceoptions)
* [SnapshotIn](index.md#snapshotin)
* [SnapshotOrInstance](index.md#snapshotorinstance)
* [SnapshotOut](index.md#snapshotout)
### Variables
* [DatePrimitive](index.md#const-dateprimitive)
* [boolean](index.md#const-boolean)
* [finite](index.md#const-finite)
* [float](index.md#const-float)
* [identifier](index.md#const-identifier)
* [identifierNumber](index.md#const-identifiernumber)
* [integer](index.md#const-integer)
* [nullType](index.md#const-nulltype)
* [number](index.md#const-number)
* [string](index.md#const-string)
* [undefinedType](index.md#const-undefinedtype)
### Functions
* [addDisposer](index.md#adddisposer)
* [addMiddleware](index.md#addmiddleware)
* [applyAction](index.md#applyaction)
* [applyPatch](index.md#applypatch)
* [applySnapshot](index.md#applysnapshot)
* [array](index.md#array)
* [cast](index.md#cast)
* [castFlowReturn](index.md#castflowreturn)
* [castToReferenceSnapshot](index.md#casttoreferencesnapshot)
* [castToSnapshot](index.md#casttosnapshot)
* [clone](index.md#clone)
* [compose](index.md#compose)
* [createActionTrackingMiddleware](index.md#createactiontrackingmiddleware)
* [createActionTrackingMiddleware2](index.md#createactiontrackingmiddleware2)
* [custom](index.md#custom)
* [decorate](index.md#decorate)
* [destroy](index.md#destroy)
* [detach](index.md#detach)
* [enumeration](index.md#enumeration)
* [escapeJsonPath](index.md#escapejsonpath)
* [flow](index.md#flow)
* [frozen](index.md#frozen)
* [getChildType](index.md#getchildtype)
* [getEnv](index.md#getenv)
* [getIdentifier](index.md#getidentifier)
* [getLivelinessChecking](index.md#getlivelinesschecking)
* [getMembers](index.md#getmembers)
* [getNodeId](index.md#getnodeid)
* [getParent](index.md#getparent)
* [getParentOfType](index.md#getparentoftype)
* [getPath](index.md#getpath)
* [getPathParts](index.md#getpathparts)
* [getPropertyMembers](index.md#getpropertymembers)
* [getRelativePath](index.md#getrelativepath)
* [getRoot](index.md#getroot)
* [getRunningActionContext](index.md#getrunningactioncontext)
* [getSnapshot](index.md#getsnapshot)
* [getType](index.md#gettype)
* [hasEnv](index.md#hasenv)
* [hasParent](index.md#hasparent)
* [hasParentOfType](index.md#hasparentoftype)
* [isActionContextChildOf](index.md#isactioncontextchildof)
* [isActionContextThisOrChildOf](index.md#isactioncontextthisorchildof)
* [isAlive](index.md#isalive)
* [isArrayType](index.md#isarraytype)
* [isFrozenType](index.md#isfrozentype)
* [isIdentifierType](index.md#isidentifiertype)
* [isLateType](index.md#islatetype)
* [isLiteralType](index.md#isliteraltype)
* [isMapType](index.md#ismaptype)
* [isModelType](index.md#ismodeltype)
* [isOptionalType](index.md#isoptionaltype)
* [isPrimitiveType](index.md#isprimitivetype)
* [isProtected](index.md#isprotected)
* [isReferenceType](index.md#isreferencetype)
* [isRefinementType](index.md#isrefinementtype)
* [isRoot](index.md#isroot)
* [isStateTreeNode](index.md#isstatetreenode)
* [isType](index.md#istype)
* [isUnionType](index.md#isuniontype)
* [isValidReference](index.md#isvalidreference)
* [joinJsonPath](index.md#joinjsonpath)
* [late](index.md#late)
* [lazy](index.md#lazy)
* [literal](index.md#literal)
* [map](index.md#map)
* [maybe](index.md#maybe)
* [maybeNull](index.md#maybenull)
* [model](index.md#model)
* [onAction](index.md#onaction)
* [onPatch](index.md#onpatch)
* [onSnapshot](index.md#onsnapshot)
* [optional](index.md#optional)
* [protect](index.md#protect)
* [recordActions](index.md#recordactions)
* [recordPatches](index.md#recordpatches)
* [reference](index.md#reference)
* [refinement](index.md#refinement)
* [resolveIdentifier](index.md#resolveidentifier)
* [resolvePath](index.md#resolvepath)
* [safeReference](index.md#safereference)
* [setLivelinessChecking](index.md#setlivelinesschecking)
* [snapshotProcessor](index.md#snapshotprocessor)
* [splitJsonPath](index.md#splitjsonpath)
* [toGenerator](index.md#togenerator)
* [toGeneratorFunction](index.md#togeneratorfunction)
* [tryReference](index.md#tryreference)
* [tryResolve](index.md#tryresolve)
* [typecheck](index.md#typecheck)
* [unescapeJsonPath](index.md#unescapejsonpath)
* [union](index.md#union)
* [unprotect](index.md#unprotect)
* [walk](index.md#walk)
### Object literals
* [types](index.md#const-types)
## Type aliases
### IDisposer
Ƭ **IDisposer**: *function*
*Defined in [src/utils.ts:35](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/utils.ts#L35)*
A generic disposer.
#### Type declaration:
▸ (): *void*
___
### IHooksGetter
Ƭ **IHooksGetter**: *function*
*Defined in [src/core/node/Hook.ts:19](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/node/Hook.ts#L19)*
#### Type declaration:
▸ (`self`: T): *[IHooks](interfaces/ihooks.md)*
**Parameters:**
Name | Type |
------ | ------ |
`self` | T |
___
### IMiddlewareEventType
Ƭ **IMiddlewareEventType**: *"action" | "flow_spawn" | "flow_resume" | "flow_resume_error" | "flow_return" | "flow_throw"*
*Defined in [src/core/action.ts:16](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/action.ts#L16)*
___
### IMiddlewareHandler
Ƭ **IMiddlewareHandler**: *function*
*Defined in [src/core/action.ts:54](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/action.ts#L54)*
#### Type declaration:
▸ (`actionCall`: [IMiddlewareEvent](interfaces/imiddlewareevent.md), `next`: function, `abort`: function): *any*
**Parameters:**
▪ **actionCall**: *[IMiddlewareEvent](interfaces/imiddlewareevent.md)*
▪ **next**: *function*
▸ (`actionCall`: [IMiddlewareEvent](interfaces/imiddlewareevent.md), `callback?`: undefined | function): *void*
**Parameters:**
Name | Type |
------ | ------ |
`actionCall` | [IMiddlewareEvent](interfaces/imiddlewareevent.md) |
`callback?` | undefined | function |
▪ **abort**: *function*
▸ (`value`: any): *void*
**Parameters:**
Name | Type |
------ | ------ |
`value` | any |
___
### ITypeDispatcher
Ƭ **ITypeDispatcher**: *function*
*Defined in [src/types/utility-types/union.ts:22](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/union.ts#L22)*
#### Type declaration:
▸ (`snapshot`: Types[number]["SnapshotType"]): *Types[number]*
**Parameters:**
Name | Type |
------ | ------ |
`snapshot` | Types[number]["SnapshotType"] |
___
### IUnionType
Ƭ **IUnionType**: *ITypeUnion‹Types[number]["CreationType"], Types[number]["SnapshotType"], Types[number]["TypeWithoutSTN"]›*
*Defined in [src/types/utility-types/union.ts:169](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/union.ts#L169)*
___
### IValidationContext
Ƭ **IValidationContext**: *[IValidationContextEntry](interfaces/ivalidationcontextentry.md)[]*
*Defined in [src/core/type/type-checker.ts:23](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type-checker.ts#L23)*
Array of validation context entries
___
### IValidationResult
Ƭ **IValidationResult**: *[IValidationError](interfaces/ivalidationerror.md)[]*
*Defined in [src/core/type/type-checker.ts:36](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type-checker.ts#L36)*
Type validation result, which is an array of type validation errors
___
### Instance
Ƭ **Instance**: *T extends object ? T["Type"] : T*
*Defined in [src/core/type/type.ts:233](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L233)*
The instance representation of a given type.
___
### LivelinessMode
Ƭ **LivelinessMode**: *"warn" | "error" | "ignore"*
*Defined in [src/core/node/livelinessChecking.ts:7](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/node/livelinessChecking.ts#L7)*
Defines what MST should do when running into reads / writes to objects that have died.
- `"warn"`: Print a warning (default).
- `"error"`: Throw an exception.
- "`ignore`": Do nothing.
___
### OnReferenceInvalidated
Ƭ **OnReferenceInvalidated**: *function*
*Defined in [src/types/utility-types/reference.ts:45](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/reference.ts#L45)*
#### Type declaration:
▸ (`event`: [OnReferenceInvalidatedEvent](index.md#onreferenceinvalidatedevent)‹STN›): *void*
**Parameters:**
Name | Type |
------ | ------ |
`event` | [OnReferenceInvalidatedEvent](index.md#onreferenceinvalidatedevent)‹STN› |
___
### OnReferenceInvalidatedEvent
Ƭ **OnReferenceInvalidatedEvent**: *object*
*Defined in [src/types/utility-types/reference.ts:36](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/reference.ts#L36)*
#### Type declaration:
___
### ReferenceIdentifier
Ƭ **ReferenceIdentifier**: *string | number*
*Defined in [src/types/utility-types/identifier.ts:147](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/identifier.ts#L147)*
Valid types for identifiers.
___
### ReferenceOptions
Ƭ **ReferenceOptions**: *[ReferenceOptionsGetSet](interfaces/referenceoptionsgetset.md)‹IT› | [ReferenceOptionsOnInvalidated](interfaces/referenceoptionsoninvalidated.md)‹IT› | [ReferenceOptionsGetSet](interfaces/referenceoptionsgetset.md)‹IT› & [ReferenceOptionsOnInvalidated](interfaces/referenceoptionsoninvalidated.md)‹IT›*
*Defined in [src/types/utility-types/reference.ts:481](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/reference.ts#L481)*
___
### SnapshotIn
Ƭ **SnapshotIn**: *T extends object ? T["CreationType"] : T extends IStateTreeNode ? IT["CreationType"] : T*
*Defined in [src/core/type/type.ts:238](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L238)*
The input (creation) snapshot representation of a given type.
___
### SnapshotOrInstance
Ƭ **SnapshotOrInstance**: *[SnapshotIn](index.md#snapshotin)‹T› | [Instance](index.md#instance)‹T›*
*Defined in [src/core/type/type.ts:279](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L279)*
A type which is equivalent to the union of SnapshotIn and Instance types of a given typeof TYPE or typeof VARIABLE.
For primitives it defaults to the primitive itself.
For example:
- `SnapshotOrInstance = SnapshotIn