@@ -3,9 +3,13 @@ import React from "react";
33import { test , expect } from "./utils/coverage" ;
44import { MockedProvider } from "@apollo/client/testing" ;
55import { MemoryRouter , Route , Routes } from "react-router-dom" ;
6- import { UserProfileRoute } from "../src/components/routes/UserProfileRoute" ;
76import { UserProfile } from "../src/views/UserProfile" ;
8- import { GET_USER , GET_USER_BADGES } from "../src/graphql/queries" ;
7+ import {
8+ UserProfileRouteLoadingWrapper ,
9+ UserProfileRouteResetWrapper ,
10+ UserProfileRouteSeededWrapper ,
11+ } from "./UserProfileRouteTestWrappers" ;
12+ import { GET_USER_BADGES } from "../src/graphql/queries" ;
913import { docScreenshot , releaseScreenshot } from "./utils/docScreenshot" ;
1014
1115// Mock user data
@@ -25,145 +29,97 @@ const mockPublicUser = {
2529 totalDocumentsUploaded : 10 ,
2630} ;
2731
28- test . describe ( "UserProfileRoute - Hook Ordering Regression (Issue #1295)" , ( ) => {
29- test ( "mounts with slug under a Routes tree that also defines /profile (no 'Rendered more hooks' crash)" , async ( {
32+ test . describe ( "UserProfileRoute - State-Driven Rendering" , ( ) => {
33+ // UserProfileRoute is a dumb consumer of openedUser / routeLoading /
34+ // routeError, all owned by CentralRouteManager. The wrappers seed those
35+ // reactive vars in the browser context — see UserProfileRouteTestWrappers.
36+
37+ test ( "renders the loading display when routeLoading is true and no user is resolved" , async ( {
3038 mount,
3139 page,
3240 } ) => {
33- // This test exercises the scenario that surfaced the original bug: both
34- // the redirect route (/profile, no slug) and the render route
35- // (/users/:slug) live in the same <Routes> tree. When useQuery was below
36- // the `!slug` early return, React could (depending on fiber reuse)
37- // compare hook call counts across the two renders and throw
38- // "Rendered more hooks than during the previous render". With the fix,
39- // useQuery is called unconditionally and skip: !slug gates the network
40- // request, so the component renders without crashing on either path.
41- //
42- // NOTE: This is a narrow regression guard rather than a full reproduction
43- // of the original crash. Reproducing the exact crash in-process requires
44- // keeping the same fiber across /profile -> /users/:slug, which React
45- // Router <Routes> doesn't do (it unmounts the old element when the
46- // matched path changes). The positive assertions below (loading display
47- // for the slug path, absence of hook-count errors) prove the hook
48- // ordering is stable — exactly what the fix guarantees statically.
49- const consoleErrors : string [ ] = [ ] ;
50- page . on ( "console" , ( msg ) => {
51- if ( msg . type ( ) === "error" ) consoleErrors . push ( msg . text ( ) ) ;
52- } ) ;
53-
54- // Delay the mock so the render parks on the loading state, giving us
55- // positive visible evidence (not just the absence of a crash) that
56- // useQuery ran under skip:false on the slug path.
57- const mocks = [
58- {
59- request : {
60- query : GET_USER ,
61- variables : { slug : "publicuser-123" } ,
62- } ,
63- delay : 5000 ,
64- result : {
65- data : {
66- userBySlug : mockPublicUser ,
67- } ,
68- } ,
69- } ,
70- ] ;
71-
7241 const component = await mount (
73- < MockedProvider mocks = { mocks } addTypename = { false } >
42+ < MockedProvider mocks = { [ ] } addTypename = { false } >
7443 < MemoryRouter initialEntries = { [ "/users/publicuser-123" ] } >
7544 < Routes >
76- < Route path = "/profile" element = { < UserProfileRoute /> } />
77- < Route path = "/users/:slug" element = { < UserProfileRoute /> } />
45+ < Route
46+ path = "/users/:slug"
47+ element = { < UserProfileRouteLoadingWrapper /> }
48+ />
7849 </ Routes >
7950 </ MemoryRouter >
8051 </ MockedProvider >
8152 ) ;
8253
83- // Wait for positive DOM evidence that the slug render reached the
84- // useQuery call (skip:false branch) instead of a silent bailout.
85- await expect ( page . locator ( "text=Loading profile..." ) ) . toBeVisible ( {
86- timeout : 10000 ,
87- } ) ;
88-
89- const hookErrors = consoleErrors . filter ( ( e ) =>
90- e . includes ( "Rendered more hooks than during the previous render" )
91- ) ;
92- expect ( hookErrors ) . toEqual ( [ ] ) ;
54+ await expect ( page . locator ( "text=Loading profile..." ) ) . toBeVisible ( ) ;
9355
9456 await component . unmount ( ) ;
9557 } ) ;
96- } ) ;
9758
98- test . describe ( "UserProfile View - Loading and Error States" , ( ) => {
99- test ( "should show loading state while fetching user data" , async ( {
59+ test ( "renders the not-found display when no user is resolved" , async ( {
10060 mount,
10161 page,
10262 } ) => {
103- const mocks = [
104- {
105- request : {
106- query : GET_USER ,
107- variables : { slug : "publicuser-123" } ,
108- } ,
109- delay : 2000 , // Simulate slow network
110- result : {
111- data : {
112- userBySlug : mockPublicUser ,
113- } ,
114- } ,
115- } ,
116- ] ;
117-
11863 const component = await mount (
119- < MockedProvider mocks = { mocks } addTypename = { false } >
120- < MemoryRouter initialEntries = { [ "/users/publicuser-123 " ] } >
64+ < MockedProvider mocks = { [ ] } addTypename = { false } >
65+ < MemoryRouter initialEntries = { [ "/users/nonexistent-user " ] } >
12166 < Routes >
122- < Route path = "/users/:slug" element = { < UserProfileRoute /> } />
67+ < Route
68+ path = "/users/:slug"
69+ element = { < UserProfileRouteResetWrapper /> }
70+ />
12371 </ Routes >
12472 </ MemoryRouter >
12573 </ MockedProvider >
12674 ) ;
12775
128- // Check loading spinner is visible
129- await expect ( page . locator ( "text=Loading profile..." ) ) . toBeVisible ( ) ;
76+ await expect ( page . locator ( "text=User Not Found" ) ) . toBeVisible ( ) ;
13077
13178 await component . unmount ( ) ;
13279 } ) ;
13380
134- test ( "should show error message when user not found " , async ( {
81+ test ( "renders the resolved profile when openedUser is populated " , async ( {
13582 mount,
13683 page,
13784 } ) => {
138- const mocks = [
139- {
140- request : {
141- query : GET_USER ,
142- variables : { slug : "nonexistent-user" } ,
143- } ,
144- result : {
145- data : {
146- userBySlug : null ,
85+ const badgesMock = {
86+ request : {
87+ query : GET_USER_BADGES ,
88+ variables : { userId : "VXNlclR5cGU6MQ==" , limit : 100 } ,
89+ } ,
90+ result : {
91+ data : {
92+ userBadges : {
93+ edges : [ ] ,
94+ pageInfo : {
95+ hasNextPage : false ,
96+ hasPreviousPage : false ,
97+ startCursor : null ,
98+ endCursor : null ,
99+ } ,
147100 } ,
148101 } ,
149102 } ,
150- ] ;
103+ } ;
151104
152105 const component = await mount (
153- < MockedProvider mocks = { mocks } addTypename = { false } >
154- < MemoryRouter initialEntries = { [ "/users/nonexistent-user " ] } >
106+ < MockedProvider mocks = { [ badgesMock ] } addTypename = { false } >
107+ < MemoryRouter initialEntries = { [ "/users/publicuser-123 " ] } >
155108 < Routes >
156- < Route path = "/users/:slug" element = { < UserProfileRoute /> } />
109+ < Route
110+ path = "/users/:slug"
111+ element = {
112+ < UserProfileRouteSeededWrapper user = { mockPublicUser as any } />
113+ }
114+ />
157115 </ Routes >
158116 </ MemoryRouter >
159117 </ MockedProvider >
160118 ) ;
161119
162- // Wait for query to complete
163- await page . waitForTimeout ( 1000 ) ;
164-
165- // Check error message is displayed
166- await expect ( page . locator ( "text=User not found" ) ) . toBeVisible ( ) ;
120+ await expect ( page . locator ( "text=Public User" ) ) . toBeVisible ( {
121+ timeout : 10000 ,
122+ } ) ;
167123
168124 await component . unmount ( ) ;
169125 } ) ;
0 commit comments