Merge remote-tracking branch 'origin/master' into cost-insight-trending-line

This commit is contained in:
bogdannechyporenko
2022-11-18 21:17:02 +01:00
133 changed files with 2178 additions and 1668 deletions
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/plugin-catalog-graph': patch
'@backstage/plugin-pagerduty': patch
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-splunk-on-call': patch
'@backstage/plugin-techdocs-react': patch
---
Internal refactor to improve tests
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/catalog-client': minor
---
**BREAKING PRODUCERS**: Added a new `getEntitiesByRefs` endpoint to `CatalogApi`, for efficient batch fetching of entities by ref.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Do not use deprecated `LocationSpec` from the `@backstage/plugin-catalog-node` package
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-catalog-backend': minor
---
Added new `POST /entities/by-refs` endpoint, which allows you to efficiently
batch-fetch entities by their entity ref. This can be useful e.g. in graphql
resolvers or similar contexts where you need to fetch many entities at the same
time.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-node': patch
---
Updated indexer and decorator base classes to take advantage of features introduced in Node.js v16; be sure you are running a [supported version of Node.js](https://backstage.io/docs/releases/v1.8.0#node-16-and-18).
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
fixed `headerOptions` not passed to `TemplatePage` component
+6 -1
View File
@@ -18,7 +18,7 @@ var path = require('path');
module.exports = {
root: true,
plugins: ['notice'],
plugins: ['notice', 'testing-library'],
rules: {
'notice/notice': [
'error',
@@ -45,5 +45,10 @@ module.exports = {
"CallExpression[arguments.length=0] > MemberExpression[property.name='toUpperCase']",
},
],
'testing-library/await-async-query': 'error',
'testing-library/await-async-utils': 'error',
'testing-library/no-await-sync-query': 'error',
'testing-library/prefer-wait-for': 'error',
'testing-library/no-dom-import': 'error',
},
};
+1
View File
@@ -241,6 +241,7 @@ pageview
parallelization
parseable
Patrik
pattison
Peloton
performant
Performant
+1 -1
View File
@@ -68,7 +68,7 @@ jobs:
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
- name: verify yarn dependency duplicates
run: yarn dedupe --check
run: node scripts/verify-lockfile-duplicates.js
- name: verify changesets
run: node scripts/verify-changesets.js
+3
View File
@@ -32,6 +32,9 @@ jobs:
run: yarn install --immutable
working-directory: microsite
- name: verify yarn dependency duplicates
run: node scripts/verify-lockfile-duplicates.js
- name: prettier
run: yarn prettier:check
working-directory: microsite
+1 -1
View File
@@ -6,7 +6,7 @@ category: Deployment # A single category e.g. CI, Machine Learning, Services, Mo
description: |
Show workloads, environments and resources deployed by Humanitec Platform Orchestrator.
Plugin includes an Entity ComponentCard, Backend API route and scaffolder actions.
documentation: https://github.com/thefrontside/backstage/tree/main/plugins/humanitec
documentation: https://github.com/thefrontside/playhouse/tree/main/plugins/humanitec
iconUrl: img/humanitec-logo.png
npmPackageName: '@frontside/backstage-plugin-humanitec'
addedDate: '2022-06-22'
+12 -87
View File
@@ -281,14 +281,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-validator-identifier@npm:^7.10.4, @babel/helper-validator-identifier@npm:^7.12.11":
version: 7.12.11
resolution: "@babel/helper-validator-identifier@npm:7.12.11"
checksum: e604c6bf890704fc46c1ae13bf23afb242b810224ec3403bba67cdbf0d8dabfec4b82123d6dfb18135a0ee3f7f79218583c819363ebb5e04a0a49d8418db7fce
languageName: node
linkType: hard
"@babel/helper-validator-identifier@npm:^7.18.6":
"@babel/helper-validator-identifier@npm:^7.10.4, @babel/helper-validator-identifier@npm:^7.18.6":
version: 7.18.6
resolution: "@babel/helper-validator-identifier@npm:7.18.6"
checksum: e295254d616bbe26e48c196a198476ab4d42a73b90478c9842536cf910ead887f5af6b5c4df544d3052a25ccb3614866fa808dc1e3a5a4291acd444e243c0648
@@ -1241,18 +1234,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/types@npm:^7.10.4, @babel/types@npm:^7.10.5, @babel/types@npm:^7.12.1, @babel/types@npm:^7.12.10, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.12, @babel/types@npm:^7.12.5, @babel/types@npm:^7.12.6, @babel/types@npm:^7.12.7, @babel/types@npm:^7.4.4":
version: 7.12.12
resolution: "@babel/types@npm:7.12.12"
dependencies:
"@babel/helper-validator-identifier": ^7.12.11
lodash: ^4.17.19
to-fast-properties: ^2.0.0
checksum: 1eefe9468573a32491030f2986f820eb4289ff0b93e437b97f665e73cc8dae0ec0bdd95f37995bb3b70b5ce2a9a45a302affbbad497150021cf874c5ef10e361
languageName: node
linkType: hard
"@babel/types@npm:^7.8.3":
"@babel/types@npm:^7.10.4, @babel/types@npm:^7.10.5, @babel/types@npm:^7.12.1, @babel/types@npm:^7.12.10, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.12, @babel/types@npm:^7.12.5, @babel/types@npm:^7.12.6, @babel/types@npm:^7.12.7, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
version: 7.18.10
resolution: "@babel/types@npm:7.18.10"
dependencies:
@@ -2917,7 +2899,7 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:4, debug@npm:^4.3.3":
"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.3":
version: 4.3.4
resolution: "debug@npm:4.3.4"
dependencies:
@@ -2929,7 +2911,7 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:4.3.1, debug@npm:^4.1.0":
"debug@npm:4.3.1":
version: 4.3.1
resolution: "debug@npm:4.3.1"
dependencies:
@@ -4275,21 +4257,7 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:^7.0.0, glob@npm:^7.0.5, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.6":
version: 7.2.0
resolution: "glob@npm:7.2.0"
dependencies:
fs.realpath: ^1.0.0
inflight: ^1.0.4
inherits: 2
minimatch: ^3.0.4
once: ^1.3.0
path-is-absolute: ^1.0.0
checksum: 78a8ea942331f08ed2e055cb5b9e40fe6f46f579d7fd3d694f3412fe5db23223d29b7fee1575440202e9a7ff9a72ab106a39fee39934c7bedafe5e5f8ae20134
languageName: node
linkType: hard
"glob@npm:^7.1.4":
"glob@npm:^7.0.0, glob@npm:^7.0.5, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6":
version: 7.2.3
resolution: "glob@npm:7.2.3"
dependencies:
@@ -4430,14 +4398,7 @@ __metadata:
languageName: node
linkType: hard
"graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0":
version: 4.2.4
resolution: "graceful-fs@npm:4.2.4"
checksum: 9d58c444eb4f391ce30b451aae8a8af2bd675d9f6f624719e97306f571ab89b2bd2b5f9025199bc63a2edfe2e53e7701554012f32a708148d53aa689163728cc
languageName: node
linkType: hard
"graceful-fs@npm:^4.2.6":
"graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6":
version: 4.2.10
resolution: "graceful-fs@npm:4.2.10"
checksum: 3f109d70ae123951905d85032ebeae3c2a5a7a997430df00ea30df0e3a6c60cf6689b109654d6fdacd28810a053348c4d14642da1d075049e6be1ba5216218da
@@ -5056,15 +5017,6 @@ __metadata:
languageName: node
linkType: hard
"is-core-module@npm:^2.1.0":
version: 2.2.0
resolution: "is-core-module@npm:2.2.0"
dependencies:
has: ^1.0.3
checksum: 61e2aff4a7db4f8f7d5a97b484808af17290f4197b34a797cd3d3d27b6b448951064f8d3d6ceae4394fa9b7e6cf08aacd2ba7a17ef6352e922fe803580fbde56
languageName: node
linkType: hard
"is-core-module@npm:^2.8.0":
version: 2.8.1
resolution: "is-core-module@npm:2.8.1"
@@ -6196,7 +6148,7 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:3.0.4, minimatch@npm:^3.0.4, minimatch@npm:~3.0.2":
"minimatch@npm:3.0.4, minimatch@npm:~3.0.2":
version: 3.0.4
resolution: "minimatch@npm:3.0.4"
dependencies:
@@ -6205,7 +6157,7 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:^3.1.1":
"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1":
version: 3.1.2
resolution: "minimatch@npm:3.1.2"
dependencies:
@@ -6941,7 +6893,7 @@ __metadata:
languageName: node
linkType: hard
"path-parse@npm:^1.0.6, path-parse@npm:^1.0.7":
"path-parse@npm:^1.0.7":
version: 1.0.7
resolution: "path-parse@npm:1.0.7"
checksum: 49abf3d81115642938a8700ec580da6e830dde670be21893c62f4e10bd7dd4c3742ddc603fe24f898cba7eb0c6bc1777f8d9ac14185d34540c6d4d80cd9cae8a
@@ -7967,7 +7919,7 @@ __metadata:
languageName: node
linkType: hard
"resolve@npm:^1.1.6":
"resolve@npm:^1.1.6, resolve@npm:^1.10.0":
version: 1.21.0
resolution: "resolve@npm:1.21.0"
dependencies:
@@ -7980,17 +7932,7 @@ __metadata:
languageName: node
linkType: hard
"resolve@npm:^1.10.0":
version: 1.19.0
resolution: "resolve@npm:1.19.0"
dependencies:
is-core-module: ^2.1.0
path-parse: ^1.0.6
checksum: a05b356e47b85ad3613d9e2a39a824f3c27f4fcad9c9ff6c7cc71a2e314c5904a90ab37481ad0069d03cab9eaaac6eb68aca1bc3355fdb05f1045cd50e2aacea
languageName: node
linkType: hard
"resolve@patch:resolve@^1.1.6#~builtin<compat/resolve>":
"resolve@patch:resolve@^1.1.6#~builtin<compat/resolve>, resolve@patch:resolve@^1.10.0#~builtin<compat/resolve>":
version: 1.21.0
resolution: "resolve@patch:resolve@npm%3A1.21.0#~builtin<compat/resolve>::version=1.21.0&hash=07638b"
dependencies:
@@ -8003,16 +7945,6 @@ __metadata:
languageName: node
linkType: hard
"resolve@patch:resolve@^1.10.0#~builtin<compat/resolve>":
version: 1.19.0
resolution: "resolve@patch:resolve@npm%3A1.19.0#~builtin<compat/resolve>::version=1.19.0&hash=07638b"
dependencies:
is-core-module: ^2.1.0
path-parse: ^1.0.6
checksum: 2443b94d347e6946c87c85faf13071f605e609e0b54784829b0ed2b917d050bfc1cbaf4ecc6453f224cfa7d0c5dcd97cbb273454cd210bee68e4af15c1a5abc9
languageName: node
linkType: hard
"responselike@npm:1.0.2":
version: 1.0.2
resolution: "responselike@npm:1.0.2"
@@ -8313,14 +8245,7 @@ __metadata:
languageName: node
linkType: hard
"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2":
version: 3.0.3
resolution: "signal-exit@npm:3.0.3"
checksum: f0169d3f1263d06df32ca072b0bf33b34c6f8f0341a7a1621558a2444dfbe8f5fec76b35537fcc6f0bc4944bdb5336fe0bdcf41a5422c4e45a1dba3f45475e6c
languageName: node
linkType: hard
"signal-exit@npm:^3.0.7":
"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.7":
version: 3.0.7
resolution: "signal-exit@npm:3.0.7"
checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318
+9 -1
View File
@@ -33,7 +33,8 @@
"build-storybook": "yarn ./storybook run build-storybook",
"techdocs-cli": "node scripts/techdocs-cli.js",
"techdocs-cli:dev": "cross-env TECHDOCS_CLI_DEV_MODE=true node scripts/techdocs-cli.js",
"prepare": "husky install"
"prepare": "husky install",
"postinstall": "husky install"
},
"workspaces": {
"packages": [
@@ -70,6 +71,7 @@
"e2e-test": "workspace:*",
"eslint": "^8.6.0",
"eslint-plugin-notice": "^0.9.10",
"eslint-plugin-testing-library": "^5.9.1",
"fs-extra": "10.1.0",
"husky": "^8.0.0",
"lint-staged": "^13.0.0",
@@ -92,6 +94,12 @@
],
"*.md": [
"node ./scripts/check-docs-quality"
],
"./yarn.lock": [
"node ./scripts/verify-lockfile-duplicates --fix"
],
"*/yarn.lock": [
"node ./scripts/verify-lockfile-duplicates --fix"
]
},
"packageManager": "yarn@3.2.3"
+4
View File
@@ -25,14 +25,17 @@ describe('App', () => {
it('should display support info when clicking the button', () => {
cy.visit('/');
// eslint-disable-next-line testing-library/await-async-query
cy.findByTestId('support-button').click({ force: true });
cy.contains('#backstage');
});
it('should display error message when triggering it', () => {
cy.visit('/');
// eslint-disable-next-line testing-library/await-async-query
cy.findByTestId('error-button').click({ force: true });
cy.contains('Error: Oh no!');
// eslint-disable-next-line testing-library/await-async-query
cy.findByTestId('error-button-close').click({ force: true });
});
@@ -55,6 +58,7 @@ describe('App', () => {
cy.contains('Token')
.get('input[name=github-auth-tf]')
.type('password', { force: true });
// eslint-disable-next-line testing-library/await-async-query
cy.findByTestId('github-auth-button').click({ force: true });
cy.contains(`Welcome, ${name}!`);
cy.contains('Logout').click({ force: true });
+29 -5
View File
@@ -34,6 +34,10 @@ export interface CatalogApi {
request?: GetEntitiesRequest,
options?: CatalogRequestOptions,
): Promise<GetEntitiesResponse>;
getEntitiesByRefs(
request: GetEntitiesByRefsRequest,
options?: CatalogRequestOptions,
): Promise<GetEntitiesByRefsResponse>;
getEntityAncestors(
request: GetEntityAncestorsRequest,
options?: CatalogRequestOptions,
@@ -91,6 +95,10 @@ export class CatalogClient implements CatalogApi {
request?: GetEntitiesRequest,
options?: CatalogRequestOptions,
): Promise<GetEntitiesResponse>;
getEntitiesByRefs(
request: GetEntitiesByRefsRequest,
options?: CatalogRequestOptions,
): Promise<GetEntitiesByRefsResponse>;
getEntityAncestors(
request: GetEntityAncestorsRequest,
options?: CatalogRequestOptions,
@@ -145,14 +153,30 @@ export interface CatalogRequestOptions {
export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE =
'backstage.io/catalog-processing';
// @public
export type EntityFieldsQuery = string[];
// @public
export type EntityFilterQuery =
| Record<string, string | symbol | (string | symbol)[]>[]
| Record<string, string | symbol | (string | symbol)[]>;
// @public
export interface GetEntitiesByRefsRequest {
entityRefs: string[];
fields?: EntityFieldsQuery | undefined;
}
// @public
export interface GetEntitiesByRefsResponse {
items: Array<Entity | undefined>;
}
// @public
export interface GetEntitiesRequest {
after?: string;
fields?: string[] | undefined;
filter?:
| Record<string, string | symbol | (string | symbol)[]>[]
| Record<string, string | symbol | (string | symbol)[]>
| undefined;
fields?: EntityFieldsQuery;
filter?: EntityFilterQuery;
limit?: number;
offset?: number;
}
@@ -195,6 +195,35 @@ describe('CatalogClient', () => {
});
});
describe('getEntitiesByRefs', () => {
it('encodes and decodes the query correctly', async () => {
const entity = {
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'Test2',
namespace: 'test1',
},
};
server.use(
rest.post(`${mockBaseUrl}/entities/by-refs`, async (req, res, ctx) => {
expect(req.url.searchParams.get('fields')).toBe('a,b');
await expect(req.json()).resolves.toEqual({
entityRefs: ['k:n/a', 'k:n/b'],
});
return res(ctx.json({ items: [entity, null] }));
}),
);
const response = await client.getEntitiesByRefs(
{ entityRefs: ['k:n/a', 'k:n/b'], fields: ['a', 'b'] },
{ token },
);
expect(response).toEqual({ items: [entity, null] });
});
});
describe('getEntityByRef', () => {
const existingEntity: Entity = {
apiVersion: 'v1',
@@ -37,6 +37,8 @@ import {
GetEntityFacetsRequest,
GetEntityFacetsResponse,
ValidateEntityResponse,
GetEntitiesByRefsRequest,
GetEntitiesByRefsResponse,
} from './types/api';
import { DiscoveryApi } from './types/discovery';
import { FetchApi } from './types/fetch';
@@ -169,6 +171,40 @@ export class CatalogClient implements CatalogApi {
return { items: entities.sort(refCompare) };
}
/**
* {@inheritdoc CatalogApi.getEntitiesByRefs}
*/
async getEntitiesByRefs(
request: GetEntitiesByRefsRequest,
options?: CatalogRequestOptions,
): Promise<GetEntitiesByRefsResponse> {
const params: string[] = [];
if (request.fields?.length) {
params.push(`fields=${request.fields.map(encodeURIComponent).join(',')}`);
}
const baseUrl = await this.discoveryApi.getBaseUrl('catalog');
const query = params.length ? `?${params.join('&')}` : '';
const url = `${baseUrl}/entities/by-refs${query}`;
const response = await this.fetchApi.fetch(url, {
headers: {
'Content-Type': 'application/json',
...(options?.token && { Authorization: `Bearer ${options?.token}` }),
},
method: 'POST',
body: JSON.stringify({ entityRefs: request.entityRefs }),
});
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
const { items } = await response.json();
return { items };
}
/**
* {@inheritdoc CatalogApi.getEntityByRef}
*/
+130 -58
View File
@@ -29,6 +29,75 @@ export const CATALOG_FILTER_EXISTS = Symbol.for(
'CATALOG_FILTER_EXISTS_0e15b590c0b343a2bae3e787e84c2111',
);
/**
* A key-value based filter expression for entities.
*
* @remarks
*
* Each key of a record is a dot-separated path into the entity structure, e.g.
* `metadata.name`.
*
* The values are literal values to match against. As a value you can also pass
* in the symbol `CATALOG_FILTER_EXISTS` (exported from this package), which
* means that you assert on the existence of that key, no matter what its value
* is.
*
* All matching of keys and values is case insensitive.
*
* If multiple filter sets are given as an array, then there is effectively an
* OR between each filter set.
*
* Within one filter set, there is effectively an AND between the various keys.
*
* Within one key, if there are more than one value, then there is effectively
* an OR between them.
*
* Example: For an input of
*
* ```
* [
* { kind: ['API', 'Component'] },
* { 'metadata.name': 'a', 'metadata.namespace': 'b' }
* ]
* ```
*
* This effectively means
*
* ```
* (kind = EITHER 'API' OR 'Component')
* OR
* (metadata.name = 'a' AND metadata.namespace = 'b' )
* ```
*
* @public
*/
export type EntityFilterQuery =
| Record<string, string | symbol | (string | symbol)[]>[]
| Record<string, string | symbol | (string | symbol)[]>;
/**
* A set of dot-separated paths into an entity's keys, showing what parts of an
* entity to include in a response, and excluding all others.
*
* @remarks
*
* Example: For an input of `['kind', 'metadata.annotations']`, then response
* objects will be shaped like
*
* ```
* {
* "kind": "Component",
* "metadata": {
* "annotations": {
* "foo": "bar"
* }
* }
* }
* ```
* @public
*/
export type EntityFieldsQuery = string[];
/**
* The request type for {@link CatalogClient.getEntities}.
*
@@ -36,67 +105,14 @@ export const CATALOG_FILTER_EXISTS = Symbol.for(
*/
export interface GetEntitiesRequest {
/**
* If given, return only entities that match the given patterns.
*
* @remarks
*
* If multiple filter sets are given as an array, then there is effectively an
* OR between each filter set.
*
* Within one filter set, there is effectively an AND between the various
* keys.
*
* Within one key, if there are more than one value, then there is effectively
* an OR between them.
*
* Example: For an input of
*
* ```
* [
* { kind: ['API', 'Component'] },
* { 'metadata.name': 'a', 'metadata.namespace': 'b' }
* ]
* ```
*
* This effectively means
*
* ```
* (kind = EITHER 'API' OR 'Component')
* OR
* (metadata.name = 'a' AND metadata.namespace = 'b' )
* ```
*
* Each key is a dot separated path in each object.
*
* As a value you can also pass in the symbol `CATALOG_FILTER_EXISTS`
* (exported from this package), which means that you assert on the existence
* of that key, no matter what its value is.
* If given, return only entities that match the given filter.
*/
filter?:
| Record<string, string | symbol | (string | symbol)[]>[]
| Record<string, string | symbol | (string | symbol)[]>
| undefined;
filter?: EntityFilterQuery;
/**
* If given, return only the parts of each entity that match those dot
* separated paths in each object.
*
* @remarks
*
* Example: For an input of `['kind', 'metadata.annotations']`, then response
* objects will be shaped like
*
* ```
* {
* "kind": "Component",
* "metadata": {
* "annotations": {
* "foo": "bar"
* }
* }
* }
* ```
* If given, return only the parts of each entity that match the field
* declarations.
*/
fields?: string[] | undefined;
fields?: EntityFieldsQuery;
/**
* If given, skips over the first N items in the result set.
*/
@@ -121,6 +137,45 @@ export interface GetEntitiesResponse {
items: Entity[];
}
/**
* The request type for {@link CatalogClient.getEntitiesByRefs}.
*
* @public
*/
export interface GetEntitiesByRefsRequest {
/**
* The list of entity refs to fetch.
*
* @remarks
*
* The returned list of entities will be in the same order as the refs, and
* null will be returned in those positions that were not found.
*/
entityRefs: string[];
/**
* If given, return only the parts of each entity that match the field
* declarations.
*/
fields?: EntityFieldsQuery | undefined;
}
/**
* The response type for {@link CatalogClient.getEntitiesByRefs}.
*
* @public
*/
export interface GetEntitiesByRefsResponse {
/**
* The returned list of entities.
*
* @remarks
*
* The list will be in the same order as the refs given in the request, and
* null will be returned in those positions that were not found.
*/
items: Array<Entity | undefined>;
}
/**
* The request type for {@link CatalogClient.getEntityAncestors}.
*
@@ -296,6 +351,23 @@ export interface CatalogApi {
options?: CatalogRequestOptions,
): Promise<GetEntitiesResponse>;
/**
* Gets a batch of entities, by their entity refs.
*
* @remarks
*
* The output list of entities is of the same size and in the same order as
* the requested list of entity refs. Entries that are not found are returned
* as null.
*
* @param request - Request parameters
* @param options - Additional options
*/
getEntitiesByRefs(
request: GetEntitiesByRefsRequest,
options?: CatalogRequestOptions,
): Promise<GetEntitiesByRefsResponse>;
/**
* Gets entity ancestor information, i.e. the hierarchy of parent entities
* whose processing resulted in a given entity appearing in the catalog.
+5 -1
View File
@@ -20,13 +20,17 @@ export type {
AddLocationResponse,
CatalogApi,
CatalogRequestOptions,
EntityFieldsQuery,
EntityFilterQuery,
GetEntitiesByRefsRequest,
GetEntitiesByRefsResponse,
GetEntitiesRequest,
GetEntitiesResponse,
GetEntityAncestorsRequest,
GetEntityAncestorsResponse,
Location,
GetEntityFacetsRequest,
GetEntityFacetsResponse,
Location,
ValidateEntityResponse,
} from './api';
export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status';
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { FeatureFlagged } from './FeatureFlagged';
import { render } from '@testing-library/react';
@@ -44,7 +45,7 @@ describe('FeatureFlagged', () => {
</Wrapper>,
);
expect(await queryByText('BACKSTAGE!')).toBeInTheDocument();
expect(queryByText('BACKSTAGE!')).toBeInTheDocument();
});
it('should not render contents when the feature flag is disabled', async () => {
jest
@@ -61,7 +62,7 @@ describe('FeatureFlagged', () => {
</Wrapper>,
);
expect(await queryByText('BACKSTAGE!')).not.toBeInTheDocument();
expect(queryByText('BACKSTAGE!')).not.toBeInTheDocument();
});
});
describe('without', () => {
@@ -80,7 +81,7 @@ describe('FeatureFlagged', () => {
</Wrapper>,
);
expect(await queryByText('BACKSTAGE!')).not.toBeInTheDocument();
expect(queryByText('BACKSTAGE!')).not.toBeInTheDocument();
});
it('should render contents when the feature flag is disabled', async () => {
jest
@@ -97,7 +98,7 @@ describe('FeatureFlagged', () => {
</Wrapper>,
);
expect(await queryByText('BACKSTAGE!')).toBeInTheDocument();
expect(queryByText('BACKSTAGE!')).toBeInTheDocument();
});
});
});
@@ -70,8 +70,8 @@ describe('<FeatureCalloutCircular />', () => {
description="description"
/>,
);
const dot = await getByTestId('dot');
const text = await getByTestId('text');
const dot = getByTestId('dot');
const text = getByTestId('text');
expect(dot).toBeInTheDocument();
expect(text).toBeInTheDocument();
@@ -96,8 +96,8 @@ describe('<FeatureCalloutCircular />', () => {
description="description"
/>,
);
const dot = await getByTestId('dot');
const text = await getByTestId('text');
const dot = getByTestId('dot');
const text = getByTestId('text');
act(() => {
Element.prototype.getBoundingClientRect = jest.fn(
@@ -128,8 +128,8 @@ describe('<FeatureCalloutCircular />', () => {
description="description"
/>,
);
const dot = await getByTestId('dot');
const text = await getByTestId('text');
const dot = getByTestId('dot');
const text = getByTestId('text');
act(() => {
Element.prototype.getBoundingClientRect = jest.fn(
@@ -41,7 +41,7 @@ describe('<Select />', () => {
const { getByText, getByTestId } = render(<Select {...minProps} />);
expect(getByText('Default')).toBeInTheDocument();
const input = await getByTestId('select');
const input = getByTestId('select');
expect(input.textContent).toBe('All results');
// Simulate click on input
@@ -18,7 +18,6 @@ import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
import Typography from '@material-ui/core/Typography';
import { WarningPanel, WarningProps } from './WarningPanel';
const propsTitle: WarningProps = { title: 'Mock title' };
@@ -41,7 +40,7 @@ describe('<WarningPanel />', () => {
it('renders title', async () => {
await renderInTestApp(<WarningPanel {...propsTitleMessage} />);
const expandIcon = await screen.getByText('Warning: Mock title');
const expandIcon = screen.getByText('Warning: Mock title');
fireEvent.click(expandIcon);
expect(screen.getByText('Warning: Mock title')).toBeInTheDocument();
expect(screen.getByText('Some more info')).toBeInTheDocument();
@@ -46,7 +46,7 @@ describe('BitriseArtifactsComponent', () => {
const rendered = renderComponent();
const btn = await rendered.findByTestId('btn');
expect(await rendered.queryByText('VISIBLE')).not.toBeInTheDocument();
expect(rendered.queryByText('VISIBLE')).not.toBeInTheDocument();
btn.click();
+14 -13
View File
@@ -37,7 +37,7 @@ import { EntityRelationSpec } from '@backstage/plugin-catalog-node';
import { GetEntitiesRequest } from '@backstage/catalog-client';
import { JsonValue } from '@backstage/types';
import { LocationEntityV1alpha1 } from '@backstage/catalog-model';
import { LocationSpec } from '@backstage/plugin-catalog-node';
import { LocationSpec as LocationSpec_2 } from '@backstage/plugin-catalog-common';
import { Logger } from 'winston';
import { Permission } from '@backstage/plugin-permission-common';
import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
@@ -86,9 +86,9 @@ export class AnnotateLocationEntityProcessor implements CatalogProcessor {
// (undocumented)
preProcessEntity(
entity: Entity,
location: LocationSpec,
location: LocationSpec_2,
_: CatalogProcessorEmit,
originLocation: LocationSpec,
originLocation: LocationSpec_2,
): Promise<Entity>;
}
@@ -100,7 +100,7 @@ export class AnnotateScmSlugEntityProcessor implements CatalogProcessor {
// (undocumented)
getProcessorName(): string;
// (undocumented)
preProcessEntity(entity: Entity, location: LocationSpec): Promise<Entity>;
preProcessEntity(entity: Entity, location: LocationSpec_2): Promise<Entity>;
}
// @public (undocumented)
@@ -110,7 +110,7 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor {
// (undocumented)
postProcessEntity(
entity: Entity,
_location: LocationSpec,
_location: LocationSpec_2,
emit: CatalogProcessorEmit,
): Promise<Entity>;
// (undocumented)
@@ -284,7 +284,7 @@ export class CodeOwnersProcessor implements CatalogProcessor {
// (undocumented)
getProcessorName(): string;
// (undocumented)
preProcessEntity(entity: Entity, location: LocationSpec): Promise<Entity>;
preProcessEntity(entity: Entity, location: LocationSpec_2): Promise<Entity>;
}
// @alpha
@@ -409,7 +409,7 @@ export class FileReaderProcessor implements CatalogProcessor {
getProcessorName(): string;
// (undocumented)
readLocation(
location: LocationSpec,
location: LocationSpec_2,
optional: boolean,
emit: CatalogProcessorEmit,
parser: CatalogProcessorParser,
@@ -431,7 +431,7 @@ export class LocationEntityProcessor implements CatalogProcessor {
// (undocumented)
postProcessEntity(
entity: Entity,
location: LocationSpec,
location: LocationSpec_2,
emit: CatalogProcessorEmit,
): Promise<Entity>;
}
@@ -441,18 +441,19 @@ export type LocationEntityProcessorOptions = {
integrations: ScmIntegrationRegistry;
};
export { LocationSpec };
// @public @deprecated
export type LocationSpec = LocationSpec_2;
// @public (undocumented)
export function locationSpecToLocationEntity(opts: {
location: LocationSpec;
location: LocationSpec_2;
parentEntity?: Entity;
}): LocationEntityV1alpha1;
// @public (undocumented)
export function parseEntityYaml(
data: Buffer,
location: LocationSpec,
location: LocationSpec_2,
): Iterable<CatalogProcessorResult>;
// @alpha
@@ -518,7 +519,7 @@ export class PlaceholderProcessor implements CatalogProcessor {
// (undocumented)
preProcessEntity(
entity: Entity,
location: LocationSpec,
location: LocationSpec_2,
emit: CatalogProcessorEmit,
): Promise<Entity>;
}
@@ -574,7 +575,7 @@ export class UrlReaderProcessor implements CatalogProcessor {
getProcessorName(): string;
// (undocumented)
readLocation(
location: LocationSpec,
location: LocationSpec_2,
optional: boolean,
emit: CatalogProcessorEmit,
parser: CatalogProcessorParser,
@@ -80,6 +80,36 @@ export type EntitiesResponse = {
pageInfo: PageInfo;
};
/**
* A request for a batch of entities.
*/
export interface EntitiesBatchRequest {
/**
* The refs for which to fetch entities.
*/
entityRefs: string[];
/**
* Any additional filters to apply in the selection of the entities.
*/
filter?: EntityFilter;
/**
* Strips out only the parts of the entity bodies to include in the response.
*/
fields?: (entity: Entity) => Entity;
/**
* The optional token that authorizes the action.
*/
authorizationToken?: string;
}
export interface EntitiesBatchResponse {
/**
* The list of entities, in the same order as the refs in the request. Entries
* that are null signify that no entity existed with that ref.
*/
items: Array<Entity | null>;
}
export type EntityAncestryResponse = {
rootEntityRef: string;
items: Array<{
@@ -130,6 +160,11 @@ export interface EntitiesCatalog {
*/
entities(request?: EntitiesRequest): Promise<EntitiesResponse>;
/**
* Fetches a batch of entities.
*/
entitiesBatch(request: EntitiesBatchRequest): Promise<EntitiesBatchResponse>;
/**
* Removes a single entity.
*
+16 -1
View File
@@ -22,7 +22,6 @@
export type {
DeferredEntity,
LocationSpec,
EntityRelationSpec,
CatalogProcessor,
CatalogProcessorParser,
@@ -48,3 +47,19 @@ export * from './processing';
export * from './search';
export * from './service';
export * from './util';
import { LocationSpec as NonDeprecatedLocationSpec } from '@backstage/plugin-catalog-common';
/**
* Holds the entity location information.
*
* @remarks
*
* `presence` flag: when using repo importer plugin, location is being created before the component yaml file is merged to the main branch.
* This flag is then set to indicate that the file can be not present.
* default value: 'required'.
*
* @public
* @deprecated use the same type from `@backstage/plugin-catalog-common` instead
*/
export type LocationSpec = NonDeprecatedLocationSpec;
@@ -17,7 +17,7 @@
import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import path from 'path';
import { LocationSpec } from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common';
/**
* Rules to apply to catalog entities.
@@ -22,7 +22,8 @@ import {
ScmIntegrations,
} from '@backstage/integration';
import { Logger } from 'winston';
import { CatalogProcessor, LocationSpec } from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import { CatalogProcessor } from '@backstage/plugin-catalog-node';
import { findCodeOwnerByTarget } from './lib';
const ALLOWED_KINDS = ['API', 'Component', 'Domain', 'Resource', 'System'];
@@ -25,10 +25,10 @@ import {
} from '@backstage/catalog-model';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { identity, merge, pickBy } from 'lodash';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import {
CatalogProcessor,
CatalogProcessorEmit,
LocationSpec,
} from '@backstage/plugin-catalog-node';
/** @public */
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import {
@@ -21,7 +22,8 @@ import {
} from '@backstage/integration';
import parseGitUrl from 'git-url-parse';
import { identity, merge, pickBy } from 'lodash';
import { CatalogProcessor, LocationSpec } from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import { CatalogProcessor } from '@backstage/plugin-catalog-node';
const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug';
const GITLAB_ACTIONS_ANNOTATION = 'gitlab.com/project-slug';
@@ -48,10 +48,10 @@ import {
UserEntity,
userEntityV1alpha1Validator,
} from '@backstage/catalog-model';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import {
CatalogProcessor,
CatalogProcessorEmit,
LocationSpec,
processingResult,
} from '@backstage/plugin-catalog-node';
@@ -18,11 +18,11 @@ import fs from 'fs-extra';
import g from 'glob';
import path from 'path';
import { promisify } from 'util';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import {
CatalogProcessor,
CatalogProcessorEmit,
CatalogProcessorParser,
LocationSpec,
processingResult,
} from '@backstage/plugin-catalog-node';
@@ -17,11 +17,11 @@
import { Entity, LocationEntity } from '@backstage/catalog-model';
import { ScmIntegrationRegistry } from '@backstage/integration';
import path from 'path';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import {
processingResult,
CatalogProcessor,
CatalogProcessorEmit,
LocationSpec,
} from '@backstage/plugin-catalog-node';
export function toAbsoluteUrl(
@@ -19,10 +19,10 @@ import { Entity } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/types';
import { ScmIntegrationRegistry } from '@backstage/integration';
import yaml from 'yaml';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import {
CatalogProcessor,
CatalogProcessorEmit,
LocationSpec,
processingResult,
} from '@backstage/plugin-catalog-node';
@@ -20,6 +20,7 @@ import { assertError } from '@backstage/errors';
import parseGitUrl from 'git-url-parse';
import limiterFactory from 'p-limit';
import { Logger } from 'winston';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import {
CatalogProcessor,
CatalogProcessorCache,
@@ -27,7 +28,6 @@ import {
CatalogProcessorEntityResult,
CatalogProcessorParser,
CatalogProcessorResult,
LocationSpec,
processingResult,
} from '@backstage/plugin-catalog-node';
@@ -17,10 +17,10 @@
import { Entity, stringifyLocationRef } from '@backstage/catalog-model';
import lodash from 'lodash';
import yaml from 'yaml';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import {
CatalogProcessorParser,
CatalogProcessorResult,
LocationSpec,
processingResult,
} from '@backstage/plugin-catalog-node';
@@ -32,10 +32,10 @@ import { JsonValue } from '@backstage/types';
import { ScmIntegrationRegistry } from '@backstage/integration';
import path from 'path';
import { Logger } from 'winston';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import {
CatalogProcessor,
CatalogProcessorParser,
LocationSpec,
processingResult,
} from '@backstage/plugin-catalog-node';
import {
@@ -27,7 +27,7 @@ import { JsonObject, JsonValue } from '@backstage/types';
import { InputError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import path from 'path';
import { LocationSpec } from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common';
export function isLocationEntity(entity: Entity): entity is LocationEntity {
return entity.kind === 'Location';
@@ -24,6 +24,7 @@ import { AuthorizedEntitiesCatalog } from './AuthorizedEntitiesCatalog';
describe('AuthorizedEntitiesCatalog', () => {
const fakeCatalog = {
entities: jest.fn(),
entitiesBatch: jest.fn(),
removeEntityByUid: jest.fn(),
entityAncestry: jest.fn(),
facets: jest.fn(),
@@ -92,6 +93,67 @@ describe('AuthorizedEntitiesCatalog', () => {
});
});
describe('entitiesBatch', () => {
it('returns empty response on DENY', async () => {
fakePermissionApi.authorizeConditional.mockResolvedValue([
{ result: AuthorizeResult.DENY },
]);
const catalog = createCatalog();
await expect(
catalog.entitiesBatch({
entityRefs: ['component:default/component-a'],
authorizationToken: 'abcd',
}),
).resolves.toEqual({
items: [null],
});
expect(fakeCatalog.entitiesBatch).not.toHaveBeenCalled();
});
it('calls underlying catalog method with correct filter on CONDITIONAL', async () => {
fakePermissionApi.authorizeConditional.mockResolvedValue([
{
result: AuthorizeResult.CONDITIONAL,
conditions: {
rule: 'IS_ENTITY_KIND',
params: { kinds: ['b'] },
},
},
]);
const catalog = createCatalog(isEntityKind);
await catalog.entitiesBatch({
entityRefs: ['component:default/component-a'],
authorizationToken: 'abcd',
});
expect(fakeCatalog.entitiesBatch).toHaveBeenCalledWith({
entityRefs: ['component:default/component-a'],
authorizationToken: 'abcd',
filter: { key: 'kind', values: ['b'] },
});
});
it('calls underlying catalog method on ALLOW', async () => {
fakePermissionApi.authorizeConditional.mockResolvedValue([
{ result: AuthorizeResult.ALLOW },
]);
const catalog = createCatalog();
await catalog.entitiesBatch({
entityRefs: ['component:default/component-a'],
authorizationToken: 'abcd',
});
expect(fakeCatalog.entitiesBatch).toHaveBeenCalledWith({
entityRefs: ['component:default/component-a'],
authorizationToken: 'abcd',
});
});
});
describe('removeEntityByUid', () => {
it('throws error on DENY', async () => {
fakeCatalog.entities.mockResolvedValue({
@@ -26,6 +26,8 @@ import {
} from '@backstage/plugin-permission-common';
import { ConditionTransformer } from '@backstage/plugin-permission-node';
import {
EntitiesBatchRequest,
EntitiesBatchResponse,
EntitiesCatalog,
EntitiesRequest,
EntitiesResponse,
@@ -73,6 +75,37 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog {
return this.entitiesCatalog.entities(request);
}
async entitiesBatch(
request: EntitiesBatchRequest,
): Promise<EntitiesBatchResponse> {
const authorizeDecision = (
await this.permissionApi.authorizeConditional(
[{ permission: catalogEntityReadPermission }],
{ token: request?.authorizationToken },
)
)[0];
if (authorizeDecision.result === AuthorizeResult.DENY) {
return {
items: new Array(request.entityRefs.length).fill(null),
};
}
if (authorizeDecision.result === AuthorizeResult.CONDITIONAL) {
const permissionFilter: EntityFilter = this.transformConditions(
authorizeDecision.conditions,
);
return this.entitiesCatalog.entitiesBatch({
...request,
filter: request?.filter
? { allOf: [permissionFilter, request.filter] }
: permissionFilter,
});
}
return this.entitiesCatalog.entitiesBatch(request);
}
async removeEntityByUid(
uid: string,
options?: { authorizationToken?: string },
@@ -534,6 +534,60 @@ describe('DefaultEntitiesCatalog', () => {
);
});
describe('entitiesBatch', () => {
it.each(databases.eachSupportedId())(
'queries for entities by ref, including duplicates, and gracefully returns null for missing entities',
async databaseId => {
const { knex } = await createDatabase(databaseId);
await addEntity(
knex,
{
apiVersion: 'a',
kind: 'k',
metadata: { name: 'one' },
spec: {},
relations: [],
},
[],
);
await addEntity(
knex,
{
apiVersion: 'a',
kind: 'k',
metadata: { name: 'two' },
spec: {},
relations: [],
},
[],
);
const catalog = new DefaultEntitiesCatalog(knex, stitcher);
const { items } = await catalog.entitiesBatch({
entityRefs: [
'k:default/two',
'k:default/one',
'k:default/two',
'not-even-a-ref',
'k:default/does-not-exist',
'k:default/two',
],
});
expect(items.map(e => e && stringifyEntityRef(e))).toEqual([
'k:default/two',
'k:default/one',
'k:default/two',
null,
null,
'k:default/two',
]);
},
);
});
describe('removeEntityByUid', () => {
it.each(databases.eachSupportedId())(
'also clears parent hashes',
@@ -23,6 +23,8 @@ import { InputError, NotFoundError } from '@backstage/errors';
import { Knex } from 'knex';
import lodash from 'lodash';
import {
EntitiesBatchRequest,
EntitiesBatchResponse,
EntitiesCatalog,
EntitiesRequest,
EntitiesResponse,
@@ -237,6 +239,38 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog {
};
}
async entitiesBatch(
request: EntitiesBatchRequest,
): Promise<EntitiesBatchResponse> {
const lookup = new Map<string, Entity>();
for (const chunk of lodash.chunk(request.entityRefs, 200)) {
let query = this.database<DbFinalEntitiesRow>('final_entities')
.innerJoin<DbRefreshStateRow>('refresh_state', {
'refresh_state.entity_id': 'final_entities.entity_id',
})
.select({
entityRef: 'refresh_state.entity_ref',
entity: 'final_entities.final_entity',
})
.whereIn('refresh_state.entity_ref', chunk);
if (request?.filter) {
query = parseFilter(request.filter, query, this.database);
}
for (const row of await query) {
lookup.set(row.entityRef, row.entity ? JSON.parse(row.entity) : null);
}
}
let items = request.entityRefs.map(ref => lookup.get(ref) ?? null);
if (request.fields) {
items = items.map(e => e && request.fields!(e));
}
return { items };
}
async removeEntityByUid(uid: string): Promise<void> {
// Clear the hashed state of the immediate parents of the deleted entity.
// This makes sure that when they get reprocessed, their output is written
@@ -48,6 +48,7 @@ describe('createRouter readonly disabled', () => {
beforeAll(async () => {
entitiesCatalog = {
entities: jest.fn(),
entitiesBatch: jest.fn(),
removeEntityByUid: jest.fn(),
entityAncestry: jest.fn(),
facets: jest.fn(),
@@ -257,6 +258,38 @@ describe('createRouter readonly disabled', () => {
});
});
describe('POST /entities/by-refs', () => {
it.each([
'',
'not json',
'[',
'[]',
'{}',
'{"unknown":7}',
'{"entityRefs":7}',
'{"entityRefs":[7]}',
])('properly rejects malformed request body, %p', async p => {
await expect(
request(app)
.post('/entities/by-refs')
.set('Content-Type', 'application/json')
.send(p),
).resolves.toMatchObject({ status: 400 });
});
it('can fetch entities by refs', async () => {
const entity: Entity = {} as any;
entitiesCatalog.entitiesBatch.mockResolvedValue({ items: [entity] });
const response = await request(app)
.post('/entities/by-refs')
.set('Content-Type', 'application/json')
.send('{"entityRefs":["a"]}');
expect(entitiesCatalog.entitiesBatch).toHaveBeenCalledTimes(1);
expect(response.status).toEqual(200);
expect(response.body).toEqual({ items: [entity] });
});
});
describe('GET /locations', () => {
it('happy path: lists locations', async () => {
const locations: Location[] = [
@@ -517,6 +550,7 @@ describe('createRouter readonly enabled', () => {
beforeAll(async () => {
entitiesCatalog = {
entities: jest.fn(),
entitiesBatch: jest.fn(),
removeEntityByUid: jest.fn(),
entityAncestry: jest.fn(),
facets: jest.fn(),
@@ -706,6 +740,7 @@ describe('NextRouter permissioning', () => {
beforeAll(async () => {
entitiesCatalog = {
entities: jest.fn(),
entitiesBatch: jest.fn(),
removeEntityByUid: jest.fn(),
entityAncestry: jest.fn(),
facets: jest.fn(),
@@ -28,24 +28,25 @@ import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import yn from 'yn';
import { z } from 'zod';
import { EntitiesCatalog } from '../catalog/types';
import { LocationAnalyzer } from '../ingestion/types';
import { CatalogProcessingOrchestrator } from '../processing/types';
import { validateEntityEnvelope } from '../processing/util';
import {
basicEntityFilter,
entitiesBatchRequest,
parseEntityFilterParams,
parseEntityPaginationParams,
parseEntityTransformParams,
} from './request';
import { parseEntityFacetParams } from './request/parseEntityFacetParams';
import { LocationService, RefreshOptions, RefreshService } from './types';
import {
disallowReadonlyMode,
locationInput,
validateRequestBody,
} from './util';
import { z } from 'zod';
import { parseEntityFacetParams } from './request/parseEntityFacetParams';
import { RefreshOptions, LocationService, RefreshService } from './types';
import { CatalogProcessingOrchestrator } from '../processing/types';
import { validateEntityEnvelope } from '../processing/util';
/**
* Options used by {@link createRouter}.
@@ -173,6 +174,16 @@ export async function createRouter(
res.status(200).json(response);
},
)
.post('/entities/by-refs', async (req, res) => {
const request = entitiesBatchRequest(req);
const token = getBearerToken(req.header('authorization'));
const response = await entitiesCatalog.entitiesBatch({
entityRefs: request.entityRefs,
fields: parseEntityTransformParams(req.query),
authorizationToken: token,
});
res.status(200).json(response);
})
.get('/entity-facets', async (req, res) => {
const response = await entitiesCatalog.facets({
filter: parseEntityFilterParams(req.query),
@@ -0,0 +1,33 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InputError } from '@backstage/errors';
import { Request } from 'express';
import { z } from 'zod';
const schema = z.object({
entityRefs: z.array(z.string()),
});
export function entitiesBatchRequest(req: Request) {
try {
return schema.parse(req.body);
} catch (error) {
throw new InputError(
`Malformed request body (did you remember to specify an application/json content type?), ${error.message}`,
);
}
}
@@ -14,6 +14,7 @@
* limitations under the License.
*/
export { entitiesBatchRequest } from './entitiesBatchRequest';
export { basicEntityFilter } from './basicEntityFilter';
export { parseEntityFilterParams } from './parseEntityFilterParams';
export { parseEntityPaginationParams } from './parseEntityPaginationParams';
@@ -23,7 +23,7 @@ import {
stringifyLocationRef,
} from '@backstage/catalog-model';
import { createHash } from 'crypto';
import { LocationSpec } from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common';
export function locationSpecToMetadataName(location: LocationSpec) {
const hash = createHash('sha1')
+5 -1
View File
@@ -1 +1,5 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
rules: {
'testing-library/prefer-screen-queries': 'error',
},
});
@@ -28,6 +28,7 @@ import {
TestApiProvider,
TestApiRegistry,
} from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { catalogGraphRouteRef } from '../../routes';
@@ -79,15 +80,15 @@ describe('<CatalogGraphCard/>', () => {
relations: [],
}));
const { findByText, findAllByTestId } = await renderInTestApp(wrapper, {
await renderInTestApp(wrapper, {
mountedRoutes: {
'/entity/{kind}/{namespace}/{name}': entityRouteRef,
'/catalog-graph': catalogGraphRouteRef,
},
});
expect(await findByText('b:d/c')).toBeInTheDocument();
expect(await findAllByTestId('node')).toHaveLength(1);
expect(await screen.findByText('b:d/c')).toBeInTheDocument();
expect(await screen.findAllByTestId('node')).toHaveLength(1);
expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1);
});
@@ -97,7 +98,7 @@ describe('<CatalogGraphCard/>', () => {
relations: [],
}));
const { findByText } = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
<CatalogGraphCard title="Custom Title" />
@@ -111,7 +112,7 @@ describe('<CatalogGraphCard/>', () => {
},
);
expect(await findByText('Custom Title')).toBeInTheDocument();
expect(await screen.findByText('Custom Title')).toBeInTheDocument();
});
test('renders link to standalone viewer', async () => {
@@ -120,15 +121,15 @@ describe('<CatalogGraphCard/>', () => {
relations: [],
}));
const { findByText, getByText } = await renderInTestApp(wrapper, {
await renderInTestApp(wrapper, {
mountedRoutes: {
'/entity/{kind}/{namespace}/{name}': entityRouteRef,
'/catalog-graph': catalogGraphRouteRef,
},
});
expect(await findByText('b:d/c')).toBeInTheDocument();
const button = getByText('View graph');
expect(await screen.findByText('b:d/c')).toBeInTheDocument();
const button = screen.getByText('View graph');
expect(button).toBeInTheDocument();
expect(button.closest('a')).toHaveAttribute(
'href',
@@ -137,7 +138,7 @@ describe('<CatalogGraphCard/>', () => {
});
test('renders link to standalone viewer with custom config', async () => {
const { findByText, getByText } = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
<CatalogGraphCard maxDepth={2} mergeRelations={false} />
@@ -151,8 +152,8 @@ describe('<CatalogGraphCard/>', () => {
},
);
expect(await findByText('b:d/c')).toBeInTheDocument();
const button = getByText('View graph');
expect(await screen.findByText('b:d/c')).toBeInTheDocument();
const button = screen.getByText('View graph');
expect(button).toBeInTheDocument();
expect(button.closest('a')).toHaveAttribute(
'href',
@@ -167,7 +168,7 @@ describe('<CatalogGraphCard/>', () => {
}));
const analyticsSpy = new MockAnalyticsApi();
const { findByText } = await renderInTestApp(
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analyticsSpy]]}>
{wrapper}
</TestApiProvider>,
@@ -179,8 +180,8 @@ describe('<CatalogGraphCard/>', () => {
},
);
expect(await findByText('b:d/c')).toBeInTheDocument();
await userEvent.click(await findByText('b:d/c'));
expect(await screen.findByText('b:d/c')).toBeInTheDocument();
await userEvent.click(await screen.findByText('b:d/c'));
expect(analyticsSpy.getEvents()[0]).toMatchObject({
action: 'click',
@@ -22,6 +22,7 @@ import {
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { CatalogGraphPage } from './CatalogGraphPage';
@@ -108,19 +109,16 @@ describe('<CatalogGraphPage/>', () => {
n === 'b:d/e' ? entityE : entityC,
);
const { getByText, findByText, findAllByTestId } = await renderInTestApp(
wrapper,
{
mountedRoutes: {
'/entity/{kind}/{namespace}/{name}': entityRouteRef,
},
await renderInTestApp(wrapper, {
mountedRoutes: {
'/entity/{kind}/{namespace}/{name}': entityRouteRef,
},
);
});
expect(getByText('Catalog Graph')).toBeInTheDocument();
expect(await findByText('b:d/c')).toBeInTheDocument();
expect(await findByText('b:d/e')).toBeInTheDocument();
expect(await findAllByTestId('node')).toHaveLength(2);
expect(screen.getByText('Catalog Graph')).toBeInTheDocument();
expect(await screen.findByText('b:d/c')).toBeInTheDocument();
expect(await screen.findByText('b:d/e')).toBeInTheDocument();
expect(await screen.findAllByTestId('node')).toHaveLength(2);
expect(catalog.getEntityByRef).toHaveBeenCalledTimes(2);
});
@@ -129,17 +127,17 @@ describe('<CatalogGraphPage/>', () => {
n === 'b:d/e' ? entityE : entityC,
);
const { getByText, queryByText } = await renderInTestApp(wrapper, {
await renderInTestApp(wrapper, {
mountedRoutes: {
'/entity/{kind}/{namespace}/{name}': entityRouteRef,
},
});
expect(queryByText('Max Depth')).toBeNull();
expect(screen.queryByText('Max Depth')).toBeNull();
await userEvent.click(getByText('Filters'));
await userEvent.click(screen.getByText('Filters'));
expect(getByText('Max Depth')).toBeInTheDocument();
expect(screen.getByText('Max Depth')).toBeInTheDocument();
});
test('should select other entity', async () => {
@@ -147,20 +145,17 @@ describe('<CatalogGraphPage/>', () => {
n === 'b:d/e' ? entityE : entityC,
);
const { getByText, findByText, findAllByTestId } = await renderInTestApp(
wrapper,
{
mountedRoutes: {
'/entity/{kind}/{namespace}/{name}': entityRouteRef,
},
await renderInTestApp(wrapper, {
mountedRoutes: {
'/entity/{kind}/{namespace}/{name}': entityRouteRef,
},
);
});
expect(await findAllByTestId('node')).toHaveLength(2);
expect(await screen.findAllByTestId('node')).toHaveLength(2);
await userEvent.click(getByText('b:d/e'));
await userEvent.click(screen.getByText('b:d/e'));
expect(await findByText('hasPart')).toBeInTheDocument();
expect(await screen.findByText('hasPart')).toBeInTheDocument();
});
test('should navigate to entity', async () => {
@@ -168,17 +163,17 @@ describe('<CatalogGraphPage/>', () => {
n === 'b:d/e' ? entityE : entityC,
);
const { getByText, findAllByTestId } = await renderInTestApp(wrapper, {
await renderInTestApp(wrapper, {
mountedRoutes: {
'/entity/{kind}/{namespace}/{name}': entityRouteRef,
},
});
expect(await findAllByTestId('node')).toHaveLength(2);
expect(await screen.findAllByTestId('node')).toHaveLength(2);
const user = userEvent.setup();
await user.keyboard('{Shift>}');
await user.click(getByText('b:d/e'));
await user.click(screen.getByText('b:d/e'));
expect(navigate).toHaveBeenCalledWith('/entity/{kind}/{namespace}/{name}');
});
@@ -188,7 +183,7 @@ describe('<CatalogGraphPage/>', () => {
);
const analyticsSpy = new MockAnalyticsApi();
const { getByText, findAllByTestId } = await renderInTestApp(
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analyticsSpy]]}>
{wrapper}
</TestApiProvider>,
@@ -199,12 +194,12 @@ describe('<CatalogGraphPage/>', () => {
},
);
expect(await findAllByTestId('node')).toHaveLength(2);
expect(await screen.findAllByTestId('node')).toHaveLength(2);
// We wait a bit here to reliably reproduce an issue where that requires the `baseVal` and `view` mocks
await new Promise(r => setTimeout(r, 100));
await userEvent.click(getByText('b:d/e'));
await userEvent.click(screen.getByText('b:d/e'));
expect(analyticsSpy.getEvents()[0]).toMatchObject({
action: 'click',
@@ -218,7 +213,7 @@ describe('<CatalogGraphPage/>', () => {
);
const analyticsSpy = new MockAnalyticsApi();
const { getByText, findAllByTestId } = await renderInTestApp(
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analyticsSpy]]}>
{wrapper}
</TestApiProvider>,
@@ -229,11 +224,11 @@ describe('<CatalogGraphPage/>', () => {
},
);
expect(await findAllByTestId('node')).toHaveLength(2);
expect(await screen.findAllByTestId('node')).toHaveLength(2);
const user = userEvent.setup();
await user.keyboard('{Shift>}');
await user.click(getByText('b:d/e'));
await user.click(screen.getByText('b:d/e'));
expect(analyticsSpy.getEvents()[0]).toMatchObject({
action: 'click',
@@ -13,7 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render, waitFor } from '@testing-library/react';
import { render, waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { CurveFilter } from './CurveFilter';
@@ -21,26 +22,22 @@ import { CurveFilter } from './CurveFilter';
describe('<CurveFilter/>', () => {
test('should display current curve label', () => {
const onChange = jest.fn();
const { getByText } = render(
<CurveFilter value="curveMonotoneX" onChange={onChange} />,
);
render(<CurveFilter value="curveMonotoneX" onChange={onChange} />);
expect(getByText('Monotone X')).toBeInTheDocument();
expect(screen.getByText('Monotone X')).toBeInTheDocument();
});
test('should select an alternative curve factory', async () => {
const onChange = jest.fn();
const { getByText, getByTestId } = render(
<CurveFilter value="curveStepBefore" onChange={onChange} />,
);
render(<CurveFilter value="curveStepBefore" onChange={onChange} />);
expect(getByText('Step Before')).toBeInTheDocument();
expect(screen.getByText('Step Before')).toBeInTheDocument();
await userEvent.click(getByTestId('select'));
await userEvent.click(getByText('Monotone X'));
await userEvent.click(screen.getByTestId('select'));
await userEvent.click(screen.getByText('Monotone X'));
await waitFor(() => {
expect(getByText('Monotone X')).toBeInTheDocument();
expect(screen.getByText('Monotone X')).toBeInTheDocument();
expect(onChange).toHaveBeenCalledWith('curveMonotoneX');
});
});
@@ -13,7 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render, waitFor } from '@testing-library/react';
import { render, waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Direction } from '../EntityRelationsGraph';
@@ -21,26 +22,26 @@ import { DirectionFilter } from './DirectionFilter';
describe('<DirectionFilter/>', () => {
test('should display current value', () => {
const { getByText } = render(
render(
<DirectionFilter value={Direction.LEFT_RIGHT} onChange={() => {}} />,
);
expect(getByText('Left to right')).toBeInTheDocument();
expect(screen.getByText('Left to right')).toBeInTheDocument();
});
test('should select direction', async () => {
const onChange = jest.fn();
const { getByText, getByTestId } = render(
render(
<DirectionFilter value={Direction.RIGHT_LEFT} onChange={onChange} />,
);
expect(getByText('Right to left')).toBeInTheDocument();
expect(screen.getByText('Right to left')).toBeInTheDocument();
await userEvent.click(getByTestId('select'));
await userEvent.click(getByText('Top to bottom'));
await userEvent.click(screen.getByTestId('select'));
await userEvent.click(screen.getByText('Top to bottom'));
await waitFor(() => {
expect(getByText('Top to bottom')).toBeInTheDocument();
expect(screen.getByText('Top to bottom')).toBeInTheDocument();
expect(onChange).toHaveBeenCalledWith(Direction.TOP_BOTTOM);
});
});
@@ -13,64 +13,65 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import user from '@testing-library/user-event';
import React from 'react';
import { MaxDepthFilter } from './MaxDepthFilter';
describe('<MaxDepthFilter/>', () => {
test('should display current value', () => {
const { getByLabelText } = render(
<MaxDepthFilter value={5} onChange={() => {}} />,
);
render(<MaxDepthFilter value={5} onChange={() => {}} />);
expect(getByLabelText('maxp')).toBeInTheDocument();
expect(getByLabelText('maxp')).toHaveValue(5);
expect(screen.getByLabelText('maxp')).toBeInTheDocument();
expect(screen.getByLabelText('maxp')).toHaveValue(5);
});
test('should display infinite if non finite', () => {
const { getByPlaceholderText, getByLabelText } = render(
render(
<MaxDepthFilter value={Number.POSITIVE_INFINITY} onChange={() => {}} />,
);
expect(getByPlaceholderText(/Infinite/)).toBeInTheDocument();
expect(getByLabelText('maxp')).toHaveValue(null);
expect(screen.getByPlaceholderText(/Infinite/)).toBeInTheDocument();
expect(screen.getByLabelText('maxp')).toHaveValue(null);
});
test('should clear max depth', async () => {
const onChange = jest.fn();
const { getByLabelText } = render(
<MaxDepthFilter value={10} onChange={onChange} />,
);
render(<MaxDepthFilter value={10} onChange={onChange} />);
await userEvent.click(getByLabelText('clear max depth'));
expect(onChange).not.toHaveBeenCalled();
await user.click(screen.getByLabelText('clear max depth'));
expect(onChange).toHaveBeenCalledWith(Number.POSITIVE_INFINITY);
});
test('should set max depth to undefined if below one', async () => {
const onChange = jest.fn();
const { getByLabelText } = render(
<MaxDepthFilter value={1} onChange={onChange} />,
);
render(<MaxDepthFilter value={1} onChange={onChange} />);
await userEvent.clear(getByLabelText('maxp'));
await userEvent.type(getByLabelText('maxp'), '0');
await user.clear(screen.getByLabelText('maxp'));
await user.type(screen.getByLabelText('maxp'), '0');
expect(onChange).toHaveBeenCalledWith(Number.POSITIVE_INFINITY);
});
test('should select direction', async () => {
const onChange = jest.fn();
const { getByLabelText } = render(
<MaxDepthFilter value={5} onChange={onChange} />,
let value = 5;
render(
<MaxDepthFilter
value={value}
onChange={v => {
value = v;
}}
/>,
);
expect(getByLabelText('maxp')).toHaveValue(5);
expect(screen.getByLabelText('maxp')).toHaveValue(5);
expect(value).toBe(5);
await userEvent.clear(getByLabelText('maxp'));
await userEvent.type(getByLabelText('maxp'), '10');
waitFor(() => {
expect(onChange).toHaveBeenCalledWith(10);
});
await user.clear(screen.getByLabelText('maxp'));
expect(value).toBe(Number.POSITIVE_INFINITY);
await user.type(screen.getByLabelText('maxp'), '10');
expect(value).toBe(10);
});
});
@@ -23,7 +23,7 @@ import {
Typography,
} from '@material-ui/core';
import ClearIcon from '@material-ui/icons/Clear';
import React, { useCallback } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
export type Props = {
value: number;
@@ -42,18 +42,37 @@ const useStyles = makeStyles(
export const MaxDepthFilter = ({ value, onChange }: Props) => {
const classes = useStyles();
const onChangeRef = useRef(onChange);
const [currentValue, setCurrentValue] = useState(value);
// Keep a fresh reference to the latest callback
useEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);
// If the value changes externally, update ourselves
useEffect(() => {
setCurrentValue(value);
}, [value]);
// When the entered text changes, update ourselves and communicate externally
const handleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const v = Number(event.target.value);
onChange(v <= 0 ? Number.POSITIVE_INFINITY : v);
const newValueNumeric = Number(event.target.value);
const newValue =
Number.isFinite(newValueNumeric) && newValueNumeric > 0
? newValueNumeric
: Number.POSITIVE_INFINITY;
setCurrentValue(newValue);
onChangeRef.current(newValue);
},
[onChange],
[],
);
const reset = useCallback(() => {
onChange(Number.POSITIVE_INFINITY);
}, [onChange]);
setCurrentValue(Number.POSITIVE_INFINITY);
onChangeRef.current(Number.POSITIVE_INFINITY);
}, [onChangeRef]);
return (
<Box pb={1} pt={1}>
@@ -62,7 +81,7 @@ export const MaxDepthFilter = ({ value, onChange }: Props) => {
<OutlinedInput
type="number"
placeholder="∞ Infinite"
value={isFinite(value) ? value : ''}
value={Number.isFinite(currentValue) ? String(currentValue) : ''}
onChange={handleChange}
endAdornment={
<InputAdornment position="end">
@@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GetEntityFacetsResponse } from '@backstage/catalog-client';
import { ApiProvider } from '@backstage/core-app-api';
import { AlertApi, alertApiRef } from '@backstage/core-plugin-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import { waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { SelectedKindsFilter } from './SelectedKindsFilter';
@@ -42,37 +43,37 @@ const apis = TestApiRegistry.from(
describe('<SelectedKindsFilter/>', () => {
it('should not explode while loading', async () => {
const rendered = await renderWithEffects(
const { baseElement } = await renderWithEffects(
<ApiProvider apis={apis}>
<SelectedKindsFilter value={['api', 'component']} onChange={() => {}} />
</ApiProvider>,
);
expect(rendered.baseElement).toBeInTheDocument();
expect(baseElement).toBeInTheDocument();
});
it('should render current value', async () => {
const rendered = await renderWithEffects(
await renderWithEffects(
<ApiProvider apis={apis}>
<SelectedKindsFilter value={['api', 'component']} onChange={() => {}} />
</ApiProvider>,
);
expect(rendered.getByText('API')).toBeInTheDocument();
expect(rendered.getByText('Component')).toBeInTheDocument();
expect(screen.getByText('API')).toBeInTheDocument();
expect(screen.getByText('Component')).toBeInTheDocument();
});
it('should select value', async () => {
const onChange = jest.fn();
const { getByLabelText, getByText } = await renderWithEffects(
await renderWithEffects(
<ApiProvider apis={apis}>
<SelectedKindsFilter value={['api', 'component']} onChange={onChange} />
</ApiProvider>,
);
await userEvent.click(getByLabelText('Open'));
await waitFor(() => expect(getByText('System')).toBeInTheDocument());
await userEvent.click(screen.getByLabelText('Open'));
await waitFor(() => expect(screen.getByText('System')).toBeInTheDocument());
await userEvent.click(getByText('System'));
await userEvent.click(screen.getByText('System'));
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(['api', 'component', 'system']);
@@ -81,7 +82,7 @@ describe('<SelectedKindsFilter/>', () => {
it('should return undefined if all values are selected', async () => {
const onChange = jest.fn();
const { getByLabelText, getByText } = await renderWithEffects(
await renderWithEffects(
<ApiProvider apis={apis}>
<SelectedKindsFilter
value={['api', 'component', 'system', 'domain']}
@@ -89,11 +90,13 @@ describe('<SelectedKindsFilter/>', () => {
/>
</ApiProvider>,
);
await userEvent.click(getByLabelText('Open'));
await userEvent.click(screen.getByLabelText('Open'));
await waitFor(() => expect(getByText('Resource')).toBeInTheDocument());
await waitFor(() =>
expect(screen.getByText('Resource')).toBeInTheDocument(),
);
await userEvent.click(getByText('Resource'));
await userEvent.click(screen.getByText('Resource'));
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(undefined);
@@ -102,13 +105,13 @@ describe('<SelectedKindsFilter/>', () => {
it('should return all values when cleared', async () => {
const onChange = jest.fn();
const { getByRole } = await renderWithEffects(
await renderWithEffects(
<ApiProvider apis={apis}>
<SelectedKindsFilter value={[]} onChange={onChange} />
</ApiProvider>,
);
await userEvent.click(getByRole('combobox'));
await userEvent.click(screen.getByRole('combobox'));
await userEvent.tab();
await waitFor(() => {
@@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
RELATION_CHILD_OF,
RELATION_HAS_MEMBER,
RELATION_OWNED_BY,
} from '@backstage/catalog-model';
import { render, waitFor } from '@testing-library/react';
import { render, waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { ALL_RELATION_PAIRS } from '../EntityRelationsGraph';
@@ -26,7 +27,7 @@ import { SelectedRelationsFilter } from './SelectedRelationsFilter';
describe('<SelectedRelationsFilter/>', () => {
test('should render current value', () => {
const { getByText } = render(
render(
<SelectedRelationsFilter
relationPairs={ALL_RELATION_PAIRS}
value={[RELATION_OWNED_BY, RELATION_CHILD_OF]}
@@ -34,13 +35,13 @@ describe('<SelectedRelationsFilter/>', () => {
/>,
);
expect(getByText(RELATION_OWNED_BY)).toBeInTheDocument();
expect(getByText(RELATION_CHILD_OF)).toBeInTheDocument();
expect(screen.getByText(RELATION_OWNED_BY)).toBeInTheDocument();
expect(screen.getByText(RELATION_CHILD_OF)).toBeInTheDocument();
});
test('should select value', async () => {
const onChange = jest.fn();
const { getByText, getByLabelText } = render(
render(
<SelectedRelationsFilter
relationPairs={ALL_RELATION_PAIRS}
value={[RELATION_OWNED_BY, RELATION_CHILD_OF]}
@@ -48,13 +49,13 @@ describe('<SelectedRelationsFilter/>', () => {
/>,
);
await userEvent.click(getByLabelText('Open'));
await userEvent.click(screen.getByLabelText('Open'));
await waitFor(() =>
expect(getByText(RELATION_HAS_MEMBER)).toBeInTheDocument(),
expect(screen.getByText(RELATION_HAS_MEMBER)).toBeInTheDocument(),
);
await userEvent.click(getByText(RELATION_HAS_MEMBER));
await userEvent.click(screen.getByText(RELATION_HAS_MEMBER));
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith([
@@ -67,7 +68,7 @@ describe('<SelectedRelationsFilter/>', () => {
test('should return undefined if all values are selected', async () => {
const onChange = jest.fn();
const { getByText, getByLabelText } = render(
render(
<SelectedRelationsFilter
relationPairs={ALL_RELATION_PAIRS}
value={ALL_RELATION_PAIRS.flatMap(p => p).filter(
@@ -77,13 +78,13 @@ describe('<SelectedRelationsFilter/>', () => {
/>,
);
await userEvent.click(getByLabelText('Open'));
await userEvent.click(screen.getByLabelText('Open'));
await waitFor(() =>
expect(getByText(RELATION_HAS_MEMBER)).toBeInTheDocument(),
expect(screen.getByText(RELATION_HAS_MEMBER)).toBeInTheDocument(),
);
await userEvent.click(getByText(RELATION_HAS_MEMBER));
await userEvent.click(screen.getByText(RELATION_HAS_MEMBER));
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(undefined);
@@ -92,7 +93,7 @@ describe('<SelectedRelationsFilter/>', () => {
test('should return all values when cleared', async () => {
const onChange = jest.fn();
const { getByRole } = render(
render(
<SelectedRelationsFilter
relationPairs={ALL_RELATION_PAIRS}
value={[]}
@@ -100,7 +101,7 @@ describe('<SelectedRelationsFilter/>', () => {
/>,
);
await userEvent.click(getByRole('combobox'));
await userEvent.click(screen.getByRole('combobox'));
await userEvent.tab();
await waitFor(() => {
@@ -13,31 +13,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { SwitchFilter } from './SwitchFilter';
describe('<SwitchFilter/>', () => {
test('should render value', () => {
const { getByLabelText } = render(
<SwitchFilter label="My label" value={false} onChange={() => {}} />,
);
render(<SwitchFilter label="My label" value={false} onChange={() => {}} />);
expect(getByLabelText('My label')).toBeInTheDocument();
expect(getByLabelText('My label')).not.toBeChecked();
expect(screen.getByLabelText('My label')).toBeInTheDocument();
expect(screen.getByLabelText('My label')).not.toBeChecked();
});
test('should toggle value', async () => {
const onChange = jest.fn();
const { getByLabelText } = render(
<SwitchFilter label="My label" value onChange={onChange} />,
);
render(<SwitchFilter label="My label" value onChange={onChange} />);
expect(getByLabelText('My label')).toBeInTheDocument();
expect(getByLabelText('My label')).toBeChecked();
expect(screen.getByLabelText('My label')).toBeInTheDocument();
expect(screen.getByLabelText('My label')).toBeChecked();
await userEvent.click(getByLabelText('My label'));
await userEvent.click(screen.getByLabelText('My label'));
expect(onChange).toHaveBeenCalledWith(false);
});
@@ -13,17 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
RELATION_CHILD_OF,
RELATION_PARENT_OF,
} from '@backstage/catalog-model';
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { CustomLabel } from './CustomLabel';
describe('<CustomLabel />', () => {
test('renders label', () => {
const { getByText } = render(
render(
<svg xmlns="http://www.w3.org/2000/svg">
<CustomLabel
edge={{
@@ -36,11 +37,11 @@ describe('<CustomLabel />', () => {
</svg>,
);
expect(getByText(RELATION_PARENT_OF)).toBeInTheDocument();
expect(screen.getByText(RELATION_PARENT_OF)).toBeInTheDocument();
});
test('renders label with multiple relations', () => {
const { getByText } = render(
render(
<svg xmlns="http://www.w3.org/2000/svg">
<CustomLabel
edge={{
@@ -53,7 +54,7 @@ describe('<CustomLabel />', () => {
</svg>,
);
expect(getByText(RELATION_PARENT_OF)).toBeInTheDocument();
expect(getByText(RELATION_CHILD_OF)).toBeInTheDocument();
expect(screen.getByText(RELATION_PARENT_OF)).toBeInTheDocument();
expect(screen.getByText(RELATION_CHILD_OF)).toBeInTheDocument();
});
});
@@ -13,14 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { CustomNode } from './CustomNode';
import userEvent from '@testing-library/user-event';
describe('<CustomNode />', () => {
test('renders node', async () => {
const { getByText } = await renderInTestApp(
await renderInTestApp(
<svg xmlns="http://www.w3.org/2000/svg">
<CustomNode
node={{
@@ -35,11 +37,11 @@ describe('<CustomNode />', () => {
</svg>,
);
expect(getByText('kind:namespace/name')).toBeInTheDocument();
expect(screen.getByText('kind:namespace/name')).toBeInTheDocument();
});
test('renders node, skips default namespace', async () => {
const { getByText } = await renderInTestApp(
await renderInTestApp(
<svg xmlns="http://www.w3.org/2000/svg">
<CustomNode
node={{
@@ -53,12 +55,12 @@ describe('<CustomNode />', () => {
</svg>,
);
expect(getByText('kind:name')).toBeInTheDocument();
expect(screen.getByText('kind:name')).toBeInTheDocument();
});
test('renders node with onClick', async () => {
const onClick = jest.fn();
const { getByText } = await renderInTestApp(
await renderInTestApp(
<svg xmlns="http://www.w3.org/2000/svg">
<CustomNode
node={{
@@ -73,13 +75,13 @@ describe('<CustomNode />', () => {
</svg>,
);
expect(getByText('kind:namespace/name')).toBeInTheDocument();
await userEvent.click(getByText('kind:namespace/name'));
expect(screen.getByText('kind:namespace/name')).toBeInTheDocument();
await userEvent.click(screen.getByText('kind:namespace/name'));
expect(onClick).toHaveBeenCalledTimes(1);
});
test('renders title if entity has one', async () => {
const { getByText } = await renderInTestApp(
await renderInTestApp(
<svg xmlns="http://www.w3.org/2000/svg">
<CustomNode
node={{
@@ -94,6 +96,6 @@ describe('<CustomNode />', () => {
</svg>,
);
expect(getByText('Custom Title')).toBeInTheDocument();
expect(screen.getByText('Custom Title')).toBeInTheDocument();
});
});
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { EntityKindIcon } from './EntityKindIcon';
@@ -24,6 +24,7 @@ import {
import { DependencyGraphTypes } from '@backstage/core-components';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React, { FunctionComponent } from 'react';
import { EntityRelationsGraph } from './EntityRelationsGraph';
@@ -142,7 +143,7 @@ describe('<EntityRelationsGraph/>', () => {
relations: [],
});
const { findByText, findAllByTestId } = await renderInTestApp(
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
@@ -150,15 +151,15 @@ describe('<EntityRelationsGraph/>', () => {
</Wrapper>,
);
expect(await findByText('b:d/c')).toBeInTheDocument();
expect(await findAllByTestId('node')).toHaveLength(1);
expect(await screen.findByText('b:d/c')).toBeInTheDocument();
expect(await screen.findAllByTestId('node')).toHaveLength(1);
expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1);
});
test('renders a progress indicator while loading', async () => {
catalog.getEntityByRef.mockImplementation(() => new Promise(() => {}));
const { findByRole } = await renderInTestApp(
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
@@ -166,7 +167,7 @@ describe('<EntityRelationsGraph/>', () => {
</Wrapper>,
);
expect(await findByRole('progressbar')).toBeInTheDocument();
expect(await screen.findByRole('progressbar')).toBeInTheDocument();
expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1);
});
@@ -197,7 +198,7 @@ describe('<EntityRelationsGraph/>', () => {
return undefined;
});
const { findByText, findAllByTestId } = await renderInTestApp(
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
@@ -205,32 +206,31 @@ describe('<EntityRelationsGraph/>', () => {
</Wrapper>,
);
expect(await findByText('b:d/c')).toBeInTheDocument();
expect(await findAllByTestId('node')).toHaveLength(1);
expect(await screen.findByText('b:d/c')).toBeInTheDocument();
expect(await screen.findAllByTestId('node')).toHaveLength(1);
expect(catalog.getEntityByRef).toHaveBeenCalledTimes(2);
});
test('renders at max depth of one', async () => {
catalog.getEntityByRef.mockImplementation(async n => entities[n as string]);
const { findByText, findAllByTestId, findAllByText } =
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
maxDepth={1}
/>
</Wrapper>,
);
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
maxDepth={1}
/>
</Wrapper>,
);
expect(await findByText('b:d/c')).toBeInTheDocument();
expect(await findByText('b:d/c1')).toBeInTheDocument();
expect(await findByText('k:d/a1')).toBeInTheDocument();
expect(await findAllByTestId('node')).toHaveLength(3);
expect(await screen.findByText('b:d/c')).toBeInTheDocument();
expect(await screen.findByText('b:d/c1')).toBeInTheDocument();
expect(await screen.findByText('k:d/a1')).toBeInTheDocument();
expect(await screen.findAllByTestId('node')).toHaveLength(3);
expect(await findAllByText('ownerOf')).toHaveLength(1);
expect(await findAllByText('hasPart')).toHaveLength(1);
expect(await findAllByTestId('label')).toHaveLength(2);
expect(await screen.findAllByText('ownerOf')).toHaveLength(1);
expect(await screen.findAllByText('hasPart')).toHaveLength(1);
expect(await screen.findAllByTestId('label')).toHaveLength(2);
expect(catalog.getEntityByRef).toHaveBeenCalledTimes(3);
});
@@ -238,26 +238,25 @@ describe('<EntityRelationsGraph/>', () => {
test('renders simplified graph at full depth', async () => {
catalog.getEntityByRef.mockImplementation(async n => entities[n as string]);
const { findByText, findAllByText, findAllByTestId } =
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
unidirectional
maxDepth={Number.POSITIVE_INFINITY}
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
/>
</Wrapper>,
);
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
unidirectional
maxDepth={Number.POSITIVE_INFINITY}
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
/>
</Wrapper>,
);
expect(await findByText('b:d/c')).toBeInTheDocument();
expect(await findByText('b:d/c1')).toBeInTheDocument();
expect(await findByText('k:d/a1')).toBeInTheDocument();
expect(await findByText('b:d/c2')).toBeInTheDocument();
expect(await findAllByTestId('node')).toHaveLength(4);
expect(await screen.findByText('b:d/c')).toBeInTheDocument();
expect(await screen.findByText('b:d/c1')).toBeInTheDocument();
expect(await screen.findByText('k:d/a1')).toBeInTheDocument();
expect(await screen.findByText('b:d/c2')).toBeInTheDocument();
expect(await screen.findAllByTestId('node')).toHaveLength(4);
expect(await findAllByText('ownerOf')).toHaveLength(1);
expect(await findAllByText('hasPart')).toHaveLength(2);
expect(await findAllByTestId('label')).toHaveLength(3);
expect(await screen.findAllByText('ownerOf')).toHaveLength(1);
expect(await screen.findAllByText('hasPart')).toHaveLength(2);
expect(await screen.findAllByTestId('label')).toHaveLength(3);
expect(catalog.getEntityByRef).toHaveBeenCalledTimes(4);
});
@@ -265,28 +264,27 @@ describe('<EntityRelationsGraph/>', () => {
test('renders full graph at full depth', async () => {
catalog.getEntityByRef.mockImplementation(async n => entities[n as string]);
const { findAllByText, findByText, findAllByTestId } =
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
unidirectional={false}
mergeRelations={false}
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
/>
</Wrapper>,
);
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
unidirectional={false}
mergeRelations={false}
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
/>
</Wrapper>,
);
expect(await findByText('b:d/c')).toBeInTheDocument();
expect(await findByText('b:d/c1')).toBeInTheDocument();
expect(await findByText('k:d/a1')).toBeInTheDocument();
expect(await findByText('b:d/c2')).toBeInTheDocument();
expect(await findAllByTestId('node')).toHaveLength(4);
expect(await screen.findByText('b:d/c')).toBeInTheDocument();
expect(await screen.findByText('b:d/c1')).toBeInTheDocument();
expect(await screen.findByText('k:d/a1')).toBeInTheDocument();
expect(await screen.findByText('b:d/c2')).toBeInTheDocument();
expect(await screen.findAllByTestId('node')).toHaveLength(4);
expect(await findAllByText('ownerOf')).toHaveLength(2);
expect(await findAllByText('ownedBy')).toHaveLength(2);
expect(await findAllByText('hasPart')).toHaveLength(2);
expect(await findAllByText('partOf')).toHaveLength(2);
expect(await findAllByTestId('label')).toHaveLength(8);
expect(await screen.findAllByText('ownerOf')).toHaveLength(2);
expect(await screen.findAllByText('ownedBy')).toHaveLength(2);
expect(await screen.findAllByText('hasPart')).toHaveLength(2);
expect(await screen.findAllByText('partOf')).toHaveLength(2);
expect(await screen.findAllByTestId('label')).toHaveLength(8);
expect(catalog.getEntityByRef).toHaveBeenCalledTimes(4);
});
@@ -294,26 +292,25 @@ describe('<EntityRelationsGraph/>', () => {
test('renders full graph at full depth with merged relations', async () => {
catalog.getEntityByRef.mockImplementation(async n => entities[n as string]);
const { findAllByText, findByText, findAllByTestId } =
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
unidirectional={false}
mergeRelations
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
/>
</Wrapper>,
);
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
unidirectional={false}
mergeRelations
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
/>
</Wrapper>,
);
expect(await findByText('b:d/c')).toBeInTheDocument();
expect(await findByText('b:d/c1')).toBeInTheDocument();
expect(await findByText('k:d/a1')).toBeInTheDocument();
expect(await findByText('b:d/c2')).toBeInTheDocument();
expect(await findAllByTestId('node')).toHaveLength(4);
expect(await screen.findByText('b:d/c')).toBeInTheDocument();
expect(await screen.findByText('b:d/c1')).toBeInTheDocument();
expect(await screen.findByText('k:d/a1')).toBeInTheDocument();
expect(await screen.findByText('b:d/c2')).toBeInTheDocument();
expect(await screen.findAllByTestId('node')).toHaveLength(4);
expect(await findAllByText('ownerOf')).toHaveLength(2);
expect(await findAllByText('hasPart')).toHaveLength(2);
expect(await findAllByTestId('label')).toHaveLength(4);
expect(await screen.findAllByText('ownerOf')).toHaveLength(2);
expect(await screen.findAllByText('hasPart')).toHaveLength(2);
expect(await screen.findAllByTestId('label')).toHaveLength(4);
expect(catalog.getEntityByRef).toHaveBeenCalledTimes(4);
});
@@ -321,27 +318,26 @@ describe('<EntityRelationsGraph/>', () => {
test('renders a graph with multiple root nodes', async () => {
catalog.getEntityByRef.mockImplementation(async n => entities[n as string]);
const { findAllByText, findByText, findAllByTestId } =
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
rootEntityNames={[
{ kind: 'b', namespace: 'd', name: 'c' },
{ kind: 'b', namespace: 'd', name: 'c2' },
]}
/>
</Wrapper>,
);
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
rootEntityNames={[
{ kind: 'b', namespace: 'd', name: 'c' },
{ kind: 'b', namespace: 'd', name: 'c2' },
]}
/>
</Wrapper>,
);
expect(await findByText('b:d/c')).toBeInTheDocument();
expect(await findByText('b:d/c1')).toBeInTheDocument();
expect(await findByText('k:d/a1')).toBeInTheDocument();
expect(await findByText('b:d/c2')).toBeInTheDocument();
expect(await findAllByTestId('node')).toHaveLength(4);
expect(await screen.findByText('b:d/c')).toBeInTheDocument();
expect(await screen.findByText('b:d/c1')).toBeInTheDocument();
expect(await screen.findByText('k:d/a1')).toBeInTheDocument();
expect(await screen.findByText('b:d/c2')).toBeInTheDocument();
expect(await screen.findAllByTestId('node')).toHaveLength(4);
expect(await findAllByText('ownerOf')).toHaveLength(1);
expect(await findAllByText('partOf')).toHaveLength(2);
expect(await findAllByTestId('label')).toHaveLength(3);
expect(await screen.findAllByText('ownerOf')).toHaveLength(1);
expect(await screen.findAllByText('partOf')).toHaveLength(2);
expect(await screen.findAllByTestId('label')).toHaveLength(3);
expect(catalog.getEntityByRef).toHaveBeenCalledTimes(4);
});
@@ -349,23 +345,22 @@ describe('<EntityRelationsGraph/>', () => {
test('renders a graph with filtered kinds and relations', async () => {
catalog.getEntityByRef.mockImplementation(async n => entities[n as string]);
const { findAllByText, findByText, findAllByTestId } =
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
relations={['ownerOf', 'ownedBy']}
kinds={['k']}
/>
</Wrapper>,
);
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
relations={['ownerOf', 'ownedBy']}
kinds={['k']}
/>
</Wrapper>,
);
expect(await findByText('b:d/c')).toBeInTheDocument();
expect(await findByText('k:d/a1')).toBeInTheDocument();
expect(await findAllByTestId('node')).toHaveLength(2);
expect(await screen.findByText('b:d/c')).toBeInTheDocument();
expect(await screen.findByText('k:d/a1')).toBeInTheDocument();
expect(await screen.findAllByTestId('node')).toHaveLength(2);
expect(await findAllByText('ownerOf')).toHaveLength(1);
expect(await findAllByTestId('label')).toHaveLength(1);
expect(await screen.findAllByText('ownerOf')).toHaveLength(1);
expect(await screen.findAllByTestId('label')).toHaveLength(1);
expect(catalog.getEntityByRef).toHaveBeenCalledTimes(2);
});
@@ -374,7 +369,7 @@ describe('<EntityRelationsGraph/>', () => {
catalog.getEntityByRef.mockImplementation(async n => entities[n as string]);
const onNodeClick = jest.fn();
const { findByText } = await renderInTestApp(
await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
@@ -383,7 +378,7 @@ describe('<EntityRelationsGraph/>', () => {
</Wrapper>,
);
await userEvent.click(await findByText('k:d/a1'));
await userEvent.click(await screen.findByText('k:d/a1'));
expect(onNodeClick).toHaveBeenCalledTimes(1);
});
@@ -397,7 +392,7 @@ describe('<EntityRelationsGraph/>', () => {
</g>
);
const { findAllByTestId, container } = await renderInTestApp(
const { container } = await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
@@ -406,7 +401,7 @@ describe('<EntityRelationsGraph/>', () => {
</Wrapper>,
);
const node = await findAllByTestId(CUSTOM_TEST_ID);
const node = await screen.findAllByTestId(CUSTOM_TEST_ID);
expect(node[0]).toBeInTheDocument();
expect(container.querySelector('circle')).toBeInTheDocument();
});
@@ -421,7 +416,7 @@ describe('<EntityRelationsGraph/>', () => {
</g>
);
const { findAllByTestId, findAllByText, container } = await renderInTestApp(
const { container } = await renderInTestApp(
<Wrapper>
<EntityRelationsGraph
rootEntityNames={{ kind: 'b', namespace: 'd', name: 'c' }}
@@ -429,10 +424,10 @@ describe('<EntityRelationsGraph/>', () => {
/>
</Wrapper>,
);
const node = await findAllByTestId(CUSTOM_TEST_ID);
const node = await screen.findAllByTestId(CUSTOM_TEST_ID);
expect(node[0]).toBeInTheDocument();
expect(container.querySelector('circle')).toBeInTheDocument();
const labels = await findAllByText('Test-Labelvisible');
const labels = await screen.findAllByText('Test-Labelvisible');
expect(labels[0]).toBeInTheDocument();
});
});
+5 -1
View File
@@ -1 +1,5 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
rules: {
'testing-library/prefer-screen-queries': 'error',
},
});
@@ -19,6 +19,7 @@ import { ApiProvider, ConfigReader } from '@backstage/core-app-api';
import { configApiRef } from '@backstage/core-plugin-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { catalogImportApiRef, CatalogImportClient } from '../../api';
import { DefaultImportPage } from './DefaultImportPage';
@@ -65,14 +66,14 @@ describe('<DefaultImportPage />', () => {
});
it('renders without exploding', async () => {
const { getByText } = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={apis}>
<DefaultImportPage />
</ApiProvider>,
);
expect(
getByText('Start tracking your component in Backstage'),
screen.getByText('Start tracking your component in Backstage'),
).toBeInTheDocument();
});
});
@@ -21,6 +21,7 @@ import {
TestApiProvider,
TestApiRegistry,
} from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { CatalogImportApi, catalogImportApiRef } from '../../api';
import { ImportInfoCard } from './ImportInfoCard';
@@ -49,7 +50,7 @@ describe('<ImportInfoCard />', () => {
});
it('renders without exploding', async () => {
const { getByText } = await renderInTestApp(
await renderInTestApp(
<TestApiProvider
apis={[
[configApiRef, new ConfigReader({ integrations: {} })],
@@ -60,32 +61,34 @@ describe('<ImportInfoCard />', () => {
</TestApiProvider>,
);
expect(getByText('Register an existing component')).toBeInTheDocument();
expect(
screen.getByText('Register an existing component'),
).toBeInTheDocument();
});
it('renders section on GitHub discovery if supported', async () => {
catalogImportApi.preparePullRequest = async () => ({ title: '', body: '' });
const { getByText } = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={apis}>
<ImportInfoCard />
</ApiProvider>,
);
expect(getByText(/The wizard discovers all/)).toBeInTheDocument();
expect(screen.getByText(/The wizard discovers all/)).toBeInTheDocument();
});
it('renders section on pull requests if supported', async () => {
catalogImportApi.preparePullRequest = async () => ({ title: '', body: '' });
const { getByText } = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={apis}>
<ImportInfoCard />
</ApiProvider>,
);
expect(
getByText(/the wizard will prepare a Pull Request/),
screen.getByText(/the wizard will prepare a Pull Request/),
).toBeInTheDocument();
});
});
@@ -19,6 +19,7 @@ import { ApiProvider, ConfigReader } from '@backstage/core-app-api';
import { configApiRef } from '@backstage/core-plugin-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { useOutlet } from 'react-router';
import { catalogImportApiRef, CatalogImportClient } from '../../api';
@@ -71,26 +72,26 @@ describe('<ImportPage />', () => {
afterEach(() => jest.resetAllMocks());
it('renders without exploding', async () => {
const { getByText } = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={apis}>
<ImportPage />
</ApiProvider>,
);
expect(
getByText('Start tracking your component in Backstage'),
screen.getByText('Start tracking your component in Backstage'),
).toBeInTheDocument();
});
it('renders with custom children', async () => {
(useOutlet as jest.Mock).mockReturnValue(<div>Hello World</div>);
const { getByText } = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={apis}>
<ImportPage />
</ApiProvider>,
);
expect(getByText('Hello World')).toBeInTheDocument();
expect(screen.getByText('Hello World')).toBeInTheDocument();
});
});
@@ -16,7 +16,7 @@
import { errorApiRef } from '@backstage/core-plugin-api';
import { TestApiProvider } from '@backstage/test-utils';
import { act, render } from '@testing-library/react';
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { AnalyzeResult, catalogImportApiRef } from '../../api/';
@@ -60,19 +60,20 @@ describe('<StepInitAnalyzeUrl />', () => {
});
it('renders without exploding', async () => {
const { getByRole } = render(
<StepInitAnalyzeUrl onAnalysis={() => undefined} />,
{
wrapper: Wrapper,
},
);
render(<StepInitAnalyzeUrl onAnalysis={() => undefined} />, {
wrapper: Wrapper,
});
expect(getByRole('textbox', { name: /Repository/i })).toBeInTheDocument();
expect(getByRole('textbox', { name: /Repository/i })).toHaveValue('');
expect(
screen.getByRole('textbox', { name: /Repository/i }),
).toBeInTheDocument();
expect(screen.getByRole('textbox', { name: /Repository/i })).toHaveValue(
'',
);
});
it('should use default analysis url', async () => {
const { getByRole } = render(
render(
<StepInitAnalyzeUrl
onAnalysis={() => undefined}
analysisUrl="https://default"
@@ -82,8 +83,10 @@ describe('<StepInitAnalyzeUrl />', () => {
},
);
expect(getByRole('textbox', { name: /Repository/i })).toBeInTheDocument();
expect(getByRole('textbox', { name: /Repository/i })).toHaveValue(
expect(
screen.getByRole('textbox', { name: /Repository/i }),
).toBeInTheDocument();
expect(screen.getByRole('textbox', { name: /Repository/i })).toHaveValue(
'https://default',
);
});
@@ -91,16 +94,13 @@ describe('<StepInitAnalyzeUrl />', () => {
it('should not analyze without url', async () => {
const onAnalysisFn = jest.fn();
const { getByRole } = render(
<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />,
{
wrapper: Wrapper,
},
);
render(<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />, {
wrapper: Wrapper,
});
await act(async () => {
try {
await userEvent.click(getByRole('button', { name: /Analyze/i }));
await userEvent.click(screen.getByRole('button', { name: /Analyze/i }));
} catch {
return;
}
@@ -114,26 +114,23 @@ describe('<StepInitAnalyzeUrl />', () => {
it('should not analyze invalid value', async () => {
const onAnalysisFn = jest.fn();
const { getByRole, getByText } = render(
<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />,
{
wrapper: Wrapper,
},
);
render(<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />, {
wrapper: Wrapper,
});
await act(async () => {
await userEvent.type(
getByRole('textbox', { name: /Repository/i }),
screen.getByRole('textbox', { name: /Repository/i }),
'http:/',
);
await userEvent.click(getByRole('button', { name: /Analyze/i }));
await userEvent.click(screen.getByRole('button', { name: /Analyze/i }));
});
expect(catalogImportApi.analyzeUrl).toHaveBeenCalledTimes(0);
expect(onAnalysisFn).toHaveBeenCalledTimes(0);
expect(errorApi.post).toHaveBeenCalledTimes(0);
expect(
getByText('Must start with http:// or https://.'),
screen.getByText('Must start with http:// or https://.'),
).toBeInTheDocument();
});
@@ -145,12 +142,9 @@ describe('<StepInitAnalyzeUrl />', () => {
locations: [location],
} as AnalyzeResult;
const { getByRole } = render(
<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />,
{
wrapper: Wrapper,
},
);
render(<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />, {
wrapper: Wrapper,
});
catalogImportApi.analyzeUrl.mockReturnValueOnce(
Promise.resolve(analyzeResult),
@@ -158,10 +152,10 @@ describe('<StepInitAnalyzeUrl />', () => {
await act(async () => {
await userEvent.type(
getByRole('textbox', { name: /Repository/i }),
screen.getByRole('textbox', { name: /Repository/i }),
'https://my-repository',
);
await userEvent.click(getByRole('button', { name: /Analyze/i }));
await userEvent.click(screen.getByRole('button', { name: /Analyze/i }));
});
expect(onAnalysisFn).toHaveBeenCalledTimes(1);
@@ -182,12 +176,9 @@ describe('<StepInitAnalyzeUrl />', () => {
locations: [location, location],
} as AnalyzeResult;
const { getByRole } = render(
<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />,
{
wrapper: Wrapper,
},
);
render(<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />, {
wrapper: Wrapper,
});
catalogImportApi.analyzeUrl.mockReturnValueOnce(
Promise.resolve(analyzeResult),
@@ -195,10 +186,10 @@ describe('<StepInitAnalyzeUrl />', () => {
await act(async () => {
await userEvent.type(
getByRole('textbox', { name: /Repository/i }),
screen.getByRole('textbox', { name: /Repository/i }),
'https://my-repository-1',
);
await userEvent.click(getByRole('button', { name: /Analyze/i }));
await userEvent.click(screen.getByRole('button', { name: /Analyze/i }));
});
expect(onAnalysisFn).toHaveBeenCalledTimes(1);
@@ -218,12 +209,9 @@ describe('<StepInitAnalyzeUrl />', () => {
locations: [],
} as AnalyzeResult;
const { getByRole, getByText } = render(
<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />,
{
wrapper: Wrapper,
},
);
render(<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />, {
wrapper: Wrapper,
});
catalogImportApi.analyzeUrl.mockReturnValueOnce(
Promise.resolve(analyzeResult),
@@ -231,15 +219,15 @@ describe('<StepInitAnalyzeUrl />', () => {
await act(async () => {
await userEvent.type(
getByRole('textbox', { name: /Repository/i }),
screen.getByRole('textbox', { name: /Repository/i }),
'https://my-repository-1',
);
await userEvent.click(getByRole('button', { name: /Analyze/i }));
await userEvent.click(screen.getByRole('button', { name: /Analyze/i }));
});
expect(onAnalysisFn).toHaveBeenCalledTimes(0);
expect(
getByText('There are no entities at this location'),
screen.getByText('There are no entities at this location'),
).toBeInTheDocument();
expect(errorApi.post).toHaveBeenCalledTimes(0);
});
@@ -262,12 +250,9 @@ describe('<StepInitAnalyzeUrl />', () => {
],
} as AnalyzeResult;
const { getByRole } = render(
<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />,
{
wrapper: Wrapper,
},
);
render(<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />, {
wrapper: Wrapper,
});
catalogImportApi.analyzeUrl.mockReturnValueOnce(
Promise.resolve(analyzeResult),
@@ -275,10 +260,10 @@ describe('<StepInitAnalyzeUrl />', () => {
await act(async () => {
await userEvent.type(
getByRole('textbox', { name: /Repository/i }),
screen.getByRole('textbox', { name: /Repository/i }),
'https://my-repository-2',
);
await userEvent.click(getByRole('button', { name: /Analyze/i }));
await userEvent.click(screen.getByRole('button', { name: /Analyze/i }));
});
expect(onAnalysisFn).toHaveBeenCalledTimes(1);
@@ -300,12 +285,9 @@ describe('<StepInitAnalyzeUrl />', () => {
generatedEntities: [],
} as AnalyzeResult;
const { getByRole, getByText } = render(
<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />,
{
wrapper: Wrapper,
},
);
render(<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />, {
wrapper: Wrapper,
});
catalogImportApi.analyzeUrl.mockReturnValueOnce(
Promise.resolve(analyzeResult),
@@ -313,15 +295,15 @@ describe('<StepInitAnalyzeUrl />', () => {
await act(async () => {
await userEvent.type(
getByRole('textbox', { name: /Repository/i }),
screen.getByRole('textbox', { name: /Repository/i }),
'https://my-repository-2',
);
await userEvent.click(getByRole('button', { name: /Analyze/i }));
await userEvent.click(screen.getByRole('button', { name: /Analyze/i }));
});
expect(onAnalysisFn).toHaveBeenCalledTimes(0);
expect(
getByText("Couldn't generate entities for your repository"),
screen.getByText("Couldn't generate entities for your repository"),
).toBeInTheDocument();
expect(errorApi.post).toHaveBeenCalledTimes(0);
});
@@ -344,7 +326,7 @@ describe('<StepInitAnalyzeUrl />', () => {
],
} as AnalyzeResult;
const { getByRole, getByText } = render(
render(
<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} disablePullRequest />,
{
wrapper: Wrapper,
@@ -357,15 +339,15 @@ describe('<StepInitAnalyzeUrl />', () => {
await act(async () => {
await userEvent.type(
getByRole('textbox', { name: /Repository/i }),
screen.getByRole('textbox', { name: /Repository/i }),
'https://my-repository-2',
);
await userEvent.click(getByRole('button', { name: /Analyze/i }));
await userEvent.click(screen.getByRole('button', { name: /Analyze/i }));
});
expect(onAnalysisFn).toHaveBeenCalledTimes(0);
expect(
getByText("Couldn't generate entities for your repository"),
screen.getByText("Couldn't generate entities for your repository"),
).toBeInTheDocument();
expect(errorApi.post).toHaveBeenCalledTimes(0);
});
@@ -373,12 +355,9 @@ describe('<StepInitAnalyzeUrl />', () => {
it('should report unknown type to the errorapi', async () => {
const onAnalysisFn = jest.fn();
const { getByRole, getByText } = render(
<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />,
{
wrapper: Wrapper,
},
);
render(<StepInitAnalyzeUrl onAnalysis={onAnalysisFn} />, {
wrapper: Wrapper,
});
catalogImportApi.analyzeUrl.mockReturnValueOnce(
Promise.resolve({ type: 'unknown' } as any as AnalyzeResult),
@@ -386,15 +365,15 @@ describe('<StepInitAnalyzeUrl />', () => {
await act(async () => {
await userEvent.type(
getByRole('textbox', { name: /Repository/i }),
screen.getByRole('textbox', { name: /Repository/i }),
'https://my-repository-2',
);
await userEvent.click(getByRole('button', { name: /Analyze/i }));
await userEvent.click(screen.getByRole('button', { name: /Analyze/i }));
});
expect(onAnalysisFn).toHaveBeenCalledTimes(0);
expect(
getByText(
screen.getByText(
'Received unknown analysis result of type unknown. Please contact the support team.',
),
).toBeInTheDocument();
@@ -15,7 +15,7 @@
*/
import { FormHelperText, TextField } from '@material-ui/core';
import { act, render } from '@testing-library/react';
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { asInputRef } from '../helpers';
@@ -25,7 +25,7 @@ describe('<PreparePullRequestForm />', () => {
it('renders without exploding', async () => {
const onSubmitFn = jest.fn();
const { getByRole } = render(
render(
<PreparePullRequestForm<{ main: string }>
defaultValues={{ main: 'default' }}
render={({ register }) => (
@@ -39,7 +39,7 @@ describe('<PreparePullRequestForm />', () => {
);
await act(async () => {
await userEvent.click(getByRole('button', { name: /submit/i }));
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
});
expect(onSubmitFn).toHaveBeenCalledTimes(1);
@@ -49,7 +49,7 @@ describe('<PreparePullRequestForm />', () => {
it('should register a text field', async () => {
const onSubmitFn = jest.fn();
const { getByRole, getByLabelText } = render(
render(
<PreparePullRequestForm<{ main: string }>
defaultValues={{ main: 'default' }}
render={({ register }) => (
@@ -67,9 +67,9 @@ describe('<PreparePullRequestForm />', () => {
);
await act(async () => {
await userEvent.clear(getByLabelText('Main Field'));
await userEvent.type(getByLabelText('Main Field'), 'My Text');
await userEvent.click(getByRole('button', { name: /submit/i }));
await userEvent.clear(screen.getByLabelText('Main Field'));
await userEvent.type(screen.getByLabelText('Main Field'), 'My Text');
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
});
expect(onSubmitFn).toHaveBeenCalledTimes(1);
@@ -79,7 +79,7 @@ describe('<PreparePullRequestForm />', () => {
it('registers required attribute', async () => {
const onSubmitFn = jest.fn();
const { queryByText, getByRole } = render(
render(
<PreparePullRequestForm<{ main: string }>
defaultValues={{}}
render={({ formState, register }) => (
@@ -100,13 +100,17 @@ describe('<PreparePullRequestForm />', () => {
/>,
);
expect(queryByText('Error in required main field')).not.toBeInTheDocument();
expect(
screen.queryByText('Error in required main field'),
).not.toBeInTheDocument();
await act(async () => {
await userEvent.click(getByRole('button', { name: /submit/i }));
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
});
expect(onSubmitFn).not.toHaveBeenCalled();
expect(queryByText('Error in required main field')).toBeInTheDocument();
expect(
screen.queryByText('Error in required main field'),
).toBeInTheDocument();
});
});
@@ -15,7 +15,7 @@
*/
import { makeStyles } from '@material-ui/core';
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import React from 'react';
import { PreviewPullRequestComponent } from './PreviewPullRequestComponent';
@@ -28,15 +28,15 @@ const useStyles = makeStyles({
describe('<PreviewPullRequestComponent />', () => {
it('renders without exploding', async () => {
const { getByText } = render(
render(
<PreviewPullRequestComponent
title="My Title"
description="My **description**"
/>,
);
const title = getByText('My Title');
const description = getByText('description', { selector: 'strong' });
const title = screen.getByText('My Title');
const description = screen.getByText('description', { selector: 'strong' });
expect(title).toBeInTheDocument();
expect(title).toBeVisible();
expect(description).toBeInTheDocument();
@@ -46,7 +46,7 @@ describe('<PreviewPullRequestComponent />', () => {
it('renders card with custom styles', async () => {
const { result } = renderHook(() => useStyles());
const { getByText } = render(
render(
<PreviewPullRequestComponent
title="My Title"
description="My **description**"
@@ -54,8 +54,8 @@ describe('<PreviewPullRequestComponent />', () => {
/>,
);
const title = getByText('My Title');
const description = getByText('description', { selector: 'strong' });
const title = screen.getByText('My Title');
const description = screen.getByText('description', { selector: 'strong' });
expect(title).toBeInTheDocument();
expect(title).not.toBeVisible();
expect(description).toBeInTheDocument();
@@ -65,7 +65,7 @@ describe('<PreviewPullRequestComponent />', () => {
it('renders with custom styles', async () => {
const { result } = renderHook(() => useStyles());
const { getByText } = render(
render(
<PreviewPullRequestComponent
title="My Title"
description="My **description**"
@@ -73,8 +73,8 @@ describe('<PreviewPullRequestComponent />', () => {
/>,
);
const title = getByText('My Title');
const description = getByText('description', { selector: 'strong' });
const title = screen.getByText('My Title');
const description = screen.getByText('description', { selector: 'strong' });
expect(title).toBeInTheDocument();
expect(title).toBeVisible();
expect(description).toBeInTheDocument();
@@ -102,7 +102,7 @@ describe('<StepPrepareCreatePullRequest />', () => {
catalogApi.getEntities.mockReturnValue(Promise.resolve({ items: [] }));
await act(async () => {
const { findByText } = render(
render(
<StepPrepareCreatePullRequest
analyzeResult={analyzeResult}
onPrepare={onPrepareFn}
@@ -122,8 +122,10 @@ describe('<StepPrepareCreatePullRequest />', () => {
},
);
const title = await findByText('My title');
const description = await findByText('body', { selector: 'strong' });
const title = await screen.findByText('My title');
const description = await screen.findByText('body', {
selector: 'strong',
});
expect(title).toBeInTheDocument();
expect(title).toBeVisible();
expect(description).toBeInTheDocument();
@@ -15,7 +15,7 @@
*/
import { renderInTestApp } from '@backstage/test-utils';
import { act } from '@testing-library/react';
import { act, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { AnalyzeResult } from '../../api';
@@ -58,7 +58,7 @@ describe('<StepPrepareSelectLocations />', () => {
});
it('renders display locations to be added', async () => {
const rendered = await renderInTestApp(
await renderInTestApp(
<StepPrepareSelectLocations
analyzeResult={analyzeResult}
onPrepare={() => undefined}
@@ -66,15 +66,15 @@ describe('<StepPrepareSelectLocations />', () => {
/>,
);
expect(rendered.getByText('url-1')).toBeInTheDocument();
expect(rendered.getByText('url-2')).toBeInTheDocument();
expect(screen.getByText('url-1')).toBeInTheDocument();
expect(screen.getByText('url-2')).toBeInTheDocument();
expect(
rendered.queryByText(/Select one or more locations/),
screen.queryByText(/Select one or more locations/),
).toBeInTheDocument();
expect(
rendered.queryByText(/locations already exist/),
screen.queryByText(/locations already exist/),
).not.toBeInTheDocument();
expect(rendered.getByRole('button', { name: /Review/i })).toBeDisabled();
expect(screen.getByRole('button', { name: /Review/i })).toBeDisabled();
});
it('should display existing locations only', async () => {
@@ -95,7 +95,7 @@ describe('<StepPrepareSelectLocations />', () => {
],
} as Extract<AnalyzeResult, { type: 'locations' }>;
const rendered = await renderInTestApp(
await renderInTestApp(
<StepPrepareSelectLocations
analyzeResult={analyzeResultWithExistingLocation}
onPrepare={() => undefined}
@@ -103,15 +103,15 @@ describe('<StepPrepareSelectLocations />', () => {
/>,
);
expect(rendered.getByText(/my-target/)).toBeInTheDocument();
expect(rendered.queryByText(/locations already exist/)).toBeInTheDocument();
expect(screen.getByText(/my-target/)).toBeInTheDocument();
expect(screen.queryByText(/locations already exist/)).toBeInTheDocument();
expect(
rendered.queryByText(/Select one or more locations/),
screen.queryByText(/Select one or more locations/),
).not.toBeInTheDocument();
});
it('should select and deselect all', async () => {
const { getByRole, getAllByRole } = await renderInTestApp(
await renderInTestApp(
<StepPrepareSelectLocations
analyzeResult={analyzeResult}
onPrepare={() => undefined}
@@ -119,27 +119,31 @@ describe('<StepPrepareSelectLocations />', () => {
/>,
);
const checkboxes = getAllByRole('checkbox');
const checkboxes = screen.getAllByRole('checkbox');
checkboxes.forEach(c => expect(c).not.toBeChecked());
expect(getByRole('button', { name: /Review/i })).toBeDisabled();
expect(screen.getByRole('button', { name: /Review/i })).toBeDisabled();
await act(async () => {
await userEvent.click(getByRole('button', { name: /Select All/i }));
await userEvent.click(
screen.getByRole('button', { name: /Select All/i }),
);
});
checkboxes.forEach(c => expect(c).toBeChecked());
expect(getByRole('button', { name: /Review/i })).not.toBeDisabled();
expect(screen.getByRole('button', { name: /Review/i })).not.toBeDisabled();
await act(async () => {
await userEvent.click(getByRole('button', { name: /Select All/i }));
await userEvent.click(
screen.getByRole('button', { name: /Select All/i }),
);
});
checkboxes.forEach(c => expect(c).not.toBeChecked());
expect(getByRole('button', { name: /Review/i })).toBeDisabled();
expect(screen.getByRole('button', { name: /Review/i })).toBeDisabled();
});
it('should preselect prepared locations', async () => {
const { getAllByRole } = await renderInTestApp(
await renderInTestApp(
<StepPrepareSelectLocations
analyzeResult={analyzeResult}
prepareResult={{
@@ -151,7 +155,7 @@ describe('<StepPrepareSelectLocations />', () => {
/>,
);
const checkboxes = getAllByRole('checkbox');
const checkboxes = screen.getAllByRole('checkbox');
expect(checkboxes[0]).not.toBeChecked();
expect(checkboxes[1]).toBeChecked();
@@ -159,7 +163,7 @@ describe('<StepPrepareSelectLocations />', () => {
});
it('should select items', async () => {
const { getAllByRole } = await renderInTestApp(
await renderInTestApp(
<StepPrepareSelectLocations
analyzeResult={analyzeResult}
onPrepare={() => undefined}
@@ -167,7 +171,7 @@ describe('<StepPrepareSelectLocations />', () => {
/>,
);
const checkboxes = getAllByRole('checkbox');
const checkboxes = screen.getAllByRole('checkbox');
checkboxes.forEach(c => expect(c).not.toBeChecked());
await act(async () => {
@@ -188,7 +192,7 @@ describe('<StepPrepareSelectLocations />', () => {
it('should go back', async () => {
const onGoBack = jest.fn();
const { getByRole } = await renderInTestApp(
await renderInTestApp(
<StepPrepareSelectLocations
analyzeResult={analyzeResult}
onPrepare={() => undefined}
@@ -197,7 +201,7 @@ describe('<StepPrepareSelectLocations />', () => {
);
await act(async () => {
await userEvent.click(getByRole('button', { name: /Back/i }));
await userEvent.click(screen.getByRole('button', { name: /Back/i }));
});
expect(onGoBack).toHaveBeenCalledTimes(1);
@@ -206,7 +210,7 @@ describe('<StepPrepareSelectLocations />', () => {
it('should submit', async () => {
const onPrepare = jest.fn();
const { getAllByRole, getByRole } = await renderInTestApp(
await renderInTestApp(
<StepPrepareSelectLocations
analyzeResult={analyzeResult}
onPrepare={onPrepare}
@@ -214,14 +218,14 @@ describe('<StepPrepareSelectLocations />', () => {
/>,
);
const checkboxes = getAllByRole('checkbox');
const checkboxes = screen.getAllByRole('checkbox');
await act(async () => {
await userEvent.click(checkboxes[1]);
});
await act(async () => {
await userEvent.click(getByRole('button', { name: /Review/i }));
await userEvent.click(screen.getByRole('button', { name: /Review/i }));
});
expect(onPrepare).toHaveBeenCalledTimes(1);
+5 -1
View File
@@ -1 +1,5 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
rules: {
'testing-library/prefer-screen-queries': 'error',
},
});
@@ -19,7 +19,7 @@ import { Entity } from '@backstage/catalog-model';
import { ApiProvider } from '@backstage/core-app-api';
import { alertApiRef } from '@backstage/core-plugin-api';
import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils';
import { fireEvent, waitFor } from '@testing-library/react';
import { fireEvent, waitFor, screen } from '@testing-library/react';
import { capitalize } from 'lodash';
import { default as React } from 'react';
import { catalogApiRef } from '../../api';
@@ -75,7 +75,7 @@ describe('<EntityKindPicker/>', () => {
);
it('renders available entity kinds', async () => {
const rendered = await renderWithEffects(
await renderWithEffects(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{ filters: { kind: new EntityKindFilter('component') } }}
@@ -84,16 +84,16 @@ describe('<EntityKindPicker/>', () => {
</MockEntityListContextProvider>
</ApiProvider>,
);
expect(rendered.getByText('Kind')).toBeInTheDocument();
expect(screen.getByText('Kind')).toBeInTheDocument();
const input = rendered.getByTestId('select');
const input = screen.getByTestId('select');
fireEvent.click(input);
await waitFor(() => rendered.getByText('Domain'));
await waitFor(() => screen.getByText('Domain'));
entities.forEach(entity => {
expect(
rendered.getByRole('option', {
screen.getByRole('option', {
name: capitalize(entity.kind as string),
}),
).toBeInTheDocument();
@@ -102,7 +102,7 @@ describe('<EntityKindPicker/>', () => {
it('sets the selected kind filter', async () => {
const updateFilters = jest.fn();
const rendered = await renderWithEffects(
await renderWithEffects(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{
@@ -114,11 +114,11 @@ describe('<EntityKindPicker/>', () => {
</MockEntityListContextProvider>
</ApiProvider>,
);
const input = rendered.getByTestId('select');
const input = screen.getByTestId('select');
fireEvent.click(input);
await waitFor(() => rendered.getByText('Domain'));
fireEvent.click(rendered.getByText('Domain'));
await waitFor(() => screen.getByText('Domain'));
fireEvent.click(screen.getByText('Domain'));
expect(updateFilters).toHaveBeenLastCalledWith({
kind: new EntityKindFilter('domain'),
@@ -15,7 +15,7 @@
*/
import { Entity } from '@backstage/catalog-model';
import { fireEvent, render } from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { MockEntityListContextProvider } from '../../testUtils/providers';
import { EntityLifecycleFilter } from '../../filters';
@@ -56,36 +56,36 @@ const sampleEntities: Entity[] = [
describe('<EntityLifecyclePicker/>', () => {
it('renders all lifecycles', () => {
const rendered = render(
render(
<MockEntityListContextProvider
value={{ entities: sampleEntities, backendEntities: sampleEntities }}
>
<EntityLifecyclePicker />
</MockEntityListContextProvider>,
);
expect(rendered.getByText('Lifecycle')).toBeInTheDocument();
expect(screen.getByText('Lifecycle')).toBeInTheDocument();
fireEvent.click(rendered.getByTestId('lifecycle-picker-expand'));
fireEvent.click(screen.getByTestId('lifecycle-picker-expand'));
sampleEntities
.map(e => e.spec?.lifecycle!)
.forEach(lifecycle => {
expect(rendered.getByText(lifecycle as string)).toBeInTheDocument();
expect(screen.getByText(lifecycle as string)).toBeInTheDocument();
});
});
it('renders unique lifecycles in alphabetical order', () => {
const rendered = render(
render(
<MockEntityListContextProvider
value={{ entities: sampleEntities, backendEntities: sampleEntities }}
>
<EntityLifecyclePicker />
</MockEntityListContextProvider>,
);
expect(rendered.getByText('Lifecycle')).toBeInTheDocument();
expect(screen.getByText('Lifecycle')).toBeInTheDocument();
fireEvent.click(rendered.getByTestId('lifecycle-picker-expand'));
fireEvent.click(screen.getByTestId('lifecycle-picker-expand'));
expect(rendered.getAllByRole('option').map(o => o.textContent)).toEqual([
expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([
'experimental',
'production',
]);
@@ -114,7 +114,7 @@ describe('<EntityLifecyclePicker/>', () => {
it('adds lifecycles to filters', () => {
const updateFilters = jest.fn();
const rendered = render(
render(
<MockEntityListContextProvider
value={{
entities: sampleEntities,
@@ -129,8 +129,8 @@ describe('<EntityLifecyclePicker/>', () => {
lifecycles: undefined,
});
fireEvent.click(rendered.getByTestId('lifecycle-picker-expand'));
fireEvent.click(rendered.getByText('production'));
fireEvent.click(screen.getByTestId('lifecycle-picker-expand'));
fireEvent.click(screen.getByText('production'));
expect(updateFilters).toHaveBeenLastCalledWith({
lifecycles: new EntityLifecycleFilter(['production']),
});
@@ -138,7 +138,7 @@ describe('<EntityLifecyclePicker/>', () => {
it('removes lifecycles from filters', () => {
const updateFilters = jest.fn();
const rendered = render(
render(
<MockEntityListContextProvider
value={{
entities: sampleEntities,
@@ -153,10 +153,10 @@ describe('<EntityLifecyclePicker/>', () => {
expect(updateFilters).toHaveBeenLastCalledWith({
lifecycles: new EntityLifecycleFilter(['production']),
});
fireEvent.click(rendered.getByTestId('lifecycle-picker-expand'));
expect(rendered.getByLabelText('production')).toBeChecked();
fireEvent.click(screen.getByTestId('lifecycle-picker-expand'));
expect(screen.getByLabelText('production')).toBeChecked();
fireEvent.click(rendered.getByLabelText('production'));
fireEvent.click(screen.getByLabelText('production'));
expect(updateFilters).toHaveBeenLastCalledWith({
lifecycles: undefined,
});
@@ -15,7 +15,7 @@
*/
import { Entity, parseEntityRef } from '@backstage/catalog-model';
import { fireEvent, render } from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { MockEntityListContextProvider } from '../../testUtils/providers';
import { EntityOwnerFilter } from '../../filters';
@@ -69,36 +69,36 @@ const sampleEntities: Entity[] = [
describe('<EntityOwnerPicker/>', () => {
it('renders all owners', () => {
const rendered = render(
render(
<MockEntityListContextProvider
value={{ entities: sampleEntities, backendEntities: sampleEntities }}
>
<EntityOwnerPicker />
</MockEntityListContextProvider>,
);
expect(rendered.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner')).toBeInTheDocument();
fireEvent.click(rendered.getByTestId('owner-picker-expand'));
fireEvent.click(screen.getByTestId('owner-picker-expand'));
sampleEntities
.flatMap(e => e.relations?.map(r => parseEntityRef(r.targetRef).name))
.forEach(owner => {
expect(rendered.getByText(owner as string)).toBeInTheDocument();
expect(screen.getByText(owner as string)).toBeInTheDocument();
});
});
it('renders unique owners in alphabetical order', () => {
const rendered = render(
render(
<MockEntityListContextProvider
value={{ entities: sampleEntities, backendEntities: sampleEntities }}
>
<EntityOwnerPicker />
</MockEntityListContextProvider>,
);
expect(rendered.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner')).toBeInTheDocument();
fireEvent.click(rendered.getByTestId('owner-picker-expand'));
fireEvent.click(screen.getByTestId('owner-picker-expand'));
expect(rendered.getAllByRole('option').map(o => o.textContent)).toEqual([
expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([
'another-owner',
'some-owner',
'some-owner-2',
@@ -128,7 +128,7 @@ describe('<EntityOwnerPicker/>', () => {
it('adds owners to filters', () => {
const updateFilters = jest.fn();
const rendered = render(
render(
<MockEntityListContextProvider
value={{
entities: sampleEntities,
@@ -143,8 +143,8 @@ describe('<EntityOwnerPicker/>', () => {
owners: undefined,
});
fireEvent.click(rendered.getByTestId('owner-picker-expand'));
fireEvent.click(rendered.getByText('some-owner'));
fireEvent.click(screen.getByTestId('owner-picker-expand'));
fireEvent.click(screen.getByText('some-owner'));
expect(updateFilters).toHaveBeenLastCalledWith({
owners: new EntityOwnerFilter(['some-owner']),
});
@@ -152,7 +152,7 @@ describe('<EntityOwnerPicker/>', () => {
it('removes owners from filters', () => {
const updateFilters = jest.fn();
const rendered = render(
render(
<MockEntityListContextProvider
value={{
entities: sampleEntities,
@@ -167,10 +167,10 @@ describe('<EntityOwnerPicker/>', () => {
expect(updateFilters).toHaveBeenLastCalledWith({
owners: new EntityOwnerFilter(['some-owner']),
});
fireEvent.click(rendered.getByTestId('owner-picker-expand'));
expect(rendered.getByLabelText('some-owner')).toBeChecked();
fireEvent.click(screen.getByTestId('owner-picker-expand'));
expect(screen.getByLabelText('some-owner')).toBeChecked();
fireEvent.click(rendered.getByLabelText('some-owner'));
fireEvent.click(screen.getByLabelText('some-owner'));
expect(updateFilters).toHaveBeenLastCalledWith({
owner: undefined,
});
@@ -15,7 +15,7 @@
*/
import { Entity } from '@backstage/catalog-model';
import { fireEvent, render } from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { EntityErrorFilter, EntityOrphanFilter } from '../../filters';
import { MockEntityListContextProvider } from '../../testUtils/providers';
@@ -52,23 +52,23 @@ const sampleEntities: Entity[] = [
describe('<EntityProcessingStatusPicker/>', () => {
it('renders all processing status options', () => {
const rendered = render(
render(
<MockEntityListContextProvider
value={{ entities: sampleEntities, backendEntities: sampleEntities }}
>
<EntityProcessingStatusPicker />
</MockEntityListContextProvider>,
);
expect(rendered.getByText('Processing Status')).toBeInTheDocument();
expect(screen.getByText('Processing Status')).toBeInTheDocument();
fireEvent.click(rendered.getByTestId('processing-status-picker-expand'));
expect(rendered.getByText('Is Orphan')).toBeInTheDocument();
expect(rendered.getByText('Has Error')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('processing-status-picker-expand'));
expect(screen.getByText('Is Orphan')).toBeInTheDocument();
expect(screen.getByText('Has Error')).toBeInTheDocument();
});
it('adds orphan to orphan filter', () => {
const updateFilters = jest.fn();
const rendered = render(
render(
<MockEntityListContextProvider
value={{
entities: sampleEntities,
@@ -80,8 +80,8 @@ describe('<EntityProcessingStatusPicker/>', () => {
</MockEntityListContextProvider>,
);
fireEvent.click(rendered.getByTestId('processing-status-picker-expand'));
fireEvent.click(rendered.getByText('Is Orphan'));
fireEvent.click(screen.getByTestId('processing-status-picker-expand'));
fireEvent.click(screen.getByText('Is Orphan'));
expect(updateFilters).toHaveBeenCalledWith({
orphan: new EntityOrphanFilter(true),
});
@@ -89,7 +89,7 @@ describe('<EntityProcessingStatusPicker/>', () => {
it('adds error to error filter', () => {
const updateFilters = jest.fn();
const rendered = render(
render(
<MockEntityListContextProvider
value={{
entities: sampleEntities,
@@ -101,8 +101,8 @@ describe('<EntityProcessingStatusPicker/>', () => {
</MockEntityListContextProvider>,
);
fireEvent.click(rendered.getByTestId('processing-status-picker-expand'));
fireEvent.click(rendered.getByText('Has Error'));
fireEvent.click(screen.getByTestId('processing-status-picker-expand'));
fireEvent.click(screen.getByText('Has Error'));
expect(updateFilters).toHaveBeenCalledWith({
error: new EntityErrorFilter(true),
});
@@ -110,7 +110,7 @@ describe('<EntityProcessingStatusPicker/>', () => {
it('remove orphan from orphan filter', () => {
const updateFilters = jest.fn();
const rendered = render(
render(
<MockEntityListContextProvider
value={{
entities: sampleEntities,
@@ -122,8 +122,8 @@ describe('<EntityProcessingStatusPicker/>', () => {
</MockEntityListContextProvider>,
);
fireEvent.click(rendered.getByTestId('processing-status-picker-expand'));
fireEvent.click(rendered.getByText('Is Orphan'));
fireEvent.click(screen.getByTestId('processing-status-picker-expand'));
fireEvent.click(screen.getByText('Is Orphan'));
expect(updateFilters).toHaveBeenCalledWith({
orphan: undefined,
});
@@ -131,7 +131,7 @@ describe('<EntityProcessingStatusPicker/>', () => {
it('remove error from error filter', () => {
const updateFilters = jest.fn();
const rendered = render(
render(
<MockEntityListContextProvider
value={{
entities: sampleEntities,
@@ -143,8 +143,8 @@ describe('<EntityProcessingStatusPicker/>', () => {
</MockEntityListContextProvider>,
);
fireEvent.click(rendered.getByTestId('processing-status-picker-expand'));
fireEvent.click(rendered.getByText('Has Error'));
fireEvent.click(screen.getByTestId('processing-status-picker-expand'));
fireEvent.click(screen.getByText('Has Error'));
expect(updateFilters).toHaveBeenCalledWith({
error: undefined,
});
@@ -15,6 +15,7 @@
*/
import { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { entityRouteRef } from '../../routes';
import { EntityRefLink } from './EntityRefLink';
@@ -33,16 +34,13 @@ describe('<EntityRefLink />', () => {
lifecycle: 'production',
},
};
const { getByText } = await renderInTestApp(
<EntityRefLink entityRef={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
},
await renderInTestApp(<EntityRefLink entityRef={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
},
);
});
expect(getByText('component:software')).toHaveAttribute(
expect(screen.getByText('component:software')).toHaveAttribute(
'href',
'/catalog/default/component/software',
);
@@ -62,15 +60,12 @@ describe('<EntityRefLink />', () => {
lifecycle: 'production',
},
};
const { getByText } = await renderInTestApp(
<EntityRefLink entityRef={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
},
await renderInTestApp(<EntityRefLink entityRef={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
},
);
expect(getByText('component:test/software')).toHaveAttribute(
});
expect(screen.getByText('component:test/software')).toHaveAttribute(
'href',
'/catalog/test/component/software',
);
@@ -90,7 +85,7 @@ describe('<EntityRefLink />', () => {
lifecycle: 'production',
},
};
const { getByText } = await renderInTestApp(
await renderInTestApp(
<EntityRefLink entityRef={entity} defaultKind="Component" />,
{
mountedRoutes: {
@@ -98,7 +93,7 @@ describe('<EntityRefLink />', () => {
},
},
);
expect(getByText('test/software')).toHaveAttribute(
expect(screen.getByText('test/software')).toHaveAttribute(
'href',
'/catalog/test/component/software',
);
@@ -110,15 +105,12 @@ describe('<EntityRefLink />', () => {
namespace: 'default',
name: 'software',
};
const { getByText } = await renderInTestApp(
<EntityRefLink entityRef={entityName} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
},
await renderInTestApp(<EntityRefLink entityRef={entityName} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
},
);
expect(getByText('component:software')).toHaveAttribute(
});
expect(screen.getByText('component:software')).toHaveAttribute(
'href',
'/catalog/default/component/software',
);
@@ -130,15 +122,12 @@ describe('<EntityRefLink />', () => {
namespace: 'test',
name: 'software',
};
const { getByText } = await renderInTestApp(
<EntityRefLink entityRef={entityName} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
},
await renderInTestApp(<EntityRefLink entityRef={entityName} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
},
);
expect(getByText('component:test/software')).toHaveAttribute(
});
expect(screen.getByText('component:test/software')).toHaveAttribute(
'href',
'/catalog/test/component/software',
);
@@ -150,7 +139,7 @@ describe('<EntityRefLink />', () => {
namespace: 'test',
name: 'software',
};
const { getByText } = await renderInTestApp(
await renderInTestApp(
<EntityRefLink entityRef={entityName} defaultKind="component" />,
{
mountedRoutes: {
@@ -158,7 +147,7 @@ describe('<EntityRefLink />', () => {
},
},
);
expect(getByText('test/software')).toHaveAttribute(
expect(screen.getByText('test/software')).toHaveAttribute(
'href',
'/catalog/test/component/software',
);
@@ -170,7 +159,7 @@ describe('<EntityRefLink />', () => {
namespace: 'test',
name: 'software',
};
const { getByText } = await renderInTestApp(
await renderInTestApp(
<EntityRefLink entityRef={entityName} defaultKind="component">
Custom Children
</EntityRefLink>,
@@ -180,7 +169,7 @@ describe('<EntityRefLink />', () => {
},
},
);
expect(getByText('Custom Children')).toHaveAttribute(
expect(screen.getByText('Custom Children')).toHaveAttribute(
'href',
'/catalog/test/component/software',
);
@@ -15,6 +15,7 @@
*/
import { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { entityRouteRef } from '../../routes';
import { EntityRefLinks } from './EntityRefLinks';
@@ -28,15 +29,12 @@ describe('<EntityRefLinks />', () => {
name: 'software',
},
];
const { getByText } = await renderInTestApp(
<EntityRefLinks entityRefs={entityNames} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
},
await renderInTestApp(<EntityRefLinks entityRefs={entityNames} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
},
);
expect(getByText('component:software')).toHaveAttribute(
});
expect(screen.getByText('component:software')).toHaveAttribute(
'href',
'/catalog/default/component/software',
);
@@ -55,20 +53,17 @@ describe('<EntityRefLinks />', () => {
name: 'interface',
},
];
const { getByText } = await renderInTestApp(
<EntityRefLinks entityRefs={entityNames} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
},
await renderInTestApp(<EntityRefLinks entityRefs={entityNames} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
},
);
expect(getByText(',')).toBeInTheDocument();
expect(getByText('component:software')).toHaveAttribute(
});
expect(screen.getByText(',')).toBeInTheDocument();
expect(screen.getByText('component:software')).toHaveAttribute(
'href',
'/catalog/default/component/software',
);
expect(getByText('api:interface')).toHaveAttribute(
expect(screen.getByText('api:interface')).toHaveAttribute(
'href',
'/catalog/default/api/interface',
);
@@ -16,6 +16,7 @@
import { FetchedEntityRefLinks } from './FetchedEntityRefLinks';
import { entityRouteRef } from '../../routes';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import { Entity } from '@backstage/catalog-model';
import React from 'react';
import { JsonObject } from '@backstage/types';
@@ -60,7 +61,7 @@ describe('<FetchedEntityRefLinks />', () => {
}),
};
const rendered = await renderInTestApp(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<FetchedEntityRefLinks entityRefs={entityRefs} getTitle={getTitle} />
</TestApiProvider>,
@@ -71,12 +72,12 @@ describe('<FetchedEntityRefLinks />', () => {
},
);
expect(rendered.getByText('SOFTWARE')).toHaveAttribute(
expect(screen.getByText('SOFTWARE')).toHaveAttribute(
'href',
'/catalog/default/component/software',
);
expect(rendered.getByText('INTERFACE')).toHaveAttribute(
expect(screen.getByText('INTERFACE')).toHaveAttribute(
'href',
'/catalog/default/api/interface',
);
@@ -111,7 +112,7 @@ describe('<FetchedEntityRefLinks />', () => {
const catalogApi: Partial<CatalogApi> = {};
const rendered = await renderInTestApp(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<FetchedEntityRefLinks entityRefs={entityRefs} getTitle={getTitle} />
</TestApiProvider>,
@@ -122,12 +123,12 @@ describe('<FetchedEntityRefLinks />', () => {
},
);
expect(rendered.getByText('TOOL')).toHaveAttribute(
expect(screen.getByText('TOOL')).toHaveAttribute(
'href',
'/catalog/default/component/tool',
);
expect(rendered.getByText('IMPLEMENTATION')).toHaveAttribute(
expect(screen.getByText('IMPLEMENTATION')).toHaveAttribute(
'href',
'/catalog/default/api/implementation',
);
@@ -189,7 +190,7 @@ describe('<FetchedEntityRefLinks />', () => {
}),
};
const rendered = await renderInTestApp(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<FetchedEntityRefLinks entityRefs={entityRefs} getTitle={getTitle} />
</TestApiProvider>,
@@ -200,17 +201,17 @@ describe('<FetchedEntityRefLinks />', () => {
},
);
expect(rendered.getByText('TOOL')).toHaveAttribute(
expect(screen.getByText('TOOL')).toHaveAttribute(
'href',
'/catalog/default/component/tool',
);
expect(rendered.getByText('IMPLEMENTATION')).toHaveAttribute(
expect(screen.getByText('IMPLEMENTATION')).toHaveAttribute(
'href',
'/catalog/default/api/implementation',
);
expect(rendered.getByText('INTERFACE')).toHaveAttribute(
expect(screen.getByText('INTERFACE')).toHaveAttribute(
'href',
'/catalog/default/component/interface',
);
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { fireEvent, render, waitFor } from '@testing-library/react';
import { fireEvent, render, waitFor, screen } from '@testing-library/react';
import { EntitySearchBar } from './EntitySearchBar';
import { DefaultEntityFilters } from '../../hooks/useEntityListProvider';
import { EntityTextFilter } from '../../filters';
@@ -29,13 +29,13 @@ describe('EntitySearchBar', () => {
text: new EntityTextFilter('hello'),
};
const { getByDisplayValue } = render(
render(
<MockEntityListContextProvider value={{ updateFilters, filters }}>
<EntitySearchBar />
</MockEntityListContextProvider>,
);
const searchInput = getByDisplayValue('hello');
const searchInput = screen.getByDisplayValue('hello');
expect(searchInput).toBeInTheDocument();
fireEvent.change(searchInput, { target: { value: 'world' } });
@@ -16,13 +16,13 @@
import { Entity } from '@backstage/catalog-model';
import { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import { waitFor, screen } from '@testing-library/react';
import React from 'react';
import { EntityTable } from './EntityTable';
describe('<EntityTable />', () => {
it('shows empty table', async () => {
const { getByText } = await renderInTestApp(
await renderInTestApp(
<EntityTable
title="Entities"
entities={[]}
@@ -31,8 +31,8 @@ describe('<EntityTable />', () => {
/>,
);
expect(getByText('Entities')).toBeInTheDocument();
expect(getByText('EMPTY')).toBeInTheDocument();
expect(screen.getByText('Entities')).toBeInTheDocument();
expect(screen.getByText('EMPTY')).toBeInTheDocument();
});
it('shows entities', async () => {
@@ -47,7 +47,7 @@ describe('<EntityTable />', () => {
},
];
const { getByText } = await renderInTestApp(
await renderInTestApp(
<EntityTable
title="Entities"
entities={entities}
@@ -62,7 +62,7 @@ describe('<EntityTable />', () => {
);
await waitFor(() => {
expect(getByText('my-entity')).toBeInTheDocument();
expect(screen.getByText('my-entity')).toBeInTheDocument();
});
});
});
@@ -21,7 +21,7 @@ import {
SystemEntity,
} from '@backstage/catalog-model';
import { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import { waitFor, screen } from '@testing-library/react';
import React from 'react';
import { entityRouteRef } from '../../routes';
import { EntityTable } from './EntityTable';
@@ -54,7 +54,7 @@ describe('systemEntityColumns', () => {
},
];
const { getByText } = await renderInTestApp(
await renderInTestApp(
<EntityTable
title="My Systems"
entities={entities}
@@ -69,10 +69,10 @@ describe('systemEntityColumns', () => {
);
await waitFor(() => {
expect(getByText('my-namespace/my-system')).toBeInTheDocument();
expect(getByText('my-namespace/my-domain')).toBeInTheDocument();
expect(getByText('test')).toBeInTheDocument();
expect(getByText(/Some/)).toBeInTheDocument();
expect(screen.getByText('my-namespace/my-system')).toBeInTheDocument();
expect(screen.getByText('my-namespace/my-domain')).toBeInTheDocument();
expect(screen.getByText('test')).toBeInTheDocument();
expect(screen.getByText(/Some/)).toBeInTheDocument();
});
});
});
@@ -106,7 +106,7 @@ describe('componentEntityColumns', () => {
},
];
const { getByText } = await renderInTestApp(
await renderInTestApp(
<EntityTable
title="My Components"
entities={entities}
@@ -121,12 +121,12 @@ describe('componentEntityColumns', () => {
);
await waitFor(() => {
expect(getByText('my-namespace/my-component')).toBeInTheDocument();
expect(getByText('my-namespace/my-system')).toBeInTheDocument();
expect(getByText('test')).toBeInTheDocument();
expect(getByText('production')).toBeInTheDocument();
expect(getByText('service')).toBeInTheDocument();
expect(getByText(/Some/)).toBeInTheDocument();
expect(screen.getByText('my-namespace/my-component')).toBeInTheDocument();
expect(screen.getByText('my-namespace/my-system')).toBeInTheDocument();
expect(screen.getByText('test')).toBeInTheDocument();
expect(screen.getByText('production')).toBeInTheDocument();
expect(screen.getByText('service')).toBeInTheDocument();
expect(screen.getByText(/Some/)).toBeInTheDocument();
});
});
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { fireEvent, render, waitFor } from '@testing-library/react';
import { fireEvent, render, waitFor, screen } from '@testing-library/react';
import React from 'react';
import { MockEntityListContextProvider } from '../../testUtils/providers';
import { EntityTagFilter } from '../../filters';
@@ -35,34 +35,34 @@ describe('<EntityTagPicker/>', () => {
} as unknown as CatalogApi;
it('renders all tags', async () => {
const rendered = render(
render(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider value={{}}>
<EntityTagPicker />
</MockEntityListContextProvider>
</TestApiProvider>,
);
await waitFor(() => expect(rendered.getByText('Tags')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('Tags')).toBeInTheDocument());
fireEvent.click(rendered.getByTestId('tag-picker-expand'));
fireEvent.click(screen.getByTestId('tag-picker-expand'));
tags.forEach(tag => {
expect(rendered.getByText(tag)).toBeInTheDocument();
expect(screen.getByText(tag)).toBeInTheDocument();
});
});
it('renders unique tags in alphabetical order', async () => {
const rendered = render(
render(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider value={{}}>
<EntityTagPicker />
</MockEntityListContextProvider>
</TestApiProvider>,
);
await waitFor(() => expect(rendered.getByText('Tags')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('Tags')).toBeInTheDocument());
fireEvent.click(rendered.getByTestId('tag-picker-expand'));
fireEvent.click(screen.getByTestId('tag-picker-expand'));
expect(rendered.getAllByRole('option').map(o => o.textContent)).toEqual([
expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([
'tag1',
'tag2',
'tag3',
@@ -71,18 +71,18 @@ describe('<EntityTagPicker/>', () => {
});
it('renders tags with counts', async () => {
const rendered = render(
render(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider value={{}}>
<EntityTagPicker showCounts />
</MockEntityListContextProvider>
</TestApiProvider>,
);
await waitFor(() => expect(rendered.getByText('Tags')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('Tags')).toBeInTheDocument());
fireEvent.click(rendered.getByTestId('tag-picker-expand'));
fireEvent.click(screen.getByTestId('tag-picker-expand'));
expect(rendered.getAllByRole('option').map(o => o.textContent)).toEqual([
expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([
'tag1 (0)',
'tag2 (1)',
'tag3 (2)',
@@ -115,7 +115,7 @@ describe('<EntityTagPicker/>', () => {
it('adds tags to filters', async () => {
const updateFilters = jest.fn();
const rendered = render(
render(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
@@ -132,8 +132,8 @@ describe('<EntityTagPicker/>', () => {
}),
);
fireEvent.click(rendered.getByTestId('tag-picker-expand'));
fireEvent.click(rendered.getByText('tag1'));
fireEvent.click(screen.getByTestId('tag-picker-expand'));
fireEvent.click(screen.getByText('tag1'));
expect(updateFilters).toHaveBeenLastCalledWith({
tags: new EntityTagFilter(['tag1']),
});
@@ -141,7 +141,7 @@ describe('<EntityTagPicker/>', () => {
it('removes tags from filters', async () => {
const updateFilters = jest.fn();
const rendered = render(
render(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
@@ -158,10 +158,10 @@ describe('<EntityTagPicker/>', () => {
tags: new EntityTagFilter(['tag1']),
}),
);
fireEvent.click(rendered.getByTestId('tag-picker-expand'));
expect(rendered.getByLabelText('tag1')).toBeChecked();
fireEvent.click(screen.getByTestId('tag-picker-expand'));
expect(screen.getByLabelText('tag1')).toBeChecked();
fireEvent.click(rendered.getByLabelText('tag1'));
fireEvent.click(screen.getByLabelText('tag1'));
expect(updateFilters).toHaveBeenLastCalledWith({
tags: undefined,
});
@@ -15,13 +15,12 @@
*/
import React from 'react';
import { fireEvent, waitFor } from '@testing-library/react';
import { fireEvent, waitFor, screen } from '@testing-library/react';
import { Entity } from '@backstage/catalog-model';
import { EntityTypePicker } from './EntityTypePicker';
import { MockEntityListContextProvider } from '../../testUtils/providers';
import { catalogApiRef } from '../../api';
import { EntityKindFilter, EntityTypeFilter } from '../../filters';
import { alertApiRef } from '@backstage/core-plugin-api';
import { ApiProvider } from '@backstage/core-app-api';
import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils';
@@ -84,7 +83,7 @@ const apis = TestApiRegistry.from(
describe('<EntityTypePicker/>', () => {
it('renders available entity types', async () => {
const rendered = await renderWithEffects(
await renderWithEffects(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{ filters: { kind: new EntityKindFilter('component') } }}
@@ -93,23 +92,21 @@ describe('<EntityTypePicker/>', () => {
</MockEntityListContextProvider>
</ApiProvider>,
);
expect(rendered.getByText('Type')).toBeInTheDocument();
expect(screen.getByText('Type')).toBeInTheDocument();
const input = rendered.getByTestId('select');
const input = screen.getByTestId('select');
fireEvent.click(input);
await waitFor(() => rendered.getByText('service'));
await waitFor(() => screen.getByText('service'));
entities.forEach(entity => {
expect(
rendered.getByText(entity.spec!.type as string),
).toBeInTheDocument();
expect(screen.getByText(entity.spec!.type as string)).toBeInTheDocument();
});
});
it('sets the selected type filter', async () => {
const updateFilters = jest.fn();
const rendered = await renderWithEffects(
await renderWithEffects(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{
@@ -121,18 +118,18 @@ describe('<EntityTypePicker/>', () => {
</MockEntityListContextProvider>
</ApiProvider>,
);
const input = rendered.getByTestId('select');
const input = screen.getByTestId('select');
fireEvent.click(input);
await waitFor(() => rendered.getByText('service'));
fireEvent.click(rendered.getByText('service'));
await waitFor(() => screen.getByText('service'));
fireEvent.click(screen.getByText('service'));
expect(updateFilters).toHaveBeenLastCalledWith({
type: new EntityTypeFilter(['service']),
});
fireEvent.click(input);
fireEvent.click(rendered.getByText('all'));
fireEvent.click(screen.getByText('all'));
expect(updateFilters).toHaveBeenLastCalledWith({ type: undefined });
});
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { fireEvent, render, waitFor } from '@testing-library/react';
import { fireEvent, render, waitFor, screen } from '@testing-library/react';
import {
Entity,
RELATION_OWNED_BY,
@@ -27,7 +27,6 @@ import { EntityTagFilter, UserListFilter } from '../../filters';
import { CatalogApi } from '@backstage/catalog-client';
import { catalogApiRef } from '../../api';
import { MockStorageApi, TestApiRegistry } from '@backstage/test-utils';
import { ApiProvider } from '@backstage/core-app-api';
import {
ConfigApi,
@@ -144,7 +143,7 @@ const backendEntities: Entity[] = [
describe('<UserListPicker />', () => {
it('renders filter groups', () => {
const { queryByText } = render(
render(
<ApiProvider apis={apis}>
<MockEntityListContextProvider value={{ backendEntities }}>
<UserListPicker />
@@ -152,12 +151,12 @@ describe('<UserListPicker />', () => {
</ApiProvider>,
);
expect(queryByText('Personal')).toBeInTheDocument();
expect(queryByText('Test Company')).toBeInTheDocument();
expect(screen.queryByText('Personal')).toBeInTheDocument();
expect(screen.queryByText('Test Company')).toBeInTheDocument();
});
it('renders filters', () => {
const { getAllByRole } = render(
render(
<ApiProvider apis={apis}>
<MockEntityListContextProvider value={{ backendEntities }}>
<UserListPicker />
@@ -166,12 +165,12 @@ describe('<UserListPicker />', () => {
);
expect(
getAllByRole('menuitem').map(({ textContent }) => textContent),
screen.getAllByRole('menuitem').map(({ textContent }) => textContent),
).toEqual(['Owned 1', 'Starred 1', 'All 4']);
});
it('includes counts alongside each filter', async () => {
const { getAllByRole } = render(
render(
<ApiProvider apis={apis}>
<MockEntityListContextProvider value={{ backendEntities }}>
<UserListPicker />
@@ -183,13 +182,13 @@ describe('<UserListPicker />', () => {
// menuitem itself, so we pick off the next sibling.
await waitFor(() => {
expect(
getAllByRole('menuitem').map(({ textContent }) => textContent),
screen.getAllByRole('menuitem').map(({ textContent }) => textContent),
).toEqual(['Owned 1', 'Starred 1', 'All 4']);
});
});
it('respects other frontend filters in counts', async () => {
const { getAllByRole } = render(
render(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{
@@ -204,7 +203,7 @@ describe('<UserListPicker />', () => {
await waitFor(() => {
expect(
getAllByRole('menuitem').map(({ textContent }) => textContent),
screen.getAllByRole('menuitem').map(({ textContent }) => textContent),
).toEqual(['Owned 1', 'Starred 0', 'All 2']);
});
});
@@ -229,7 +228,7 @@ describe('<UserListPicker />', () => {
it('updates user filter when a menuitem is selected', () => {
const updateFilters = jest.fn();
const { getByText } = render(
render(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{ backendEntities, updateFilters }}
@@ -239,7 +238,7 @@ describe('<UserListPicker />', () => {
</ApiProvider>,
);
fireEvent.click(getByText('Starred'));
fireEvent.click(screen.getByText('Starred'));
expect(updateFilters).toHaveBeenLastCalledWith({
user: new UserListFilter(
+5 -1
View File
@@ -1 +1,5 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
rules: {
'testing-library/prefer-screen-queries': 'error',
},
});
@@ -28,6 +28,7 @@ import {
} from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/react';
import React from 'react';
import { viewTechDocRouteRef } from '../../routes';
import { AboutCard } from './AboutCard';
@@ -73,7 +74,7 @@ describe('<AboutCard />', () => {
],
};
const { getByText } = await renderInTestApp(
await renderInTestApp(
<TestApiProvider
apis={[
[
@@ -98,10 +99,10 @@ describe('<AboutCard />', () => {
},
);
expect(getByText('service')).toBeInTheDocument();
expect(getByText('user:guest')).toBeInTheDocument();
expect(getByText('production')).toBeInTheDocument();
expect(getByText('This is the description')).toBeInTheDocument();
expect(screen.getByText('service')).toBeInTheDocument();
expect(screen.getByText('user:guest')).toBeInTheDocument();
expect(screen.getByText('production')).toBeInTheDocument();
expect(screen.getByText('This is the description')).toBeInTheDocument();
});
it('renders "view source" link', async () => {
@@ -122,7 +123,7 @@ describe('<AboutCard />', () => {
},
};
const { getByText } = await renderInTestApp(
await renderInTestApp(
<TestApiProvider
apis={[
[
@@ -153,7 +154,7 @@ describe('<AboutCard />', () => {
},
},
);
expect(getByText('View Source').closest('a')).toHaveAttribute(
expect(screen.getByText('View Source').closest('a')).toHaveAttribute(
'href',
'https://github.com/backstage/backstage/blob/master/software.yaml',
);
@@ -177,7 +178,7 @@ describe('<AboutCard />', () => {
},
};
const { getByTitle } = await renderInTestApp(
await renderInTestApp(
<TestApiProvider
apis={[
[
@@ -209,7 +210,7 @@ describe('<AboutCard />', () => {
},
);
const editLink = getByTitle('Edit Metadata').closest('a');
const editLink = screen.getByTitle('Edit Metadata').closest('a');
expect(editLink).toHaveAttribute(
'href',
'https://github.com/backstage/backstage/edit/master/software.yaml',
@@ -230,7 +231,7 @@ describe('<AboutCard />', () => {
},
};
const { getByText } = await renderInTestApp(
await renderInTestApp(
<TestApiProvider
apis={[
[
@@ -250,8 +251,8 @@ describe('<AboutCard />', () => {
},
},
);
expect(getByText('View Source')).toBeVisible();
expect(getByText('View Source').closest('a')).toBeNull();
expect(screen.getByText('View Source')).toBeVisible();
expect(screen.getByText('View Source').closest('a')).toBeNull();
});
it.each([
@@ -274,7 +275,7 @@ describe('<AboutCard />', () => {
},
};
const { getByTitle } = await renderInTestApp(
await renderInTestApp(
<TestApiProvider
apis={[
[
@@ -299,7 +300,7 @@ describe('<AboutCard />', () => {
'component:default/software',
);
await userEvent.click(getByTitle('Schedule entity refresh'));
await userEvent.click(screen.getByTitle('Schedule entity refresh'));
expect(catalogApi.refreshEntity).toHaveBeenCalledWith(
'component:default/software',
@@ -320,7 +321,7 @@ describe('<AboutCard />', () => {
},
};
const { queryByTitle } = await renderInTestApp(
await renderInTestApp(
<TestApiProvider
apis={[
[
@@ -341,7 +342,9 @@ describe('<AboutCard />', () => {
},
);
expect(queryByTitle('Schedule entity refresh')).not.toBeInTheDocument();
expect(
screen.queryByTitle('Schedule entity refresh'),
).not.toBeInTheDocument();
});
it('renders techdocs link', async () => {
@@ -361,7 +364,7 @@ describe('<AboutCard />', () => {
},
};
const { getByText } = await renderInTestApp(
await renderInTestApp(
<TestApiProvider
apis={[
[
@@ -394,7 +397,7 @@ describe('<AboutCard />', () => {
},
);
expect(getByText('View TechDocs').closest('a')).toHaveAttribute(
expect(screen.getByText('View TechDocs').closest('a')).toHaveAttribute(
'href',
'/docs/default/Component/software',
);
@@ -414,7 +417,7 @@ describe('<AboutCard />', () => {
},
};
const { getByText } = await renderInTestApp(
await renderInTestApp(
<TestApiProvider
apis={[
[
@@ -446,8 +449,8 @@ describe('<AboutCard />', () => {
},
);
expect(getByText('View TechDocs')).toBeVisible();
expect(getByText('View TechDocs').closest('a')).toBeNull();
expect(screen.getByText('View TechDocs')).toBeVisible();
expect(screen.getByText('View TechDocs').closest('a')).toBeNull();
});
it('renders disabled techdocs link when route is not bound', async () => {
@@ -467,7 +470,7 @@ describe('<AboutCard />', () => {
},
};
const { getByText } = await renderInTestApp(
await renderInTestApp(
<TestApiProvider
apis={[
[
@@ -499,7 +502,7 @@ describe('<AboutCard />', () => {
},
);
expect(getByText('View TechDocs')).toBeVisible();
expect(getByText('View TechDocs').closest('a')).toBeNull();
expect(screen.getByText('View TechDocs')).toBeVisible();
expect(screen.getByText('View TechDocs').closest('a')).toBeNull();
});
});
@@ -21,6 +21,7 @@ import {
} from '@backstage/catalog-model';
import { entityRouteRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { AboutContent } from './AboutContent';
@@ -62,32 +63,29 @@ describe('<AboutContent />', () => {
});
it('renders info', async () => {
const { getByText, queryByText } = await renderInTestApp(
<AboutContent entity={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
await renderInTestApp(<AboutContent entity={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
);
});
expect(getByText('Description')).toBeInTheDocument();
expect(getByText('Description').nextSibling).toHaveTextContent(
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('Description').nextSibling).toHaveTextContent(
'This is the description',
);
expect(getByText('Owner')).toBeInTheDocument();
expect(getByText('Owner').nextSibling).toHaveTextContent('user:o');
expect(getByText('Domain')).toBeInTheDocument();
expect(getByText('Domain').nextSibling).toHaveTextContent('d');
expect(getByText('System')).toBeInTheDocument();
expect(getByText('System').nextSibling).toHaveTextContent('s');
expect(queryByText('Parent Component')).not.toBeInTheDocument();
expect(getByText('Type')).toBeInTheDocument();
expect(getByText('Type').nextSibling).toHaveTextContent('t');
expect(getByText('Lifecycle')).toBeInTheDocument();
expect(getByText('Lifecycle').nextSibling).toHaveTextContent('l');
expect(getByText('Tags')).toBeInTheDocument();
expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1');
expect(screen.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner').nextSibling).toHaveTextContent('user:o');
expect(screen.getByText('Domain')).toBeInTheDocument();
expect(screen.getByText('Domain').nextSibling).toHaveTextContent('d');
expect(screen.getByText('System')).toBeInTheDocument();
expect(screen.getByText('System').nextSibling).toHaveTextContent('s');
expect(screen.queryByText('Parent Component')).not.toBeInTheDocument();
expect(screen.getByText('Type')).toBeInTheDocument();
expect(screen.getByText('Type').nextSibling).toHaveTextContent('t');
expect(screen.getByText('Lifecycle')).toBeInTheDocument();
expect(screen.getByText('Lifecycle').nextSibling).toHaveTextContent('l');
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Tags').nextSibling).toHaveTextContent('tag-1');
});
it('highlights missing required fields', async () => {
@@ -95,26 +93,25 @@ describe('<AboutContent />', () => {
entity.spec = {};
entity.relations = [];
const { getByText, queryByText } = await renderInTestApp(
<AboutContent entity={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
await renderInTestApp(<AboutContent entity={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
);
});
expect(getByText('Description')).toBeInTheDocument();
expect(getByText('Description').nextSibling).toHaveTextContent(
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('Description').nextSibling).toHaveTextContent(
'This is the description',
);
expect(getByText('Owner')).toBeInTheDocument();
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
expect(queryByText('Domain')).not.toBeInTheDocument();
expect(queryByText('System')).not.toBeInTheDocument();
expect(queryByText('Parent Component')).not.toBeInTheDocument();
expect(queryByText('Type')).not.toBeInTheDocument();
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
expect(screen.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner').nextSibling).toHaveTextContent(
'No Owner',
);
expect(screen.queryByText('Domain')).not.toBeInTheDocument();
expect(screen.queryByText('System')).not.toBeInTheDocument();
expect(screen.queryByText('Parent Component')).not.toBeInTheDocument();
expect(screen.queryByText('Type')).not.toBeInTheDocument();
expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument();
});
});
@@ -151,33 +148,34 @@ describe('<AboutContent />', () => {
});
it('renders info', async () => {
const { getByText, queryByText } = await renderInTestApp(
<AboutContent entity={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
await renderInTestApp(<AboutContent entity={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
);
});
expect(getByText('Description')).toBeInTheDocument();
expect(getByText('Description').nextSibling).toHaveTextContent(
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('Description').nextSibling).toHaveTextContent(
'This is the description',
);
expect(getByText('Owner')).toBeInTheDocument();
expect(getByText('Owner').nextSibling).toHaveTextContent('user:guest');
expect(queryByText('Domain')).not.toBeInTheDocument();
expect(getByText('System')).toBeInTheDocument();
expect(getByText('System').nextSibling).toHaveTextContent('system');
expect(queryByText('Parent Component')).not.toBeInTheDocument();
expect(getByText('Type')).toBeInTheDocument();
expect(getByText('Type').nextSibling).toHaveTextContent('openapi');
expect(getByText('Lifecycle')).toBeInTheDocument();
expect(getByText('Lifecycle').nextSibling).toHaveTextContent(
expect(screen.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner').nextSibling).toHaveTextContent(
'user:guest',
);
expect(screen.queryByText('Domain')).not.toBeInTheDocument();
expect(screen.getByText('System')).toBeInTheDocument();
expect(screen.getByText('System').nextSibling).toHaveTextContent(
'system',
);
expect(screen.queryByText('Parent Component')).not.toBeInTheDocument();
expect(screen.getByText('Type')).toBeInTheDocument();
expect(screen.getByText('Type').nextSibling).toHaveTextContent('openapi');
expect(screen.getByText('Lifecycle')).toBeInTheDocument();
expect(screen.getByText('Lifecycle').nextSibling).toHaveTextContent(
'production',
);
expect(getByText('Tags')).toBeInTheDocument();
expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1');
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Tags').nextSibling).toHaveTextContent('tag-1');
});
it('highlights missing required fields', async () => {
@@ -187,31 +185,34 @@ describe('<AboutContent />', () => {
delete entity.spec!.system;
entity.relations = [];
const { getByText, queryByText } = await renderInTestApp(
<AboutContent entity={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
await renderInTestApp(<AboutContent entity={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
);
});
expect(getByText('Description')).toBeInTheDocument();
expect(getByText('Description').nextSibling).toHaveTextContent(
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('Description').nextSibling).toHaveTextContent(
'This is the description',
);
expect(getByText('Owner')).toBeInTheDocument();
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
expect(queryByText('Domain')).not.toBeInTheDocument();
expect(getByText('System')).toBeInTheDocument();
expect(getByText('System').nextSibling).toHaveTextContent('No System');
expect(queryByText('Parent Component')).not.toBeInTheDocument();
expect(getByText('Type')).toBeInTheDocument();
expect(getByText('Type').nextSibling).toHaveTextContent('unknown');
expect(getByText('Lifecycle')).toBeInTheDocument();
expect(getByText('Lifecycle').nextSibling).toHaveTextContent('unknown');
expect(getByText('Tags')).toBeInTheDocument();
expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags');
expect(screen.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner').nextSibling).toHaveTextContent(
'No Owner',
);
expect(screen.queryByText('Domain')).not.toBeInTheDocument();
expect(screen.getByText('System')).toBeInTheDocument();
expect(screen.getByText('System').nextSibling).toHaveTextContent(
'No System',
);
expect(screen.queryByText('Parent Component')).not.toBeInTheDocument();
expect(screen.getByText('Type')).toBeInTheDocument();
expect(screen.getByText('Type').nextSibling).toHaveTextContent('unknown');
expect(screen.getByText('Lifecycle')).toBeInTheDocument();
expect(screen.getByText('Lifecycle').nextSibling).toHaveTextContent(
'unknown',
);
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Tags').nextSibling).toHaveTextContent('No Tags');
});
});
@@ -252,36 +253,37 @@ describe('<AboutContent />', () => {
});
it('renders info', async () => {
const { getByText, queryByText } = await renderInTestApp(
<AboutContent entity={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
await renderInTestApp(<AboutContent entity={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
);
});
expect(getByText('Description')).toBeInTheDocument();
expect(getByText('Description').nextSibling).toHaveTextContent(
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('Description').nextSibling).toHaveTextContent(
'This is the description',
);
expect(getByText('Owner')).toBeInTheDocument();
expect(getByText('Owner').nextSibling).toHaveTextContent('user:guest');
expect(queryByText('Domain')).not.toBeInTheDocument();
expect(getByText('System')).toBeInTheDocument();
expect(getByText('System').nextSibling).toHaveTextContent('system');
expect(getByText('Parent Component')).toBeInTheDocument();
expect(getByText('Parent Component').nextSibling).toHaveTextContent(
'parent-software',
expect(screen.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner').nextSibling).toHaveTextContent(
'user:guest',
);
expect(getByText('Type')).toBeInTheDocument();
expect(getByText('Type').nextSibling).toHaveTextContent('service');
expect(getByText('Lifecycle')).toBeInTheDocument();
expect(getByText('Lifecycle').nextSibling).toHaveTextContent(
expect(screen.queryByText('Domain')).not.toBeInTheDocument();
expect(screen.getByText('System')).toBeInTheDocument();
expect(screen.getByText('System').nextSibling).toHaveTextContent(
'system',
);
expect(screen.getByText('Parent Component')).toBeInTheDocument();
expect(
screen.getByText('Parent Component').nextSibling,
).toHaveTextContent('parent-software');
expect(screen.getByText('Type')).toBeInTheDocument();
expect(screen.getByText('Type').nextSibling).toHaveTextContent('service');
expect(screen.getByText('Lifecycle')).toBeInTheDocument();
expect(screen.getByText('Lifecycle').nextSibling).toHaveTextContent(
'production',
);
expect(getByText('Tags')).toBeInTheDocument();
expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1');
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Tags').nextSibling).toHaveTextContent('tag-1');
});
it('highlights missing required fields', async () => {
@@ -291,31 +293,34 @@ describe('<AboutContent />', () => {
delete entity.spec!.system;
entity.relations = [];
const { getByText, queryByText } = await renderInTestApp(
<AboutContent entity={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
await renderInTestApp(<AboutContent entity={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
);
});
expect(getByText('Description')).toBeInTheDocument();
expect(getByText('Description').nextSibling).toHaveTextContent(
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('Description').nextSibling).toHaveTextContent(
'This is the description',
);
expect(getByText('Owner')).toBeInTheDocument();
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
expect(queryByText('Domain')).not.toBeInTheDocument();
expect(getByText('System')).toBeInTheDocument();
expect(getByText('System').nextSibling).toHaveTextContent('No System');
expect(queryByText('Parent Component')).not.toBeInTheDocument();
expect(getByText('Type')).toBeInTheDocument();
expect(getByText('Type').nextSibling).toHaveTextContent('unknown');
expect(getByText('Lifecycle')).toBeInTheDocument();
expect(getByText('Lifecycle').nextSibling).toHaveTextContent('unknown');
expect(getByText('Tags')).toBeInTheDocument();
expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags');
expect(screen.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner').nextSibling).toHaveTextContent(
'No Owner',
);
expect(screen.queryByText('Domain')).not.toBeInTheDocument();
expect(screen.getByText('System')).toBeInTheDocument();
expect(screen.getByText('System').nextSibling).toHaveTextContent(
'No System',
);
expect(screen.queryByText('Parent Component')).not.toBeInTheDocument();
expect(screen.getByText('Type')).toBeInTheDocument();
expect(screen.getByText('Type').nextSibling).toHaveTextContent('unknown');
expect(screen.getByText('Lifecycle')).toBeInTheDocument();
expect(screen.getByText('Lifecycle').nextSibling).toHaveTextContent(
'unknown',
);
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Tags').nextSibling).toHaveTextContent('No Tags');
});
});
@@ -344,56 +349,54 @@ describe('<AboutContent />', () => {
});
it('renders info', async () => {
const { getByText, queryByText } = await renderInTestApp(
<AboutContent entity={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
await renderInTestApp(<AboutContent entity={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
);
});
expect(getByText('Description')).toBeInTheDocument();
expect(getByText('Description').nextSibling).toHaveTextContent(
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('Description').nextSibling).toHaveTextContent(
'This is the description',
);
expect(getByText('Owner')).toBeInTheDocument();
expect(getByText('Owner').nextSibling).toHaveTextContent('user:guest');
expect(queryByText('Domain')).not.toBeInTheDocument();
expect(queryByText('System')).not.toBeInTheDocument();
expect(queryByText('Parent Component')).not.toBeInTheDocument();
expect(queryByText('Type')).not.toBeInTheDocument();
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
expect(getByText('Tags')).toBeInTheDocument();
expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1');
expect(screen.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner').nextSibling).toHaveTextContent(
'user:guest',
);
expect(screen.queryByText('Domain')).not.toBeInTheDocument();
expect(screen.queryByText('System')).not.toBeInTheDocument();
expect(screen.queryByText('Parent Component')).not.toBeInTheDocument();
expect(screen.queryByText('Type')).not.toBeInTheDocument();
expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument();
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Tags').nextSibling).toHaveTextContent('tag-1');
});
it('highlights missing required fields', async () => {
delete entity.metadata.tags;
entity.relations = [];
const { getByText, queryByText } = await renderInTestApp(
<AboutContent entity={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
await renderInTestApp(<AboutContent entity={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
);
});
expect(getByText('Description')).toBeInTheDocument();
expect(getByText('Description').nextSibling).toHaveTextContent(
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('Description').nextSibling).toHaveTextContent(
'This is the description',
);
expect(getByText('Owner')).toBeInTheDocument();
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
expect(queryByText('Domain')).not.toBeInTheDocument();
expect(queryByText('System')).not.toBeInTheDocument();
expect(queryByText('Parent Component')).not.toBeInTheDocument();
expect(queryByText('Type')).not.toBeInTheDocument();
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
expect(getByText('Tags')).toBeInTheDocument();
expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags');
expect(screen.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner').nextSibling).toHaveTextContent(
'No Owner',
);
expect(screen.queryByText('Domain')).not.toBeInTheDocument();
expect(screen.queryByText('System')).not.toBeInTheDocument();
expect(screen.queryByText('Parent Component')).not.toBeInTheDocument();
expect(screen.queryByText('Type')).not.toBeInTheDocument();
expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument();
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Tags').nextSibling).toHaveTextContent('No Tags');
});
});
@@ -418,31 +421,30 @@ describe('<AboutContent />', () => {
});
it('renders info', async () => {
const { getByText, queryByText } = await renderInTestApp(
<AboutContent entity={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
await renderInTestApp(<AboutContent entity={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
);
});
expect(getByText('Description')).toBeInTheDocument();
expect(getByText('Description').nextSibling).toHaveTextContent(
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('Description').nextSibling).toHaveTextContent(
'This is the description',
);
expect(getByText('Owner')).toBeInTheDocument();
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
expect(queryByText('Domain')).not.toBeInTheDocument();
expect(queryByText('System')).not.toBeInTheDocument();
expect(queryByText('Parent Component')).not.toBeInTheDocument();
expect(getByText('Type')).toBeInTheDocument();
expect(getByText('Type').nextSibling).toHaveTextContent('root');
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
expect(getByText('Tags')).toBeInTheDocument();
expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1');
expect(getByText('Targets')).toBeInTheDocument();
expect(getByText('Targets').nextSibling).toHaveTextContent(
expect(screen.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner').nextSibling).toHaveTextContent(
'No Owner',
);
expect(screen.queryByText('Domain')).not.toBeInTheDocument();
expect(screen.queryByText('System')).not.toBeInTheDocument();
expect(screen.queryByText('Parent Component')).not.toBeInTheDocument();
expect(screen.getByText('Type')).toBeInTheDocument();
expect(screen.getByText('Type').nextSibling).toHaveTextContent('root');
expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument();
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Tags').nextSibling).toHaveTextContent('tag-1');
expect(screen.getByText('Targets')).toBeInTheDocument();
expect(screen.getByText('Targets').nextSibling).toHaveTextContent(
'https://backstage.io',
);
});
@@ -451,29 +453,28 @@ describe('<AboutContent />', () => {
delete entity.metadata.tags;
delete entity.spec!.type;
const { getByText, queryByText } = await renderInTestApp(
<AboutContent entity={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
await renderInTestApp(<AboutContent entity={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
);
});
expect(getByText('Description')).toBeInTheDocument();
expect(getByText('Description').nextSibling).toHaveTextContent(
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('Description').nextSibling).toHaveTextContent(
'This is the description',
);
expect(getByText('Owner')).toBeInTheDocument();
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
expect(queryByText('Domain')).not.toBeInTheDocument();
expect(queryByText('System')).not.toBeInTheDocument();
expect(queryByText('Parent Component')).not.toBeInTheDocument();
expect(getByText('Type')).toBeInTheDocument();
expect(getByText('Type').nextSibling).toHaveTextContent('unknown');
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
expect(getByText('Tags')).toBeInTheDocument();
expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags');
expect(screen.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner').nextSibling).toHaveTextContent(
'No Owner',
);
expect(screen.queryByText('Domain')).not.toBeInTheDocument();
expect(screen.queryByText('System')).not.toBeInTheDocument();
expect(screen.queryByText('Parent Component')).not.toBeInTheDocument();
expect(screen.getByText('Type')).toBeInTheDocument();
expect(screen.getByText('Type').nextSibling).toHaveTextContent('unknown');
expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument();
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Tags').nextSibling).toHaveTextContent('No Tags');
});
});
@@ -508,30 +509,31 @@ describe('<AboutContent />', () => {
});
it('renders info', async () => {
const { getByText, queryByText } = await renderInTestApp(
<AboutContent entity={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
await renderInTestApp(<AboutContent entity={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
);
});
expect(getByText('Description')).toBeInTheDocument();
expect(getByText('Description').nextSibling).toHaveTextContent(
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('Description').nextSibling).toHaveTextContent(
'This is the description',
);
expect(getByText('Owner')).toBeInTheDocument();
expect(getByText('Owner').nextSibling).toHaveTextContent('user:guest');
expect(queryByText('Domain')).not.toBeInTheDocument();
expect(getByText('System')).toBeInTheDocument();
expect(getByText('System').nextSibling).toHaveTextContent('system');
expect(queryByText('Parent Component')).not.toBeInTheDocument();
expect(getByText('Type')).toBeInTheDocument();
expect(getByText('Type').nextSibling).toHaveTextContent('s3');
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
expect(getByText('Tags')).toBeInTheDocument();
expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1');
expect(screen.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner').nextSibling).toHaveTextContent(
'user:guest',
);
expect(screen.queryByText('Domain')).not.toBeInTheDocument();
expect(screen.getByText('System')).toBeInTheDocument();
expect(screen.getByText('System').nextSibling).toHaveTextContent(
'system',
);
expect(screen.queryByText('Parent Component')).not.toBeInTheDocument();
expect(screen.getByText('Type')).toBeInTheDocument();
expect(screen.getByText('Type').nextSibling).toHaveTextContent('s3');
expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument();
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Tags').nextSibling).toHaveTextContent('tag-1');
});
it('highlights missing required fields', async () => {
@@ -540,30 +542,31 @@ describe('<AboutContent />', () => {
delete entity.spec!.system;
entity.relations = [];
const { getByText, queryByText } = await renderInTestApp(
<AboutContent entity={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
await renderInTestApp(<AboutContent entity={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
);
});
expect(getByText('Description')).toBeInTheDocument();
expect(getByText('Description').nextSibling).toHaveTextContent(
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('Description').nextSibling).toHaveTextContent(
'This is the description',
);
expect(getByText('Owner')).toBeInTheDocument();
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
expect(queryByText('Domain')).not.toBeInTheDocument();
expect(getByText('System')).toBeInTheDocument();
expect(getByText('System').nextSibling).toHaveTextContent('No System');
expect(queryByText('Parent Component')).not.toBeInTheDocument();
expect(getByText('Type')).toBeInTheDocument();
expect(getByText('Type').nextSibling).toHaveTextContent('unknown');
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
expect(getByText('Tags')).toBeInTheDocument();
expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags');
expect(screen.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner').nextSibling).toHaveTextContent(
'No Owner',
);
expect(screen.queryByText('Domain')).not.toBeInTheDocument();
expect(screen.getByText('System')).toBeInTheDocument();
expect(screen.getByText('System').nextSibling).toHaveTextContent(
'No System',
);
expect(screen.queryByText('Parent Component')).not.toBeInTheDocument();
expect(screen.getByText('Type')).toBeInTheDocument();
expect(screen.getByText('Type').nextSibling).toHaveTextContent('unknown');
expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument();
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Tags').nextSibling).toHaveTextContent('No Tags');
});
});
@@ -597,29 +600,30 @@ describe('<AboutContent />', () => {
});
it('renders info', async () => {
const { getByText, queryByText } = await renderInTestApp(
<AboutContent entity={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
await renderInTestApp(<AboutContent entity={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
);
});
expect(getByText('Description')).toBeInTheDocument();
expect(getByText('Description').nextSibling).toHaveTextContent(
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('Description').nextSibling).toHaveTextContent(
'This is the description',
);
expect(getByText('Owner')).toBeInTheDocument();
expect(getByText('Owner').nextSibling).toHaveTextContent('user:guest');
expect(getByText('Domain')).toBeInTheDocument();
expect(getByText('Domain').nextSibling).toHaveTextContent('domain');
expect(queryByText('System')).not.toBeInTheDocument();
expect(queryByText('Parent Component')).not.toBeInTheDocument();
expect(queryByText('Type')).not.toBeInTheDocument();
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
expect(getByText('Tags')).toBeInTheDocument();
expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1');
expect(screen.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner').nextSibling).toHaveTextContent(
'user:guest',
);
expect(screen.getByText('Domain')).toBeInTheDocument();
expect(screen.getByText('Domain').nextSibling).toHaveTextContent(
'domain',
);
expect(screen.queryByText('System')).not.toBeInTheDocument();
expect(screen.queryByText('Parent Component')).not.toBeInTheDocument();
expect(screen.queryByText('Type')).not.toBeInTheDocument();
expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument();
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Tags').nextSibling).toHaveTextContent('tag-1');
});
it('highlights missing required fields', async () => {
@@ -627,29 +631,30 @@ describe('<AboutContent />', () => {
delete entity.spec!.domain;
entity.relations = [];
const { getByText, queryByText } = await renderInTestApp(
<AboutContent entity={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
await renderInTestApp(<AboutContent entity={entity} />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
);
});
expect(getByText('Description')).toBeInTheDocument();
expect(getByText('Description').nextSibling).toHaveTextContent(
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('Description').nextSibling).toHaveTextContent(
'This is the description',
);
expect(getByText('Owner')).toBeInTheDocument();
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
expect(getByText('Domain')).toBeInTheDocument();
expect(getByText('Domain').nextSibling).toHaveTextContent('No Domain');
expect(queryByText('System')).not.toBeInTheDocument();
expect(queryByText('Parent Component')).not.toBeInTheDocument();
expect(queryByText('Type')).not.toBeInTheDocument();
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
expect(getByText('Tags')).toBeInTheDocument();
expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags');
expect(screen.getByText('Owner')).toBeInTheDocument();
expect(screen.getByText('Owner').nextSibling).toHaveTextContent(
'No Owner',
);
expect(screen.getByText('Domain')).toBeInTheDocument();
expect(screen.getByText('Domain').nextSibling).toHaveTextContent(
'No Domain',
);
expect(screen.queryByText('System')).not.toBeInTheDocument();
expect(screen.queryByText('Parent Component')).not.toBeInTheDocument();
expect(screen.queryByText('Type')).not.toBeInTheDocument();
expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument();
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Tags').nextSibling).toHaveTextContent('No Tags');
});
});
});
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { fireEvent } from '@testing-library/react';
import { fireEvent, screen } from '@testing-library/react';
import { GetEntityFacetsResponse } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import {
@@ -83,7 +83,7 @@ const apis = TestApiRegistry.from(
describe('<CatalogKindHeader />', () => {
it('renders available kinds', async () => {
const rendered = await renderWithEffects(
await renderWithEffects(
<ApiProvider apis={apis}>
<MockEntityListContextProvider>
<CatalogKindHeader />
@@ -91,18 +91,18 @@ describe('<CatalogKindHeader />', () => {
</ApiProvider>,
);
const input = rendered.getByText('Components');
const input = screen.getByText('Components');
fireEvent.mouseDown(input);
entities.map(entity => {
expect(
rendered.getByRole('option', { name: `${entity.kind}s` }),
screen.getByRole('option', { name: `${entity.kind}s` }),
).toBeInTheDocument();
});
});
it('renders unknown kinds provided in query parameters', async () => {
const rendered = await renderWithEffects(
await renderWithEffects(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{ queryParameters: { kind: 'frob' } }}
@@ -112,12 +112,12 @@ describe('<CatalogKindHeader />', () => {
</ApiProvider>,
);
expect(rendered.getByText('Frobs')).toBeInTheDocument();
expect(screen.getByText('Frobs')).toBeInTheDocument();
});
it('updates the kind filter', async () => {
const updateFilters = jest.fn();
const rendered = await renderWithEffects(
await renderWithEffects(
<ApiProvider apis={apis}>
<MockEntityListContextProvider value={{ updateFilters }}>
<CatalogKindHeader />
@@ -125,10 +125,10 @@ describe('<CatalogKindHeader />', () => {
</ApiProvider>,
);
const input = rendered.getByText('Components');
const input = screen.getByText('Components');
fireEvent.mouseDown(input);
const option = rendered.getByRole('option', { name: 'Templates' });
const option = screen.getByRole('option', { name: 'Templates' });
fireEvent.click(option);
expect(updateFilters).toHaveBeenCalledWith({
@@ -171,7 +171,7 @@ describe('<CatalogKindHeader />', () => {
});
it('limits kinds when allowedKinds is set', async () => {
const rendered = await renderWithEffects(
await renderWithEffects(
<ApiProvider apis={apis}>
<MockEntityListContextProvider>
<CatalogKindHeader allowedKinds={['component', 'system']} />
@@ -179,22 +179,20 @@ describe('<CatalogKindHeader />', () => {
</ApiProvider>,
);
const input = rendered.getByText('Components');
const input = screen.getByText('Components');
fireEvent.mouseDown(input);
expect(
rendered.getByRole('option', { name: 'Components' }),
screen.getByRole('option', { name: 'Components' }),
).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'Systems' })).toBeInTheDocument();
expect(
rendered.getByRole('option', { name: 'Systems' }),
).toBeInTheDocument();
expect(
rendered.queryByRole('option', { name: 'Templates' }),
screen.queryByRole('option', { name: 'Templates' }),
).not.toBeInTheDocument();
});
it('renders kind from the query parameter even when not in allowedKinds', async () => {
const rendered = await renderWithEffects(
await renderWithEffects(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{ queryParameters: { kind: 'Frob' } }}
@@ -204,12 +202,10 @@ describe('<CatalogKindHeader />', () => {
</ApiProvider>,
);
expect(rendered.getByText('Frobs')).toBeInTheDocument();
const input = rendered.getByText('Frobs');
expect(screen.getByText('Frobs')).toBeInTheDocument();
const input = screen.getByText('Frobs');
fireEvent.mouseDown(input);
expect(
rendered.getByRole('option', { name: 'Systems' }),
).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'Systems' })).toBeInTheDocument();
});
});
@@ -16,6 +16,7 @@
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import { useOutlet } from 'react-router';
import { CatalogPage } from './CatalogPage';
@@ -30,15 +31,15 @@ jest.mock('./DefaultCatalogPage', () => ({
describe('CatalogPage', () => {
it('renders provided router element', async () => {
const { getByText } = await renderInTestApp(<CatalogPage />);
await renderInTestApp(<CatalogPage />);
expect(getByText('Route Children')).toBeInTheDocument();
expect(screen.getByText('Route Children')).toBeInTheDocument();
});
it('renders DefaultCatalogPage home when no router children are provided', async () => {
(useOutlet as jest.Mock).mockReturnValueOnce(null);
const { getByText } = await renderInTestApp(<CatalogPage />);
await renderInTestApp(<CatalogPage />);
expect(getByText('DefaultCatalogPage')).toBeInTheDocument();
expect(screen.getByText('DefaultCatalogPage')).toBeInTheDocument();
});
});
@@ -161,11 +161,11 @@ describe('DefaultCatalogPage', () => {
// limit. We should investigate why these timeouts happen.
it('should render the default column of the grid', async () => {
const { getAllByRole } = await renderWrapped(<DefaultCatalogPage />);
await renderWrapped(<DefaultCatalogPage />);
const columnHeader = getAllByRole('button').filter(
c => c.tagName === 'SPAN',
);
const columnHeader = screen
.getAllByRole('button')
.filter(c => c.tagName === 'SPAN');
const columnHeaderLabels = columnHeader.map(c => c.textContent);
expect(columnHeaderLabels).toEqual([
@@ -186,26 +186,22 @@ describe('DefaultCatalogPage', () => {
{ title: 'Bar', field: 'entity.bar' },
{ title: 'Baz', field: 'entity.spec.lifecycle' },
];
const { getAllByRole } = await renderWrapped(
<DefaultCatalogPage columns={columns} />,
);
await renderWrapped(<DefaultCatalogPage columns={columns} />);
const columnHeader = getAllByRole('button').filter(
c => c.tagName === 'SPAN',
);
const columnHeader = screen
.getAllByRole('button')
.filter(c => c.tagName === 'SPAN');
const columnHeaderLabels = columnHeader.map(c => c.textContent);
expect(columnHeaderLabels).toEqual(['Foo', 'Bar', 'Baz', 'Actions']);
}, 20_000);
it('should render the default actions of an item in the grid', async () => {
const { getByTestId, findByTitle, findByText } = await renderWrapped(
<DefaultCatalogPage />,
);
fireEvent.click(getByTestId('user-picker-owned'));
expect(await findByText(/Owned \(1\)/)).toBeInTheDocument();
expect(await findByTitle(/View/)).toBeInTheDocument();
expect(await findByTitle(/Edit/)).toBeInTheDocument();
expect(await findByTitle(/Add to favorites/)).toBeInTheDocument();
await renderWrapped(<DefaultCatalogPage />);
fireEvent.click(screen.getByTestId('user-picker-owned'));
expect(await screen.findByText(/Owned \(1\)/)).toBeInTheDocument();
expect(await screen.findByTitle(/View/)).toBeInTheDocument();
expect(await screen.findByTitle(/Edit/)).toBeInTheDocument();
expect(await screen.findByTitle(/Add to favorites/)).toBeInTheDocument();
}, 20_000);
it('should render the custom actions of an item passed as prop', async () => {
@@ -228,41 +224,35 @@ describe('DefaultCatalogPage', () => {
},
];
const { getByTestId, findByTitle, findByText } = await renderWrapped(
<DefaultCatalogPage actions={actions} />,
);
fireEvent.click(getByTestId('user-picker-owned'));
expect(await findByText(/Owned \(1\)/)).toBeInTheDocument();
expect(await findByTitle(/Foo Action/)).toBeInTheDocument();
expect(await findByTitle(/Bar Action/)).toBeInTheDocument();
expect((await findByTitle(/Bar Action/)).firstChild).toBeDisabled();
await renderWrapped(<DefaultCatalogPage actions={actions} />);
fireEvent.click(screen.getByTestId('user-picker-owned'));
expect(await screen.findByText(/Owned \(1\)/)).toBeInTheDocument();
expect(await screen.findByTitle(/Foo Action/)).toBeInTheDocument();
expect(await screen.findByTitle(/Bar Action/)).toBeInTheDocument();
expect((await screen.findByTitle(/Bar Action/)).firstChild).toBeDisabled();
}, 20_000);
// this test right now causes some red lines in the log output when running tests
// related to some theme issues in mui-table
// https://github.com/mbrn/material-table/issues/1293
it('should render', async () => {
const { findByText, getByTestId } = await renderWrapped(
<DefaultCatalogPage />,
);
fireEvent.click(getByTestId('user-picker-owned'));
await expect(findByText(/Owned \(1\)/)).resolves.toBeInTheDocument();
fireEvent.click(getByTestId('user-picker-all'));
await expect(findByText(/All \(2\)/)).resolves.toBeInTheDocument();
await renderWrapped(<DefaultCatalogPage />);
fireEvent.click(screen.getByTestId('user-picker-owned'));
await expect(screen.findByText(/Owned \(1\)/)).resolves.toBeInTheDocument();
fireEvent.click(screen.getByTestId('user-picker-all'));
await expect(screen.findByText(/All \(2\)/)).resolves.toBeInTheDocument();
}, 20_000);
it('should set initial filter correctly', async () => {
const { findByText } = await renderWrapped(
<DefaultCatalogPage initiallySelectedFilter="all" />,
);
await expect(findByText(/All \(2\)/)).resolves.toBeInTheDocument();
await renderWrapped(<DefaultCatalogPage initiallySelectedFilter="all" />);
await expect(screen.findByText(/All \(2\)/)).resolves.toBeInTheDocument();
}, 20_000);
// this test is for fixing the bug after favoriting an entity, the matching
// entities defaulting to "owned" filter and not based on the selected filter
it('should render the correct entities filtered on the selected filter', async () => {
const { getByTestId } = await renderWrapped(<DefaultCatalogPage />);
fireEvent.click(getByTestId('user-picker-owned'));
await renderWrapped(<DefaultCatalogPage />);
fireEvent.click(screen.getByTestId('user-picker-owned'));
await expect(screen.findByText(/Owned \(1\)/)).resolves.toBeInTheDocument();
// The "Starred" menu option should initially be disabled, since there
// aren't any starred entities.
@@ -291,10 +281,12 @@ describe('DefaultCatalogPage', () => {
it('should wrap filter in drawer on smaller screens', async () => {
mockBreakpoint({ matches: true });
const { getByRole } = await renderWrapped(<DefaultCatalogPage />);
const button = getByRole('button', { name: 'Filters' });
expect(getByRole('presentation', { hidden: true })).toBeInTheDocument();
await renderWrapped(<DefaultCatalogPage />);
const button = screen.getByRole('button', { name: 'Filters' });
expect(
screen.getByRole('presentation', { hidden: true }),
).toBeInTheDocument();
fireEvent.click(button);
expect(getByRole('presentation')).toBeVisible();
expect(screen.getByRole('presentation')).toBeVisible();
}, 20_000);
});
@@ -29,7 +29,7 @@ import {
UserListFilter,
} from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { act, fireEvent } from '@testing-library/react';
import { act, fireEvent, screen } from '@testing-library/react';
import * as React from 'react';
import { CatalogTable } from './CatalogTable';
@@ -66,7 +66,7 @@ describe('CatalogTable component', () => {
});
it('should render error message', async () => {
const rendered = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider value={{ error: new Error('error') }}>
<CatalogTable />
@@ -78,14 +78,14 @@ describe('CatalogTable component', () => {
},
},
);
const errorMessage = await rendered.findByText(
const errorMessage = await screen.findByText(
/Could not fetch catalog entities./,
);
expect(errorMessage).toBeInTheDocument();
});
it('should display entity names when loading has finished and no error occurred', async () => {
const rendered = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider
value={{
@@ -108,10 +108,10 @@ describe('CatalogTable component', () => {
},
},
);
expect(rendered.getByText(/Owned \(3\)/)).toBeInTheDocument();
expect(rendered.getByText(/component1/)).toBeInTheDocument();
expect(rendered.getByText(/component2/)).toBeInTheDocument();
expect(rendered.getByText(/component3/)).toBeInTheDocument();
expect(screen.getByText(/Owned \(3\)/)).toBeInTheDocument();
expect(screen.getByText(/component1/)).toBeInTheDocument();
expect(screen.getByText(/component2/)).toBeInTheDocument();
expect(screen.getByText(/component3/)).toBeInTheDocument();
});
it('should use specified edit URL if in annotation', async () => {
@@ -124,7 +124,7 @@ describe('CatalogTable component', () => {
},
};
const { getByTitle } = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider value={{ entities: [entity] }}>
<CatalogTable />
@@ -137,7 +137,7 @@ describe('CatalogTable component', () => {
},
);
const editButton = getByTitle('Edit');
const editButton = screen.getByTitle('Edit');
await act(async () => {
fireEvent.click(editButton);
@@ -156,7 +156,7 @@ describe('CatalogTable component', () => {
},
};
const { getByTitle } = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider value={{ entities: [entity] }}>
<CatalogTable />
@@ -169,7 +169,7 @@ describe('CatalogTable component', () => {
},
);
const viewButton = getByTitle('View');
const viewButton = screen.getByTitle('View');
await act(async () => {
fireEvent.click(viewButton);
@@ -278,7 +278,7 @@ describe('CatalogTable component', () => {
])(
'should render correct columns with kind filter $kind',
async ({ kind, expectedColumns }) => {
const { getAllByRole } = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider
value={{
@@ -298,9 +298,9 @@ describe('CatalogTable component', () => {
},
);
const columnHeader = getAllByRole('button').filter(
c => c.tagName === 'SPAN',
);
const columnHeader = screen
.getAllByRole('button')
.filter(c => c.tagName === 'SPAN');
const columnHeaderLabels = columnHeader.map(c => c.textContent);
expect(columnHeaderLabels).toEqual(expectedColumns);
},
@@ -316,7 +316,7 @@ describe('CatalogTable component', () => {
},
};
const { getByText } = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider value={{ entities: [entity] }}>
<CatalogTable subtitle="Should be rendered" />
@@ -329,7 +329,7 @@ describe('CatalogTable component', () => {
},
);
expect(getByText('Should be rendered')).toBeInTheDocument();
expect(screen.getByText('Should be rendered')).toBeInTheDocument();
});
it('should render the label column with customised title and value as specified', async () => {
@@ -347,7 +347,7 @@ describe('CatalogTable component', () => {
};
const expectedColumns = ['Name', 'Category', 'Actions'];
const { getAllByRole, getByText } = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider value={{ entities: [entity] }}>
<CatalogTable columns={columns} />
@@ -360,13 +360,13 @@ describe('CatalogTable component', () => {
},
);
const columnHeader = getAllByRole('button').filter(
c => c.tagName === 'SPAN',
);
const columnHeader = screen
.getAllByRole('button')
.filter(c => c.tagName === 'SPAN');
const columnHeaderLabels = columnHeader.map(c => c.textContent);
expect(columnHeaderLabels).toEqual(expectedColumns);
const labelCellValue = getByText('generic');
const labelCellValue = screen.getByText('generic');
expect(labelCellValue).toBeInTheDocument();
});
});
@@ -22,7 +22,7 @@ import {
entityRouteRef,
} from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import React from 'react';
import { DependencyOfComponentsCard } from './DependencyOfComponentsCard';
@@ -51,7 +51,7 @@ describe('<DependencyOfComponentsCard />', () => {
relations: [],
};
const { getByText } = await renderInTestApp(
await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<DependencyOfComponentsCard />
@@ -64,9 +64,9 @@ describe('<DependencyOfComponentsCard />', () => {
},
);
expect(getByText('Dependency of components')).toBeInTheDocument();
expect(screen.getByText('Dependency of components')).toBeInTheDocument();
expect(
getByText(/No component depends on this component/i),
screen.getByText(/No component depends on this component/i),
).toBeInTheDocument();
});
@@ -99,7 +99,7 @@ describe('<DependencyOfComponentsCard />', () => {
],
});
const { getByText } = await renderInTestApp(
await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<DependencyOfComponentsCard />
@@ -113,8 +113,8 @@ describe('<DependencyOfComponentsCard />', () => {
);
await waitFor(() => {
expect(getByText('Dependency of components')).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
expect(screen.getByText('Dependency of components')).toBeInTheDocument();
expect(screen.getByText(/target-name/i)).toBeInTheDocument();
});
});
});
@@ -22,7 +22,7 @@ import {
entityRouteRef,
} from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import { waitFor, screen } from '@testing-library/react';
import React from 'react';
import { DependsOnComponentsCard } from './DependsOnComponentsCard';
@@ -51,7 +51,7 @@ describe('<DependsOnComponentsCard />', () => {
relations: [],
};
const { getByText } = await renderInTestApp(
await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<DependsOnComponentsCard />
@@ -64,9 +64,9 @@ describe('<DependsOnComponentsCard />', () => {
},
);
expect(getByText('Depends on components')).toBeInTheDocument();
expect(screen.getByText('Depends on components')).toBeInTheDocument();
expect(
getByText(/No component is a dependency of this component/i),
screen.getByText(/No component is a dependency of this component/i),
).toBeInTheDocument();
});
@@ -99,7 +99,7 @@ describe('<DependsOnComponentsCard />', () => {
],
});
const { getByText } = await renderInTestApp(
await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<DependsOnComponentsCard />
@@ -113,8 +113,8 @@ describe('<DependsOnComponentsCard />', () => {
);
await waitFor(() => {
expect(getByText('Depends on components')).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
expect(screen.getByText('Depends on components')).toBeInTheDocument();
expect(screen.getByText(/target-name/i)).toBeInTheDocument();
});
});
});
@@ -22,7 +22,7 @@ import {
entityRouteRef,
} from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import { waitFor, screen } from '@testing-library/react';
import React from 'react';
import { DependsOnResourcesCard } from './DependsOnResourcesCard';
@@ -51,7 +51,7 @@ describe('<DependsOnResourcesCard />', () => {
relations: [],
};
const { getByText } = await renderInTestApp(
await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<DependsOnResourcesCard />
@@ -64,9 +64,9 @@ describe('<DependsOnResourcesCard />', () => {
},
);
expect(getByText('Depends on resources')).toBeInTheDocument();
expect(screen.getByText('Depends on resources')).toBeInTheDocument();
expect(
getByText(/No resource is a dependency of this component/i),
screen.getByText(/No resource is a dependency of this component/i),
).toBeInTheDocument();
});
@@ -99,7 +99,7 @@ describe('<DependsOnResourcesCard />', () => {
],
});
const { getByText } = await renderInTestApp(
await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<DependsOnResourcesCard />
@@ -113,8 +113,8 @@ describe('<DependsOnResourcesCard />', () => {
);
await waitFor(() => {
expect(getByText('Depends on resources')).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
expect(screen.getByText('Depends on resources')).toBeInTheDocument();
expect(screen.getByText(/target-name/i)).toBeInTheDocument();
});
});
});
@@ -63,7 +63,7 @@ describe('ComponentContextMenu', () => {
it('check Unregister entity button is disabled', async () => {
const mockCallback = jest.fn();
const { getByText } = await render(
await render(
<EntityContextMenu
UNSTABLE_contextMenuOptions={{ disableUnregister: 'disable' }}
onUnregisterEntity={mockCallback}
@@ -75,10 +75,10 @@ describe('ComponentContextMenu', () => {
expect(button).toBeInTheDocument();
fireEvent.click(button);
const unregister = await screen.getByText('Unregister entity');
const unregister = screen.getByText('Unregister entity');
expect(unregister).toBeInTheDocument();
const unregisterSpanItem = getByText(/Unregister entity/);
const unregisterSpanItem = screen.getByText(/Unregister entity/);
const unregisterMenuListItem =
unregisterSpanItem?.parentElement?.parentElement;
expect(unregisterMenuListItem).toHaveAttribute('aria-disabled');
@@ -60,7 +60,7 @@ describe('ComponentContextMenu', () => {
it('check Unregister entity button is disabled', async () => {
const mockCallback = jest.fn();
const { getByText } = await render(
await render(
<UnregisterEntity
unregisterEntityOptions={{ disableUnregister: 'disable' }}
isUnregisterAllowed
@@ -69,10 +69,10 @@ describe('ComponentContextMenu', () => {
/>,
);
const unregister = await screen.getByText('Unregister entity');
const unregister = screen.getByText('Unregister entity');
expect(unregister).toBeInTheDocument();
const unregisterSpanItem = getByText(/Unregister entity/);
const unregisterSpanItem = screen.getByText(/Unregister entity/);
const unregisterMenuListItem =
unregisterSpanItem?.parentElement?.parentElement;
expect(unregisterMenuListItem).toHaveAttribute('aria-disabled');
@@ -32,7 +32,7 @@ import {
renderInTestApp,
TestApiRegistry,
} from '@backstage/test-utils';
import { act, fireEvent } from '@testing-library/react';
import { act, fireEvent, screen } from '@testing-library/react';
import React from 'react';
import { EntityLayout } from './EntityLayout';
@@ -52,7 +52,7 @@ const mockApis = TestApiRegistry.from(
describe('EntityLayout', () => {
it('renders simplest case', async () => {
const rendered = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<EntityProvider entity={mockEntity}>
<EntityLayout>
@@ -69,9 +69,9 @@ describe('EntityLayout', () => {
},
);
expect(rendered.getByText('my-entity')).toBeInTheDocument();
expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument();
expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument();
expect(screen.getByText('my-entity')).toBeInTheDocument();
expect(screen.getByText('tabbed-test-title')).toBeInTheDocument();
expect(screen.getByText('tabbed-test-content')).toBeInTheDocument();
});
it('renders the entity title if defined', async () => {
@@ -83,7 +83,7 @@ describe('EntityLayout', () => {
},
} as Entity;
const rendered = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<EntityProvider entity={mockEntityWithTitle}>
<EntityLayout>
@@ -100,13 +100,13 @@ describe('EntityLayout', () => {
},
);
expect(rendered.getByText('My Entity')).toBeInTheDocument();
expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument();
expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument();
expect(screen.getByText('My Entity')).toBeInTheDocument();
expect(screen.getByText('tabbed-test-title')).toBeInTheDocument();
expect(screen.getByText('tabbed-test-content')).toBeInTheDocument();
});
it('renders default error message when entity is not found', async () => {
const rendered = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<AsyncEntityProvider loading={false}>
<EntityLayout>
@@ -123,14 +123,14 @@ describe('EntityLayout', () => {
},
);
expect(rendered.getByText('Warning: Entity not found')).toBeInTheDocument();
expect(rendered.queryByText('my-entity')).not.toBeInTheDocument();
expect(rendered.queryByText('tabbed-test-title')).not.toBeInTheDocument();
expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument();
expect(screen.getByText('Warning: Entity not found')).toBeInTheDocument();
expect(screen.queryByText('my-entity')).not.toBeInTheDocument();
expect(screen.queryByText('tabbed-test-title')).not.toBeInTheDocument();
expect(screen.queryByText('tabbed-test-content')).not.toBeInTheDocument();
});
it('renders custom message when entity is not found', async () => {
const rendered = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<AsyncEntityProvider loading={false}>
<EntityLayout
@@ -150,15 +150,15 @@ describe('EntityLayout', () => {
);
expect(
rendered.getByText('Oppps.. Your entity was not found'),
screen.getByText('Oppps.. Your entity was not found'),
).toBeInTheDocument();
expect(rendered.queryByText('my-entity')).not.toBeInTheDocument();
expect(rendered.queryByText('tabbed-test-title')).not.toBeInTheDocument();
expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument();
expect(screen.queryByText('my-entity')).not.toBeInTheDocument();
expect(screen.queryByText('tabbed-test-title')).not.toBeInTheDocument();
expect(screen.queryByText('tabbed-test-content')).not.toBeInTheDocument();
});
it('navigates when user clicks different tab', async () => {
const rendered = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<EntityProvider entity={mockEntity}>
<EntityLayout>
@@ -181,23 +181,23 @@ describe('EntityLayout', () => {
},
);
const secondTab = rendered.queryAllByRole('tab')[1];
const secondTab = screen.queryAllByRole('tab')[1];
act(() => {
fireEvent.click(secondTab);
});
expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument();
expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument();
expect(screen.getByText('tabbed-test-title')).toBeInTheDocument();
expect(screen.queryByText('tabbed-test-content')).not.toBeInTheDocument();
expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument();
expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument();
expect(screen.getByText('tabbed-test-title-2')).toBeInTheDocument();
expect(screen.queryByText('tabbed-test-content-2')).toBeInTheDocument();
});
it('should conditionally render tabs', async () => {
const shouldRenderTab = (e: Entity) => e.metadata.name === 'my-entity';
const shouldNotRenderTab = (e: Entity) => e.metadata.name === 'some-entity';
const rendered = await renderInTestApp(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<EntityProvider entity={mockEntity}>
<EntityLayout>
@@ -228,8 +228,8 @@ describe('EntityLayout', () => {
},
);
expect(rendered.queryByText('tabbed-test-title')).toBeInTheDocument();
expect(rendered.queryByText('tabbed-test-title-2')).not.toBeInTheDocument();
expect(rendered.queryByText('tabbed-test-title-3')).toBeInTheDocument();
expect(screen.queryByText('tabbed-test-title')).toBeInTheDocument();
expect(screen.queryByText('tabbed-test-title-2')).not.toBeInTheDocument();
expect(screen.queryByText('tabbed-test-title-3')).toBeInTheDocument();
});
});
@@ -17,6 +17,7 @@
import { Entity, EntityLink } from '@backstage/catalog-model';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { EntityLinksCard } from './EntityLinksCard';
@@ -43,7 +44,7 @@ describe('EntityLinksCard', () => {
it('should render a link', async () => {
const links: EntityLink[] = [createLink()];
const { queryByText } = await renderWithEffects(
await renderWithEffects(
wrapInTestApp(
<EntityProvider entity={createEntity(links)}>
<EntityLinksCard />
@@ -51,12 +52,12 @@ describe('EntityLinksCard', () => {
),
);
expect(queryByText('admin dashboard')).toBeInTheDocument();
expect(queryByText('derp')).not.toBeInTheDocument();
expect(screen.queryByText('admin dashboard')).toBeInTheDocument();
expect(screen.queryByText('derp')).not.toBeInTheDocument();
});
it('should show empty state', async () => {
const { queryByText } = await renderWithEffects(
await renderWithEffects(
wrapInTestApp(
<EntityProvider entity={createEntity([])}>
<EntityLinksCard />
@@ -65,8 +66,8 @@ describe('EntityLinksCard', () => {
);
expect(
queryByText(/.*No links defined for this entity.*/),
screen.queryByText(/.*No links defined for this entity.*/),
).toBeInTheDocument();
expect(queryByText('admin dashboard')).not.toBeInTheDocument();
expect(screen.queryByText('admin dashboard')).not.toBeInTheDocument();
});
});
@@ -17,13 +17,13 @@
import { lightTheme } from '@backstage/theme';
import { ThemeProvider } from '@material-ui/core';
import CloudIcon from '@material-ui/icons/Cloud';
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { IconLink } from './IconLink';
describe('IconLink', () => {
it('should render an icon link', () => {
const rendered = render(
render(
<ThemeProvider theme={lightTheme}>
<IconLink
href="https://example.com"
@@ -33,6 +33,6 @@ describe('IconLink', () => {
</ThemeProvider>,
);
expect(rendered.queryByText('I am Link')).toBeInTheDocument();
expect(screen.queryByText('I am Link')).toBeInTheDocument();
});
});

Some files were not shown because too many files have changed in this diff Show More