jotai-state-tree 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jotai-state-tree",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "MobX-State-Tree API compatible library powered by Jotai",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -36,6 +36,7 @@
36
36
  "example:telemetry": "npm --prefix examples/dashboard-live-telemetry run dev",
37
37
  "example:form": "npm --prefix examples/form-builder-dynamic run dev",
38
38
  "example:notes": "npm --prefix examples/note-taking-ssr run dev",
39
+ "example:router": "npm --prefix examples/multipage-router run dev",
39
40
  "examples:install": "for dir in examples/*; do [ -d \"$dir\" ] && npm install --prefix \"$dir\"; done"
40
41
  },
41
42
  "keywords": [
@@ -0,0 +1,199 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+ import React from 'react';
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
+ import { render, screen, act, waitFor, cleanup } from '@testing-library/react';
7
+ import userEvent from '@testing-library/user-event';
8
+ import { clearAllRegistries, resetGlobalStore } from '../../index';
9
+ import { App } from '../../../examples/multipage-router/src/App';
10
+ beforeEach(() => {
11
+ clearAllRegistries();
12
+ resetGlobalStore();
13
+
14
+ if (typeof window !== 'undefined') {
15
+ vi.stubGlobal('location', {
16
+ pathname: '/',
17
+ search: '',
18
+ hash: '',
19
+ href: 'http://localhost/',
20
+ });
21
+ }
22
+ });
23
+
24
+ afterEach(() => {
25
+ cleanup();
26
+ clearAllRegistries();
27
+ resetGlobalStore();
28
+ });
29
+
30
+ describe('Multipage Bookstore Router Example App', () => {
31
+ it('should support home layout, catalog query filters, parameter matching, wildcards, auth redirection, login, and admin views', async () => {
32
+ const user = userEvent.setup();
33
+
34
+ render(
35
+ <React.StrictMode>
36
+ <App />
37
+ </React.StrictMode>
38
+ );
39
+
40
+ // ========================================================================
41
+ // 1. Initial Home Screen Check
42
+ // ========================================================================
43
+ expect(screen.getByText('Welcome to State Bookshop')).toBeDefined();
44
+
45
+ // State Inspector Check
46
+ expect(screen.getByText('"/"')).toBeDefined();
47
+ expect(screen.getByText('INITIAL')).toBeDefined();
48
+ expect(screen.getByText('"home"')).toBeDefined();
49
+
50
+ // ========================================================================
51
+ // 2. Navigation to Catalog and Query Filtering
52
+ // ========================================================================
53
+ const catalogNavLink = screen.getByRole('link', { name: /Catalog/ });
54
+ await user.click(catalogNavLink);
55
+
56
+ await waitFor(() => {
57
+ expect(screen.getByText('Book Catalog')).toBeDefined();
58
+ });
59
+ expect(screen.getByText('"/books"')).toBeDefined();
60
+ expect(screen.getByText('PUSH')).toBeDefined();
61
+
62
+ // Check list of books (should display all initial books)
63
+ expect(screen.getByText('Designing Data-Intensive Applications')).toBeDefined();
64
+ expect(screen.getByText('Dune')).toBeDefined();
65
+ expect(screen.getByText('The Hobbit')).toBeDefined();
66
+
67
+ // Filter by Tech category
68
+ const techCategoryButton = screen.getByRole('button', { name: 'Tech' });
69
+ await user.click(techCategoryButton);
70
+
71
+ await waitFor(() => {
72
+ // Should show Tech books
73
+ expect(screen.getByText('Designing Data-Intensive Applications')).toBeDefined();
74
+ expect(screen.getByText('The Pragmatic Programmer')).toBeDefined();
75
+ // Should filter out Sci-Fi and Fantasy
76
+ expect(screen.queryByText('Dune')).toBeNull();
77
+ expect(screen.queryByText('The Hobbit')).toBeNull();
78
+ });
79
+ expect(screen.getByText(/"category": "Tech"/)).toBeDefined();
80
+
81
+ // Search query within Tech category
82
+ const searchInput = screen.getByPlaceholderText('Search by title or author...');
83
+ const searchButton = screen.getByRole('button', { name: 'Search' });
84
+
85
+ await user.type(searchInput, 'Pragmatic');
86
+ await user.click(searchButton);
87
+
88
+ await waitFor(() => {
89
+ expect(screen.queryByText('Designing Data-Intensive Applications')).toBeNull();
90
+ expect(screen.getByText('The Pragmatic Programmer')).toBeDefined();
91
+ });
92
+ expect(screen.getByText(/"search": "Pragmatic"/)).toBeDefined();
93
+
94
+ // ========================================================================
95
+ // 3. Dynamic Route Parameter Matching (Book Details)
96
+ // ========================================================================
97
+ const bookCard = screen.getByText('The Pragmatic Programmer');
98
+ await user.click(bookCard);
99
+
100
+ await waitFor(() => {
101
+ expect(screen.getByText('Andy Hunt & Dave Thomas')).toBeDefined();
102
+ expect(screen.getByText('2')).toBeDefined();
103
+ });
104
+ expect(screen.getByText('"/books/2"')).toBeDefined();
105
+ expect(screen.getByText('"book-details"')).toBeDefined();
106
+ expect(screen.getByText(/"id": "2"/)).toBeDefined();
107
+
108
+ // Click Go Back
109
+ const backBtn = screen.getByRole('button', { name: /Go Back/ });
110
+
111
+ // Mock window.history.back to simulate popping location back to catalog
112
+ window.history.back = vi.fn().mockImplementation(() => {
113
+ act(() => {
114
+ vi.stubGlobal('location', {
115
+ pathname: '/books',
116
+ search: '?category=Tech&search=Pragmatic',
117
+ hash: '',
118
+ href: 'http://localhost/books?category=Tech&search=Pragmatic',
119
+ });
120
+ window.dispatchEvent(new PopStateEvent('popstate'));
121
+ });
122
+ });
123
+
124
+ await user.click(backBtn);
125
+ await waitFor(() => {
126
+ expect(screen.getByText('Book Catalog')).toBeDefined();
127
+ expect(screen.getByText('The Pragmatic Programmer')).toBeDefined();
128
+ });
129
+ expect(screen.getByText('"/books"')).toBeDefined();
130
+
131
+ // Restore stubbed globals so subsequent navigations work
132
+ vi.unstubAllGlobals();
133
+
134
+ // ========================================================================
135
+ // 4. Wildcard Route Matching
136
+ // ========================================================================
137
+ const filesNavLink = screen.getByRole('link', { name: /Files/ });
138
+ await user.click(filesNavLink);
139
+
140
+ await waitFor(() => {
141
+ expect(screen.getByText('Wildcard File Browser')).toBeDefined();
142
+ });
143
+ expect(screen.getByText('"/files"')).toBeDefined();
144
+ expect(screen.getByText('"files"')).toBeDefined();
145
+
146
+ // Click on a file test link
147
+ const duneFileBtn = screen.getByRole('button', { name: 'dune.jpg' });
148
+ await user.click(duneFileBtn);
149
+
150
+ await waitFor(() => {
151
+ expect(screen.getByText('/images/covers/dune.jpg')).toBeDefined();
152
+ });
153
+ expect(screen.getByText('"/files/images/covers/dune.jpg"')).toBeDefined();
154
+ expect(screen.getByText(/"\*": "\/images\/covers\/dune.jpg"/)).toBeDefined();
155
+
156
+ // ========================================================================
157
+ // 5. Auth Navigation Guards & Interception Redirects
158
+ // ========================================================================
159
+ const adminNavLink = screen.getByRole('link', { name: /Admin Panel/ });
160
+ await user.click(adminNavLink);
161
+
162
+ // Intercepted and Redirected to login
163
+ await waitFor(() => {
164
+ expect(screen.getByText('Administrative Login')).toBeDefined();
165
+ expect(screen.getByText(/You were redirected because access to/)).toBeDefined();
166
+ });
167
+ expect(screen.getByText('"/login"')).toBeDefined();
168
+ expect(screen.getByText(/"redirect": "\/admin"/)).toBeDefined();
169
+
170
+ // ========================================================================
171
+ // 6. Login and Return Redirection
172
+ // ========================================================================
173
+ const usernameInput = screen.getByPlaceholderText('Enter your name (e.g. brandon)');
174
+ const submitLoginBtn = screen.getByRole('button', { name: 'Login & Continue' });
175
+
176
+ await user.type(usernameInput, 'brandon');
177
+ await user.click(submitLoginBtn);
178
+
179
+ // Redirected back to the original target: /admin
180
+ await waitFor(() => {
181
+ expect(screen.getByText('Admin Dashboard')).toBeDefined();
182
+ expect(screen.getAllByText('brandon').length).toBeGreaterThan(0);
183
+ });
184
+ expect(screen.getByText('"/admin"')).toBeDefined();
185
+ expect(screen.getByText('"admin"')).toBeDefined();
186
+
187
+ // ========================================================================
188
+ // 7. Logout Flow
189
+ // ========================================================================
190
+ const logoutBtn = screen.getByRole('button', { name: 'Logout' });
191
+ await user.click(logoutBtn);
192
+
193
+ await waitFor(() => {
194
+ expect(screen.getByText('Welcome to State Bookshop')).toBeDefined();
195
+ expect(screen.queryAllByText('brandon').length).toBe(0);
196
+ });
197
+ expect(screen.getByText('"/"')).toBeDefined();
198
+ });
199
+ });