go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/build/pages/builder_page/builder_page.test.tsx (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import { GrpcError, RpcCode } from '@chopsui/prpc-client'; 16 import { act, render, screen } from '@testing-library/react'; 17 18 import { CacheOption } from '@/generic_libs/tools/cached_fn'; 19 import { BuilderItem } from '@/proto/go.chromium.org/luci/buildbucket/proto/builder_common.pb'; 20 import { 21 BuildersClientImpl, 22 GetBuilderRequest, 23 } from '@/proto/go.chromium.org/luci/buildbucket/proto/builder_service.pb'; 24 import { FakeContextProvider } from '@/testing_tools/fakes/fake_context_provider'; 25 26 import { BuilderPage } from './builder_page'; 27 import { EndedBuildsSection } from './ended_builds_section'; 28 import { MachinePoolSection } from './machine_pool_section'; 29 30 jest.mock('./machine_pool_section', () => 31 createSelectiveMockFromModule<typeof import('./machine_pool_section')>( 32 './machine_pool_section', 33 ['MachinePoolSection'], 34 ), 35 ); 36 37 jest.mock('./ended_builds_section', () => 38 createSelectiveMockFromModule<typeof import('./ended_builds_section')>( 39 './ended_builds_section', 40 ['EndedBuildsSection'], 41 ), 42 ); 43 44 jest.mock('./started_builds_section', () => 45 createSelectiveMockFromModule<typeof import('./started_builds_section')>( 46 './started_builds_section', 47 ['StartedBuildsSection'], 48 ), 49 ); 50 51 jest.mock('./pending_builds_section', () => 52 createSelectiveMockFromModule<typeof import('./pending_builds_section')>( 53 './pending_builds_section', 54 ['PendingBuildsSection'], 55 ), 56 ); 57 58 jest.mock('./views_section', () => 59 createSelectiveMockFromModule<typeof import('./views_section')>( 60 './views_section', 61 ['ViewsSection'], 62 ), 63 ); 64 65 describe('BuilderPage', () => { 66 let getBuilderMock: jest.SpyInstance< 67 Promise<BuilderItem>, 68 [req: GetBuilderRequest, cacheOpt?: CacheOption | undefined] 69 >; 70 let mockedEndedBuildsSection: jest.MockedFunctionDeep< 71 typeof EndedBuildsSection 72 >; 73 let mockedMachinePoolSection: jest.MockedFunctionDeep< 74 typeof MachinePoolSection 75 >; 76 77 beforeEach(() => { 78 jest.useFakeTimers(); 79 getBuilderMock = jest.spyOn(BuildersClientImpl.prototype, 'GetBuilder'); 80 mockedEndedBuildsSection = jest 81 .mocked(EndedBuildsSection) 82 .mockReturnValue(<>mocked ended builds section</>); 83 mockedMachinePoolSection = jest.mocked(MachinePoolSection); 84 }); 85 86 afterEach(() => { 87 jest.useRealTimers(); 88 getBuilderMock.mockReset(); 89 mockedEndedBuildsSection.mockReset(); 90 mockedMachinePoolSection.mockReset(); 91 }); 92 93 test('should render correctly', async () => { 94 getBuilderMock.mockResolvedValue( 95 BuilderItem.fromPartial({ 96 id: { project: 'project', bucket: 'bucket', builder: 'builder' }, 97 config: { 98 swarmingHost: 'https://swarming.host', 99 descriptionHtml: 'some builder description', 100 dimensions: ['10:key1:val1', '12:key2:val2'] as readonly string[], 101 }, 102 }), 103 ); 104 render( 105 <FakeContextProvider 106 mountedPath="/p/:project/builder/:bucket/:builder" 107 routerOptions={{ 108 initialEntries: ['/p/project/builder/bucket/builder'], 109 }} 110 > 111 <BuilderPage /> 112 </FakeContextProvider>, 113 ); 114 115 await act(() => jest.runAllTimersAsync()); 116 117 expect( 118 screen.queryByText( 119 'Failed to query the builder. If you can see recent builds, the builder might have been deleted recently.', 120 ), 121 ).not.toBeInTheDocument(); 122 expect(screen.getByText('some builder description')).toBeInTheDocument(); 123 expect(mockedEndedBuildsSection).toHaveBeenCalledWith( 124 { 125 builderId: { project: 'project', bucket: 'bucket', builder: 'builder' }, 126 }, 127 expect.anything(), 128 ); 129 expect(mockedMachinePoolSection).toHaveBeenCalledWith( 130 { 131 swarmingHost: 'https://swarming.host', 132 dimensions: [ 133 { key: 'key1', value: 'val1' }, 134 { key: 'key2', value: 'val2' }, 135 ], 136 }, 137 expect.anything(), 138 ); 139 }); 140 141 test('failing to query builder should not break the builder page', async () => { 142 getBuilderMock.mockRejectedValue( 143 new GrpcError(RpcCode.NOT_FOUND, 'builder was removed'), 144 ); 145 render( 146 <FakeContextProvider 147 mountedPath="/p/:project/builder/:bucket/:builder" 148 routerOptions={{ 149 initialEntries: ['/p/project/builder/bucket/builder'], 150 }} 151 > 152 <BuilderPage /> 153 </FakeContextProvider>, 154 ); 155 156 await act(() => jest.runAllTimersAsync()); 157 158 expect( 159 screen.getByText( 160 'Failed to query the builder. If you can see recent builds, the builder might have been deleted recently.', 161 ), 162 ).toBeInTheDocument(); 163 // The ended builds section should still be displayed. 164 expect(screen.getByText('mocked ended builds section')).toBeInTheDocument(); 165 expect(mockedEndedBuildsSection).toHaveBeenCalledWith( 166 { 167 builderId: { project: 'project', bucket: 'bucket', builder: 'builder' }, 168 }, 169 expect.anything(), 170 ); 171 expect(mockedMachinePoolSection).not.toHaveBeenCalled(); 172 }); 173 });