jotai-state-tree 1.6.0 → 1.7.1
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.
|
|
3
|
+
"version": "1.7.1",
|
|
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
|
+
});
|