@shipfox/client-projects 17.0.0 → 22.0.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.
Files changed (58) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +42 -0
  3. package/dist/components/source-strip.d.ts.map +1 -1
  4. package/dist/components/source-strip.js +49 -45
  5. package/dist/components/source-strip.js.map +1 -1
  6. package/dist/pages/create-project-page.d.ts.map +1 -1
  7. package/dist/pages/create-project-page.js +161 -175
  8. package/dist/pages/create-project-page.js.map +1 -1
  9. package/dist/pages/create-project-page.stories.js +1 -1
  10. package/dist/pages/create-project-page.stories.js.map +1 -1
  11. package/dist/pages/project-settings-page.d.ts.map +1 -1
  12. package/dist/pages/project-settings-page.js +82 -84
  13. package/dist/pages/project-settings-page.js.map +1 -1
  14. package/dist/pages/project-settings-page.stories.js +1 -1
  15. package/dist/pages/project-settings-page.stories.js.map +1 -1
  16. package/dist/pages/projects-hub-page.d.ts.map +1 -1
  17. package/dist/pages/projects-hub-page.js +160 -159
  18. package/dist/pages/projects-hub-page.js.map +1 -1
  19. package/dist/pages/projects-hub-page.stories.d.ts +22 -0
  20. package/dist/pages/projects-hub-page.stories.d.ts.map +1 -0
  21. package/dist/pages/projects-hub-page.stories.js +214 -0
  22. package/dist/pages/projects-hub-page.stories.js.map +1 -0
  23. package/dist/routes/create-project.d.ts +3 -0
  24. package/dist/routes/create-project.js +3 -0
  25. package/dist/routes/create-project.js.map +1 -1
  26. package/dist/routes/home.d.ts +3 -0
  27. package/dist/routes/home.js +3 -0
  28. package/dist/routes/home.js.map +1 -1
  29. package/dist/routes/project-index.d.ts +3 -0
  30. package/dist/routes/project-index.d.ts.map +1 -1
  31. package/dist/routes/project-index.js +3 -0
  32. package/dist/routes/project-index.js.map +1 -1
  33. package/dist/routes/project-settings-index.d.ts +3 -0
  34. package/dist/routes/project-settings-index.d.ts.map +1 -1
  35. package/dist/routes/project-settings-index.js +3 -0
  36. package/dist/routes/project-settings-index.js.map +1 -1
  37. package/dist/routes/project-settings.d.ts +3 -0
  38. package/dist/routes/project-settings.js +3 -0
  39. package/dist/routes/project-settings.js.map +1 -1
  40. package/dist/tsconfig.test.tsbuildinfo +1 -1
  41. package/package.json +7 -7
  42. package/src/components/source-strip.tsx +29 -26
  43. package/src/pages/create-project-page.stories.tsx +1 -1
  44. package/src/pages/create-project-page.test.tsx +85 -1
  45. package/src/pages/create-project-page.tsx +165 -153
  46. package/src/pages/home-router.test.tsx +2 -1
  47. package/src/pages/project-settings-page.stories.tsx +1 -1
  48. package/src/pages/project-settings-page.test.tsx +10 -1
  49. package/src/pages/project-settings-page.tsx +88 -76
  50. package/src/pages/projects-hub-page.stories.tsx +203 -0
  51. package/src/pages/projects-hub-page.test.tsx +90 -10
  52. package/src/pages/projects-hub-page.tsx +136 -125
  53. package/src/routes/create-project.tsx +4 -1
  54. package/src/routes/home.tsx +1 -0
  55. package/src/routes/project-index.tsx +1 -0
  56. package/src/routes/project-settings-index.tsx +1 -0
  57. package/src/routes/project-settings.tsx +4 -1
  58. package/tsconfig.build.tsbuildinfo +1 -1
@@ -13,8 +13,9 @@ import {Callout} from '@shipfox/react-ui/callout';
13
13
  import {EmptyState} from '@shipfox/react-ui/empty-state';
