backstage

Форк
0
296 строк · 8.6 Кб
1
/*
2
 * Copyright 2022 The Backstage Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
import React, { ReactElement } from 'react';
18

19
// Shadow DOM support for the simple and complete DOM testing utilities
20
// https://github.com/testing-library/dom-testing-library/issues/742#issuecomment-674987855
21
import { screen } from 'testing-library__dom';
22
import { Route } from 'react-router-dom';
23
import { act, render } from '@testing-library/react';
24

25
import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils';
26
import { FlatRoutes } from '@backstage/core-app-api';
27
import { ApiRef } from '@backstage/core-plugin-api';
28

29
import {
30
  TechDocsAddons,
31
  techdocsApiRef,
32
  TechDocsEntityMetadata,
33
  TechDocsMetadata,
34
  techdocsStorageApiRef,
35
} from '@backstage/plugin-techdocs-react';
36
import { TechDocsReaderPage, techdocsPlugin } from '@backstage/plugin-techdocs';
37
import { entityRouteRef } from '@backstage/plugin-catalog-react';
38
import { searchApiRef } from '@backstage/plugin-search-react';
39
import { scmIntegrationsApiRef } from '@backstage/integration-react';
40

41
// Since React 18 react-dom/server eagerly uses TextEncoder, so lazy load and make it available globally first
42
if (!global.TextEncoder) {
43
  global.TextEncoder = require('util').TextEncoder;
44
}
45
const { renderToStaticMarkup } =
46
  require('react-dom/server') as typeof import('react-dom/server');
47

48
const techdocsApi = {
49
  getTechDocsMetadata: jest.fn(),
50
  getEntityMetadata: jest.fn(),
51
};
52

53
const techdocsStorageApi = {
54
  getApiOrigin: jest.fn(),
55
  getBaseUrl: jest.fn(),
56
  getEntityDocs: jest.fn(),
57
  syncEntityDocs: jest.fn(),
58
};
59

60
const searchApi = {
61
  query: jest.fn().mockResolvedValue({ results: [] }),
62
};
63

64
const scmIntegrationsApi = {
65
  fromConfig: jest.fn().mockReturnValue({}),
66
};
67

68
/** @ignore */
69
type TechDocsAddonTesterTestApiPair<TApi> = TApi extends infer TImpl
70
  ? readonly [ApiRef<TApi>, Partial<TImpl>]
71
  : never;
72

73
/** @ignore */
74
type TechdocsAddonTesterApis<TApiPairs> = {
75
  [TIndex in keyof TApiPairs]: TechDocsAddonTesterTestApiPair<
76
    TApiPairs[TIndex]
77
  >;
78
};
79

80
type TechDocsAddonTesterOptions = {
81
  dom: ReactElement;
82
  entity: Partial<TechDocsEntityMetadata>;
83
  metadata: Partial<TechDocsMetadata>;
84
  componentId: string;
85
  apis: TechdocsAddonTesterApis<any[]>;
86
  path: string;
87
};
88

89
const defaultOptions: TechDocsAddonTesterOptions = {
90
  dom: <></>,
91
  entity: { metadata: { name: '' } },
92
  metadata: {},
93
  componentId: 'docs',
94
  apis: [],
95
  path: '',
96
};
97

98
const defaultMetadata = {
99
  site_name: 'Tech Docs',
100
  site_description: 'Tech Docs',
101
};
102

103
const defaultEntity = {
104
  kind: 'Component',
105
  metadata: { namespace: 'default', name: 'docs' },
106
};
107

108
const defaultDom = (
109
  <html lang="en">
110
    <head />
111
    <body>
112
      <div data-md-component="container">
113
        <div data-md-component="navigation" />
114
        <div data-md-component="toc" />
115
        <div data-md-component="main" />
116
      </div>
117
    </body>
118
  </html>
119
);
120

121
/**
122
 * Utility class for rendering TechDocs Addons end-to-end within the TechDocs
123
 * reader page, with a set of givens (e.g. page DOM, metadata, etc).
124
 *
125
 * @example
126
 * ```tsx
127
 * const { getByText } = await TechDocsAddonTester.buildAddonsInTechDocs([<AnAddon />])
128
 *   .withDom(<body>TEST_CONTENT</body>)
129
 *   .renderWithEffects();
130
 *
131
 * expect(getByText('TEST_CONTENT')).toBeInTheDocument();
132
 * ```
133
 *
134
 * @public
135
 */