14
14
  import {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';
15
15
  import {FullPageLoader} from '@shipfox/react-ui/loader';
16
+ import {Panel, PanelBody} from '@shipfox/react-ui/panel';
16
17
  import {toast} from '@shipfox/react-ui/toast';
17
- import {Header, Text} from '@shipfox/react-ui/typography';
18
+ import {Header} from '@shipfox/react-ui/typography';
18
19
  import {useForm} from '@tanstack/react-form';
19
20
  import {useNavigate} from '@tanstack/react-router';
20
21
  import {useState} from 'react';
@@ -124,86 +125,97 @@ function ProjectSettingsForm({project}: {project: Project}) {
124
125
  return (
125
126
  <>
126
127
  <div className="flex min-w-0 flex-col gap-section">
127
- <header className="flex flex-col gap-inline">
128
- <Header variant="h1">General</Header>
129
- <Text size="sm" className="text-foreground-neutral-muted">
130
- Update the project name and the slug used in its URLs.
131
- </Text>
132
- </header>
128
+ <Header variant="h1">General</Header>
133
129
 
134
- {formError ? (
135
- <Callout role="alert" type="error">
136
- {formError}
137
- </Callout>
138
- ) : null}
130
+ <Panel>
131
+ <PanelBody className="gap-group p-panel">
132
+ {formError ? (
133
+ <Callout role="alert" type="error">
134
+ {formError}
135
+ </Callout>
136
+ ) : null}
139
137
 
140
- <form
141
- className="flex max-w-[560px] flex-col gap-group"
142
- noValidate
143
- onSubmit={(event) => {
144
- event.preventDefault();
145
- event.stopPropagation();
146
- void form.handleSubmit();
147
- }}
148
- >
149
- <form.Field
150
- name="name"
151
- validators={{
152
- onBlur: ({value}) =>
153
- displayNameFieldError(value, 'Project name', createProjectBodySchema.shape.name),
154
- onSubmit: ({value}) =>
155
- displayNameFieldError(value, 'Project name', createProjectBodySchema.shape.name),
156
- }}
157
- >
158
- {(field) => (
159
- <FormField label="Project name" id="project-settings-name" error={fieldError(field)}>
160
- <FormFieldInput
161
- name="name"
162
- type="text"
163
- value={field.state.value}
164
- onChange={(event) => field.handleChange(event.target.value)}
165
- onBlur={field.handleBlur}
166
- />
167
- </FormField>
168
- )}
169
- </form.Field>
138
+ <form
139
+ className="flex w-full max-w-[560px] flex-col gap-group"
140
+ noValidate
141
+ onSubmit={(event) => {
142
+ event.preventDefault();
143
+ event.stopPropagation();
144
+ void form.handleSubmit();
145
+ }}
146
+ >
147
+ <form.Field
148
+ name="name"
149
+ validators={{
150
+ onBlur: ({value}) =>
151
+ displayNameFieldError(
152
+ value,
153
+ 'Project name',
154
+ createProjectBodySchema.shape.name,
155
+ ),
156
+ onSubmit: ({value}) =>
157
+ displayNameFieldError(
158
+ value,
159
+ 'Project name',
160
+ createProjectBodySchema.shape.name,
161
+ ),
162
+ }}
163
+ >
164
+ {(field) => (
165
+ <FormField
166
+ label="Project name"
167
+ id="project-settings-name"
168
+ error={fieldError(field)}
169
+ >
170
+ <FormFieldInput
171
+ name="name"
172
+ type="text"
173
+ value={field.state.value}
174
+ onChange={(event) => field.handleChange(event.target.value)}
175
+ onBlur={field.handleBlur}
176
+ />
177
+ </FormField>
178
+ )}
179
+ </form.Field>
170
180
 
171
- <form.Field
172
- name="slug"
173
- validators={{
174
- onBlur: createProjectBodySchema.shape.slug,
175
- onSubmit: createProjectBodySchema.shape.slug,
176
- }}
177
- >
178
- {(field) => (
179
- <SlugField
180
- id="project-settings-slug"
181
- label="Project slug"
181
+ <form.Field
182
182
  name="slug"
183
- value={field.state.value}
184
- onChange={(value) => field.handleChange(value)}
185
- onBlur={field.handleBlur}
186
- error={fieldError(field)}
187
- description={
188
- <span className="break-all font-code">
189
- /w/{workspace.slug}/p/{field.state.value}
190
- </span>
191
- }
192
- placeholder="platform"
193
- className="font-code"
194
- currentSlug={project.slug}
195
- checkEnabled
196
- debounceMs={0}
197
- isValid={isSlugValid}
198
- checkAvailability={checkProjectSlugAvailability}
199
- />
200
- )}
201
- </form.Field>
183
+ validators={{
184
+ onBlur: createProjectBodySchema.shape.slug,
185
+ onSubmit: createProjectBodySchema.shape.slug,
186
+ }}
187
+ >
188
+ {(field) => (
189
+ <SlugField
190
+ id="project-settings-slug"
191
+ label="Project slug"
192
+ name="slug"
193
+ value={field.state.value}
194
+ onChange={(value) => field.handleChange(value)}
195
+ onBlur={field.handleBlur}
196
+ error={fieldError(field)}
197
+ description={
198
+ <span className="break-all font-code">
199
+ /w/{workspace.slug}/p/{field.state.value}
200
+ </span>
201
+ }
202
+ placeholder="platform"
203
+ className="font-code"
204
+ currentSlug={project.slug}
205
+ checkEnabled
206
+ debounceMs={0}
207
+ isValid={isSlugValid}
208
+ checkAvailability={checkProjectSlugAvailability}
209
+ />
210
+ )}
211
+ </form.Field>
202
212
 
203
- <Button type="submit" isLoading={updateProject.isPending} className="self-start">
204
- Save changes
205
- </Button>
206
- </form>
213
+ <Button type="submit" isLoading={updateProject.isPending} className="self-start">
214
+ Save changes
215
+ </Button>
216
+ </form>
217
+ </PanelBody>
218
+ </Panel>
207
219
  </div>
208
220
 
209
221
  <SlugChangeWarning
@@ -0,0 +1,203 @@
1
+ import {configureApiClient} from '@shipfox/client-api';
2
+ import {type AuthState, authStateAtom} from '@shipfox/client-auth';
3
+ import {Toaster} from '@shipfox/react-ui/toast';
4
+ import type {Meta, StoryObj} from '@storybook/react';
5
+ import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
6
+ import {
7
+ createMemoryHistory,
8
+ createRootRoute,
9
+ createRoute,
10
+ createRouter,
11
+ Outlet,
12
+ RouterProvider,
13
+ } from '@tanstack/react-router';
14
+ import {createStore, Provider as JotaiProvider} from 'jotai';
15
+ import {useMemo} from 'react';
16
+ import {ProjectsHubPage} from './projects-hub-page.js';
17
+
18
+ const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
19
+ const CONNECTION_ID = '33333333-3333-4333-8333-333333333333';
20
+
21
+ type Scenario = 'list' | 'empty' | 'loading' | 'error';
22
+
23
+ const authState: AuthState = {
24
+ status: 'authenticated',
25
+ token: 'token',
26
+ user: {
27
+ id: '22222222-2222-4222-8222-222222222222',
28
+ email: 'platform@example.com',
29
+ name: 'Platform Engineer',
30
+ emailVerifiedAt: '2026-01-01T00:00:00.000Z',
31
+ },
32
+ workspaces: [{id: WORKSPACE_ID, name: 'Acme', slug: 'acme', membershipId: 'membership-1'}],
33
+ };
34
+
35
+ function ProjectsHubPageStory({scenario}: {scenario: Scenario}) {
36
+ configureApiClient({
37
+ baseUrl: 'https://api.example.test',
38
+ fetchImpl: fetchForScenario(scenario),
39
+ });
40
+
41
+ const queryClient = useMemo(
42
+ () => new QueryClient({defaultOptions: {queries: {retry: false}}}),
43
+ [],
44
+ );
45
+ const store = useMemo(() => {
46
+ const nextStore = createStore();
47
+ nextStore.set(authStateAtom, authState);
48
+ return nextStore;
49
+ }, []);
50
+ const router = useMemo(() => createStoryRouter(), []);
51
+
52
+ return (
53
+ <QueryClientProvider client={queryClient}>
54
+ <JotaiProvider store={store}>
55
+ <div className="min-h-screen bg-background-subtle-base px-24 py-32">
56
+ <div className="mx-auto w-full max-w-[1120px]">
57
+ <RouterProvider router={router} />
58
+ </div>
59
+ </div>
60
+ <Toaster />
61
+ </JotaiProvider>
62
+ </QueryClientProvider>
63
+ );
64
+ }
65
+
66
+ const meta = {
67
+ title: 'Projects/ProjectsHubPage',
68
+ component: ProjectsHubPageStory,
69
+ parameters: {layout: 'fullscreen'},
70
+ args: {scenario: 'list'},
71
+ } satisfies Meta<typeof ProjectsHubPageStory>;
72
+
73
+ export default meta;
74
+ type Story = StoryObj<typeof meta>;
75
+
76
+ export const Playground: Story = {};
77
+
78
+ export const Empty: Story = {
79
+ args: {scenario: 'empty'},
80
+ };
81
+
82
+ export const Loading: Story = {
83
+ args: {scenario: 'loading'},
84
+ };
85
+
86
+ export const ErrorState: Story = {
87
+ args: {scenario: 'error'},
88
+ };
89
+
90
+ function createStoryRouter() {
91
+ const rootRoute = createRootRoute({component: Outlet});
92
+ const workspaceRoute = createRoute({
93
+ getParentRoute: () => rootRoute,
94
+ path: '/w/$workspaceSlug',
95
+ component: ProjectsHubPage,
96
+ });
97
+
98
+ return createRouter({
99
+ history: createMemoryHistory({initialEntries: ['/w/acme']}),
100
+ routeTree: rootRoute.addChildren([workspaceRoute]),
101
+ });
102
+ }
103
+
104
+ function fetchForScenario(scenario: Scenario): typeof fetch {
105
+ return (input) => {
106
+ const url = requestUrl(input);
107
+
108
+ if (url.pathname.endsWith('/agent/model-providers')) {
109
+ return Promise.resolve(
110
+ jsonResponse({
111
+ configs: [
112
+ {
113
+ kind: 'builtin',
114
+ provider_id: 'anthropic',
115
+ default_model: null,
116
+ created_at: '2026-01-01T00:00:00.000Z',
117
+ updated_at: '2026-01-01T00:00:00.000Z',
118
+ },
119
+ ],
120
+ default_provider_id: 'anthropic',
121
+ default_harness_id: null,
122
+ }),
123
+ );
124
+ }
125
+
126
+ if (url.pathname === '/integration-connections') {
127
+ return Promise.resolve(
128
+ jsonResponse({
129
+ connections: [
130
+ {
131
+ id: CONNECTION_ID,
132
+ workspace_id: WORKSPACE_ID,
133
+ provider: 'github',
134
+ external_account_id: 'octo',
135
+ slug: 'github_octo',
136
+ display_name: 'Acme GitHub',
137
+ lifecycle_status: 'active',
138
+ capabilities: ['source_control'],
139
+ created_at: '2026-01-01T00:00:00.000Z',
140
+ updated_at: '2026-01-01T00:00:00.000Z',
141
+ },
142
+ ],
143
+ }),
144
+ );
145
+ }
146
+
147
+ if (url.pathname === '/projects') {
148
+ if (scenario === 'loading') {
149
+ return new Promise<Response>(() => {
150
+ // Keep the request pending to show the initial skeleton.
151
+ });
152
+ }
153
+ if (scenario === 'error') {
154
+ return Promise.resolve(jsonResponse({code: 'server-error'}, {status: 500}));
155
+ }
156
+ if (scenario === 'empty') {
157
+ return Promise.resolve(jsonResponse({projects: [], next_cursor: null}));
158
+ }
159
+
160
+ return Promise.resolve(
161
+ jsonResponse({
162
+ projects: [
163
+ projectForStory('Platform', '11111111-1111-4111-8111-111111111112'),
164
+ projectForStory('Agent', '11111111-1111-4111-8111-111111111113'),
165
+ projectForStory('Workflows', '11111111-1111-4111-8111-111111111114'),
166
+ ],
167
+ next_cursor: null,
168
+ }),
169
+ );
170
+ }
171
+
172
+ return Promise.resolve(jsonResponse({}, {status: 404}));
173
+ };
174
+ }
175
+
176
+ function projectForStory(name: string, id: string) {
177
+ return {
178
+ id,
179
+ workspace_id: WORKSPACE_ID,
180
+ name,
181
+ slug: name.toLowerCase(),
182
+ source: {
183
+ connection_id: CONNECTION_ID,
184
+ external_repository_id: `github:octo/${name.toLowerCase()}`,
185
+ },
186
+ created_at: '2026-01-01T00:00:00.000Z',
187
+ updated_at: '2026-01-01T00:00:00.000Z',
188
+ };
189
+ }
190
+
191
+ function requestUrl(input: RequestInfo | URL): URL {
192
+ if (input instanceof Request) return new URL(input.url);
193
+ if (input instanceof URL) return input;
194
+ return new URL(input, 'https://api.example.test');
195
+ }
196
+
197
+ function jsonResponse(body: unknown, init: ResponseInit = {}) {
198
+ return new Response(JSON.stringify(body), {
199
+ status: 200,
200
+ headers: {'content-type': 'application/json'},
201
+ ...init,
202
+ });
203
+ }
@@ -1,5 +1,5 @@
1
1
  import {configureApiClient} from '@shipfox/client-api';
2
- import {fireEvent, screen, waitFor} from '@testing-library/react';
2
+ import {fireEvent, screen, waitFor, within} from '@testing-library/react';
3
3
  import {
4
4
  jsonResponse,
5
5
  PROJECT_TEST_WID,
@@ -17,7 +17,7 @@ describe('ProjectsHubPage', () => {
17
17
  window.sessionStorage.clear();
18
18
  });
19
19
 
20
- test('renders Projects H2 + New project button + empty state with create CTA', async () => {
20
+ test('renders the Projects panel controls and empty state with create CTA', async () => {
21
21
  configureApiClient({
22
22
  fetchImpl: createHubFetch({
23
23
  projects: jsonResponse({projects: [], next_cursor: null}),
@@ -28,10 +28,17 @@ describe('ProjectsHubPage', () => {
28
28
  renderProjectPage(`/w/${PROJECT_TEST_WSLUG}`, <ProjectsHubPage />);
29
29
 
30
30
  expect(await screen.findByText('Create your first project')).toBeInTheDocument();
31
- // The page-level "Projects" title belongs in content because the top nav owns
32
- // workspace identity.
33
- expect(screen.getByRole('heading', {name: 'Projects'})).toBeInTheDocument();
34
- expect(screen.getByRole('link', {name: NEW_PROJECT_REGEX})).toHaveAttribute(
31
+ const projectsRegion = screen.getByRole('region', {name: 'Projects'});
32
+ expect(projectsRegion.querySelectorAll('[data-slot="panel"]')).toHaveLength(1);
33
+ const panelHeader = projectsRegion.querySelector<HTMLElement>('[data-slot="panel-header"]');
34
+ if (!panelHeader) throw new Error('Projects panel header was not rendered');
35
+ expect(panelHeader).toBeInTheDocument();
36
+ expect(projectsRegion.querySelector('[data-slot="panel-body"]')).toBeInTheDocument();
37
+ expect(screen.queryByRole('heading', {name: 'Projects'})).not.toBeInTheDocument();
38
+ expect(
39
+ within(panelHeader).getByRole('searchbox', {name: 'Search projects'}),
40
+ ).toBeInTheDocument();
41
+ expect(within(panelHeader).getByRole('link', {name: NEW_PROJECT_REGEX})).toHaveAttribute(
35
42
  'href',
36
43
  WORKSPACE_PROJECTS_NEW_HREF,
37
44
  );
@@ -111,17 +118,90 @@ describe('ProjectsHubPage', () => {
111
118
 
112
119
  renderProjectPage(`/w/${PROJECT_TEST_WSLUG}`, <ProjectsHubPage />);
113
120
  expect(await screen.findByText('Platform')).toBeInTheDocument();
121
+ const projectsList = screen.getByRole('list', {name: 'Projects list'});
122
+ expect(projectsList).toHaveClass('grid-cols-2', 'max-[760px]:grid-cols-1');
123
+ expect(projectsList).not.toHaveClass('gap-px');
124
+ expect(projectsList).not.toHaveClass('bg-border-neutral-base');
125
+ expect(projectsList.querySelectorAll('[data-slot="panel-cell"]')).toHaveLength(1);
114
126
  const projectLink = screen.getByText('Platform').closest('a');
115
- // The whole card is the link, carrying the neutral focus ring (matching the
116
- // integration gallery cards).
117
- expect(projectLink).toHaveClass('focus-visible:shadow-button-neutral-focus');
118
- expect(projectLink?.className).not.toContain('shadow-button-secondary');
127
+ // The whole cell is the link, carrying an inset neutral focus ring.
128
+ expect(projectLink).toHaveClass('focus-visible:shadow-focus-inset');
129
+ expect(projectLink).toHaveClass('hover:bg-background-neutral-hover');
119
130
 
120
131
  fireEvent.click(screen.getByRole('button', {name: 'Load more'}));
121
132
 
122
133
  expect(await screen.findByText('API')).toBeInTheDocument();
123
134
  });
124
135
 
136
+ test('renders an odd project count without an empty border-colored cell', async () => {
137
+ configureApiClient({
138
+ fetchImpl: createHubFetch({
139
+ projects: jsonResponse({
140
+ projects: [
141
+ projectDto({id: '11111111-1111-4111-8111-111111111112', name: 'Platform'}),
142
+ projectDto({id: '11111111-1111-4111-8111-111111111113', name: 'Agent'}),
143
+ projectDto({id: '11111111-1111-4111-8111-111111111114', name: 'Workflows'}),
144
+ ],
145
+ next_cursor: null,
146
+ }),
147
+ }),
148
+ });
149
+
150
+ renderProjectPage(`/w/${PROJECT_TEST_WSLUG}`, <ProjectsHubPage />);
151
+
152
+ const projectsList = await screen.findByRole('list', {name: 'Projects list'});
153
+ expect(projectsList.querySelectorAll('[data-slot="panel-cell"]')).toHaveLength(3);
154
+ expect(projectsList).not.toHaveClass('gap-px', 'bg-border-neutral-base');
155
+ // The odd count is padded so the last row's divider still spans the panel,
156
+ // and the pad carries the panel fill rather than the border colour.
157
+ const filler = projectsList.querySelector('[data-slot="panel-cell-filler"]');
158
+ expect(filler).not.toBeNull();
159
+ expect(filler).toHaveClass('bg-background-neutral-base');
160
+ });
161
+
162
+ test('keeps existing projects and the load-more retry after a cursor failure', async () => {
163
+ let cursorRequests = 0;
164
+ const fetchImpl = vi.fn((input: RequestInfo | URL) => {
165
+ const url = new URL(requestInputUrl(input));
166
+ if (url.pathname.endsWith('/agent/model-providers')) {
167
+ return Promise.resolve(jsonResponse(modelProviderConfigsDto()));
168
+ }
169
+ if (url.pathname === '/integration-connections') {
170
+ return Promise.resolve(jsonResponse(connectionsDto()));
171
+ }
172
+ if (url.pathname === '/projects') {
173
+ if (url.searchParams.get('cursor') === 'cursor-1') {
174
+ cursorRequests += 1;
175
+ return Promise.resolve(jsonResponse({code: 'server-error'}, {status: 500}));
176
+ }
177
+ return Promise.resolve(
178
+ jsonResponse({
179
+ projects: [projectDto({id: '11111111-1111-4111-8111-111111111112', name: 'Platform'})],
180
+ next_cursor: 'cursor-1',
181
+ }),
182
+ );
183
+ }
184
+ return Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}));
185
+ });
186
+ configureApiClient({fetchImpl});
187
+
188
+ renderProjectPage(`/w/${PROJECT_TEST_WSLUG}`, <ProjectsHubPage />);
189
+ expect(await screen.findByText('Platform')).toBeInTheDocument();
190
+
191
+ fireEvent.click(screen.getByRole('button', {name: 'Load more'}));
192
+
193
+ expect(await screen.findByRole('alert')).toHaveTextContent(
194
+ 'Could not load the next page. Existing projects are still shown.',
195
+ );
196
+ expect(screen.getByText('Platform')).toBeInTheDocument();
197
+ expect(screen.getByRole('button', {name: 'Load more'})).toBeInTheDocument();
198
+ expect(cursorRequests).toBe(1);
199
+
200
+ fireEvent.click(screen.getByRole('button', {name: 'Load more'}));
201
+
202
+ await waitFor(() => expect(cursorRequests).toBe(2));
203
+ });
204
+
125
205
  test('renders an error alert with retry', async () => {
126
206
  configureApiClient({
127
207
  fetchImpl: createHubFetch({