136
export class TechDocsAddonTester {
137
  private options: TechDocsAddonTesterOptions = defaultOptions;
138
  private addons: ReactElement[];
139

140
  /**
141
   * Get a TechDocsAddonTester instance for a given set of Addons.
142
   */
143
  static buildAddonsInTechDocs(addons: ReactElement[]) {
144
    return new TechDocsAddonTester(addons);
145
  }
146

147
  // Protected in order to allow extension but not direct instantiation.
148
  protected constructor(addons: ReactElement[]) {
149
    this.addons = addons;
150
  }
151

152
  /**
153
   * Provide mock API implementations if your Addon expects any.
154
   */
155
  withApis<T extends any[]>(apis: TechdocsAddonTesterApis<T>) {
156
    const refs = apis.map(([ref]) => ref);
157
    this.options.apis = this.options.apis
158
      .filter(([ref]) => !refs.includes(ref))
159
      .concat(apis);
160
    return this;
161
  }
162

163
  /**
164
   * Provide mock HTML if your Addon expects it in the shadow DOM.
165
   */
166
  withDom(dom: ReactElement) {
167
    this.options.dom = dom;
168
    return this;
169
  }
170

171
  /**
172
   * Provide mock techdocs_metadata.json values if your Addon needs it.
173
   */
174
  withMetadata(metadata: Partial<TechDocsMetadata>) {
175
    this.options.metadata = metadata;
176
    return this;
177
  }
178

179
  /**
180
   * Provide a mock entity if your Addon needs it. This also controls the base
181
   * path at which the Addon is rendered.
182
   */
183
  withEntity(entity: Partial<TechDocsEntityMetadata>) {
184
    this.options.entity = entity;
185
    return this;
186
  }
187

188
  /**
189
   * Provide the TechDocs page path at which the Addon is rendered (e.g. the
190
   * part of the path after the entity namespace/kind/name).
191
   */
192
  atPath(path: string) {
193
    this.options.path = path;
194
    return this;
195
  }
196

197
  /**
198
   * Return a fully configured and mocked TechDocs reader page within a test
199
   * App instance, using the given Addon(s).
200
   */
201
  build() {
202
    const apis: TechdocsAddonTesterApis<any[]> = [
203
      [techdocsApiRef, techdocsApi],
204
      [techdocsStorageApiRef, techdocsStorageApi],
205
      [searchApiRef, searchApi],
206
      [scmIntegrationsApiRef, scmIntegrationsApi],
207
      ...this.options.apis,
208
    ];
209

210
    const entityName = {
211
      namespace:
212
        this.options.entity?.metadata?.namespace ||
213
        defaultEntity.metadata.namespace,
214
      kind: this.options.entity?.kind || defaultEntity.kind,
215
      name: this.options.entity?.metadata?.name || defaultEntity.metadata.name,
216
    };
217

218
    techdocsApi.getTechDocsMetadata.mockReturnValue(
219
      this.options.metadata || { ...defaultMetadata },
220
    );
221
    techdocsApi.getEntityMetadata.mockResolvedValue(
222
      this.options.entity || { ...defaultEntity },
223
    );
224

225
    techdocsStorageApi.syncEntityDocs.mockResolvedValue('cached');
226
    techdocsStorageApi.getApiOrigin.mockResolvedValue(
227
      'https://backstage.example.com/api/techdocs',
228
    );
229
    techdocsStorageApi.getBaseUrl.mockResolvedValue(
230
      `https://backstage.example.com/api/techdocs/${entityName.namespace}/${entityName.kind}/${entityName.name}/${this.options.path}`,
231
    );
232
    techdocsStorageApi.getEntityDocs.mockResolvedValue(
233
      renderToStaticMarkup(this.options.dom || defaultDom),
234
    );
235

236
    const TechDocsAddonsPage = () => {
237
      return (
238
        <TestApiProvider apis={apis}>
239
          <FlatRoutes>
240
            <Route
241
              path="/docs/:namespace/:kind/:name/*"
242
              element={<TechDocsReaderPage />}
243
            >
244
              <TechDocsAddons>
245
                {this.addons.map((addon, index) =>
246
                  React.cloneElement(addon, { key: index }),
247
                )}
248
              </TechDocsAddons>
249
            </Route>
250
          </FlatRoutes>
251
        </TestApiProvider>
252
      );
253
    };
254

255
    return wrapInTestApp(<TechDocsAddonsPage />, {
256
      routeEntries: [
257
        `/docs/${entityName.namespace}/${entityName.kind}/${entityName.name}/${this.options.path}`,
258
      ],
259
      mountedRoutes: {
260
        '/docs': techdocsPlugin.routes.root,
261
        '/docs/:namespace/:kind/:name/*': techdocsPlugin.routes.docRoot,
262
        '/catalog/:namespace/:kind/:name': entityRouteRef,
263
      },
264
    });
265
  }
266

267
  /**
268
   * Render the Addon within a fully configured and mocked TechDocs reader.
269
   *
270
   * @remarks
271
   * Components using useEffect to perform an asynchronous action (such as
272
   * fetch) must be rendered within an async act call to properly get the final
273
   * state, even with mocked responses. This utility method makes the signature
274
   * a bit cleaner, since act doesn't return the result of the evaluated
275
   * function.
276
   *
277
   * @see https://github.com/testing-library/react-testing-library/issues/281
278
   * @see https://github.com/facebook/react/pull/14853
279
   */
280
  async renderWithEffects(): Promise<
281
    typeof screen & { shadowRoot: ShadowRoot | null }
282
  > {
283
    await act(async () => {
284
      render(this.build());
285
    });
286

287
    const shadowHost = await screen.findByTestId('techdocs-native-shadowroot');
288

289
    return {
290
      ...screen,
291
      shadowRoot: shadowHost?.shadowRoot || null,
292
    };
293
  }
294
}
295

296
export default TechDocsAddonTester.buildAddonsInTechDocs;
297

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.