Merge branch 'master' into feat/26838
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Preserve directory structure for CommonJS build output, just like ESM. This makes the build output more stable and easier to browse, and allows for more effective tree shaking and lazy imports.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli-node': patch
|
||||
---
|
||||
|
||||
Added new `lockfile.getDependencyTreeHash(name)` utility.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-test-utils': patch
|
||||
---
|
||||
|
||||
Added an `ApiMock`, analogous to `ServiceMock` from the backend test utils.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': minor
|
||||
---
|
||||
|
||||
Add catalog service mocks under the `/testUtils` subpath export.
|
||||
|
||||
You can now use e.g. `const catalog = catalogApiMock.mock()` in your test and then do assertions on `catalog.getEntities` without awkward type casting.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-node': patch
|
||||
---
|
||||
|
||||
Documentation for the `testUtils` named export
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Added a new `--successCache` option to the `backstage-cli repo lint` command. The cache keeps track of successful lint runs and avoids re-running linting of individual packages if they haven't changed. This option is primarily intended to be used in CI.
|
||||
|
||||
In addition a `--successCacheDir` option has also been added to be able to override the default cache directory.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-node': patch
|
||||
---
|
||||
|
||||
Use `branch` function instead of `checkout` function when creating branch
|
||||
@@ -234,7 +234,7 @@ jobs:
|
||||
run: node scripts/verify-release.js
|
||||
|
||||
- name: lint changed packages
|
||||
run: yarn backstage-cli repo lint --since origin/master
|
||||
run: yarn backstage-cli repo lint --since origin/master --successCache
|
||||
|
||||
- name: test changed packages
|
||||
run: yarn backstage-cli repo test --maxWorkers=3 --workerIdleMemoryLimit=1300M --since origin/master
|
||||
|
||||
@@ -225,7 +225,7 @@ jobs:
|
||||
run: ls microsite/build && ls microsite/build/storybook
|
||||
|
||||
- name: Deploy both microsite and storybook to gh-pages
|
||||
uses: JamesIves/github-pages-deploy-action@920cbb300dcd3f0568dbc42700c61e2fd9e6139c # v4.6.4
|
||||
uses: JamesIves/github-pages-deploy-action@881db5376404c5c8d621010bcbec0310b58d5e29 # v4.6.8
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: microsite/build
|
||||
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
run: yarn backstage-cli config:check --lax
|
||||
|
||||
- name: lint
|
||||
run: yarn backstage-cli repo lint
|
||||
run: yarn backstage-cli repo lint --successCache
|
||||
|
||||
- name: type checking and declarations
|
||||
run: yarn tsc:full
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: lint
|
||||
run: yarn backstage-cli repo lint
|
||||
run: yarn backstage-cli repo lint --successCache
|
||||
|
||||
- name: type checking and declarations
|
||||
run: yarn tsc:full
|
||||
|
||||
@@ -44,7 +44,7 @@ There's additional configuration that you can optionally pass to setup the `conf
|
||||
You can configure these additional options by adding an override for the core service when calling `createBackend` like follows:
|
||||
|
||||
```ts
|
||||
import { rootConfigServiceFactory } from '@backstage/backend-app-api';
|
||||
import { rootConfigServiceFactory } from '@backstage/backend-defaults/rootConfig';
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
|
||||
@@ -559,6 +559,14 @@ The overrides in a single `package.json` may for example look like this:
|
||||
},
|
||||
```
|
||||
|
||||
## Caching
|
||||
|
||||
Caching is used sparingly throughout the Backstage build system. It is always used as a way to squeeze out a little bit of extra performance, rather than requirement to keep things fast. The following is a list of places where optional caching is available:
|
||||
|
||||
- **TypeScript** - The default `tsconfig.json` used by Backstage projects has `incremental` set to `true`, which enables local caching of type checking results. It is however generally not recommended in CI, where `yarn tsc:full` is preferred, which sets `--incremental false`.
|
||||
- **Linting** - The `backstage-cli repo lint` command has a `--successCache` flag that enables caching of successful linting results. This is done at the package level, meaning that if a package has not been changed since the last lint run and it was successful, the linting will be skipped. This is recommended to be used in CI, but not during local development.
|
||||
- **Webpack** - It is possible to enable experimental caching of frontend package builds using the `BACKSTAGE_CLI_EXPERIMENTAL_BUILD_CACHE` environment variable. This will enable the Webpack filesystem cache.
|
||||
|
||||
### Debugging Jest Tests
|
||||
|
||||
For your productivity working with unit tests it's quite essential to have your debugging configured in IDE. It will help you to identify the root cause of the issue faster.
|
||||
|
||||
@@ -92,6 +92,7 @@ export function isMonoRepo(): Promise<boolean>;
|
||||
export class Lockfile {
|
||||
createSimplifiedDependencyGraph(): Map<string, Set<string>>;
|
||||
diff(otherLockfile: Lockfile): LockfileDiff;
|
||||
getDependencyTreeHash(startName: string): string;
|
||||
static load(path: string): Promise<Lockfile>;
|
||||
static parse(content: string): Lockfile;
|
||||
}
|
||||
|
||||
@@ -493,4 +493,95 @@ d@^1:
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDependencyTreeHash', () => {
|
||||
const content = `${MODERN_HEADER}
|
||||
"a@npm:^1":
|
||||
version: "1.0.0"
|
||||
checksum: sha512-a-1
|
||||
dependencies:
|
||||
b: "^2"
|
||||
|
||||
"b@npm:2.0.x, b@npm:^2":
|
||||
version: "2.0.0"
|
||||
checksum: sha512-b-1
|
||||
|
||||
"b@npm:4":
|
||||
version: "3.0.0"
|
||||
checksum: sha512-b-2
|
||||
|
||||
"c@npm:^1":
|
||||
version: "4.0.0"
|
||||
checksum: sha512-c-1
|
||||
`;
|
||||
const lockfile = Lockfile.parse(content);
|
||||
|
||||
const hashA = lockfile.getDependencyTreeHash('a');
|
||||
const hashB = lockfile.getDependencyTreeHash('b');
|
||||
const hashC = lockfile.getDependencyTreeHash('c');
|
||||
|
||||
it('should generate stable dependency hashes', () => {
|
||||
expect(hashA).toMatchInlineSnapshot(
|
||||
`"2d1d4c1c577c291e815e87779c72fc78a78e56cc"`,
|
||||
);
|
||||
expect(hashB).toMatchInlineSnapshot(
|
||||
`"7e46d0c7337540179b442c87a7c5555543798f15"`,
|
||||
);
|
||||
expect(hashC).toMatchInlineSnapshot(
|
||||
`"e65103abd217954bad40e2f834b990a5e6fa4054"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate different hashes for different versions', () => {
|
||||
const lockfileNewA = Lockfile.parse(content.replace('1.0.0', '1.0.1'));
|
||||
expect(lockfileNewA.getDependencyTreeHash('a')).not.toBe(hashA);
|
||||
expect(lockfileNewA.getDependencyTreeHash('b')).toBe(hashB);
|
||||
expect(lockfileNewA.getDependencyTreeHash('c')).toBe(hashC);
|
||||
|
||||
const lockfileNewB1 = Lockfile.parse(content.replace('2.0.0', '2.0.1'));
|
||||
expect(lockfileNewB1.getDependencyTreeHash('a')).not.toBe(hashA);
|
||||
expect(lockfileNewB1.getDependencyTreeHash('b')).not.toBe(hashB);
|
||||
expect(lockfileNewB1.getDependencyTreeHash('c')).toBe(hashC);
|
||||
|
||||
const lockfileNewB2 = Lockfile.parse(content.replace('3.0.0', '3.0.1'));
|
||||
expect(lockfileNewB2.getDependencyTreeHash('a')).not.toBe(hashA);
|
||||
expect(lockfileNewB2.getDependencyTreeHash('b')).not.toBe(hashB);
|
||||
expect(lockfileNewB2.getDependencyTreeHash('c')).toBe(hashC);
|
||||
|
||||
const lockfileNewC = Lockfile.parse(content.replace('4.0.0', '4.0.1'));
|
||||
expect(lockfileNewC.getDependencyTreeHash('a')).toBe(hashA);
|
||||
expect(lockfileNewC.getDependencyTreeHash('b')).toBe(hashB);
|
||||
expect(lockfileNewC.getDependencyTreeHash('c')).not.toBe(hashC);
|
||||
});
|
||||
|
||||
it('should generate different hashes for different checksums', () => {
|
||||
const lockfileNewA = Lockfile.parse(
|
||||
content.replace('sha512-a-1', 'sha512-a-1-new'),
|
||||
);
|
||||
expect(lockfileNewA.getDependencyTreeHash('a')).not.toBe(hashA);
|
||||
expect(lockfileNewA.getDependencyTreeHash('b')).toBe(hashB);
|
||||
expect(lockfileNewA.getDependencyTreeHash('c')).toBe(hashC);
|
||||
|
||||
const lockfileNewB1 = Lockfile.parse(
|
||||
content.replace('sha512-b-1', 'sha512-b-1-new'),
|
||||
);
|
||||
expect(lockfileNewB1.getDependencyTreeHash('a')).not.toBe(hashA);
|
||||
expect(lockfileNewB1.getDependencyTreeHash('b')).not.toBe(hashB);
|
||||
expect(lockfileNewB1.getDependencyTreeHash('c')).toBe(hashC);
|
||||
|
||||
const lockfileNewB2 = Lockfile.parse(
|
||||
content.replace('sha512-b-2', 'sha512-b-2-new'),
|
||||
);
|
||||
expect(lockfileNewB2.getDependencyTreeHash('a')).not.toBe(hashA);
|
||||
expect(lockfileNewB2.getDependencyTreeHash('b')).not.toBe(hashB);
|
||||
expect(lockfileNewB2.getDependencyTreeHash('c')).toBe(hashC);
|
||||
|
||||
const lockfileNewC = Lockfile.parse(
|
||||
content.replace('sha512-c-1', 'sha512-c-1-new'),
|
||||
);
|
||||
expect(lockfileNewC.getDependencyTreeHash('a')).toBe(hashA);
|
||||
expect(lockfileNewC.getDependencyTreeHash('b')).toBe(hashB);
|
||||
expect(lockfileNewC.getDependencyTreeHash('c')).not.toBe(hashC);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { parseSyml } from '@yarnpkg/parsers';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/;
|
||||
@@ -215,4 +216,63 @@ export class Lockfile {
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a sha1 hex hash of the dependency graph for a package.
|
||||
*/
|
||||
getDependencyTreeHash(startName: string): string {
|
||||
if (!this.packages.has(startName)) {
|
||||
throw new Error(`Package '${startName}' not found in lockfile`);
|
||||
}
|
||||
|
||||
const hash = crypto.createHash('sha1');
|
||||
|
||||
const queue = [startName];
|
||||
const seen = new Set<string>();
|
||||
|
||||
while (queue.length > 0) {
|
||||
const name = queue.pop()!;
|
||||
|
||||
if (seen.has(name)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(name);
|
||||
|
||||
const entries = this.packages.get(name);
|
||||
if (!entries) {
|
||||
continue; // In case of missing optional peer dependencies
|
||||
}
|
||||
|
||||
hash.update(`pkg:${name}`);
|
||||
hash.update('\0');
|
||||
|
||||
// TODO(Rugvip): This uses the same simplified lookup as createSimplifiedDependencyGraph()
|
||||
// we could match version queries to make the resulting tree a bit smaller.
|
||||
const deps = new Array<string>();
|
||||
for (const entry of entries) {
|
||||
// We're not being particular about stable ordering here. If the lockfile ordering changes, so will likely hash.
|
||||
hash.update(entry.version);
|
||||
|
||||
const data = this.data[entry.dataKey];
|
||||
if (!data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const checksum = data.checksum || data.integrity;
|
||||
if (checksum) {
|
||||
hash.update('#');
|
||||
hash.update(checksum);
|
||||
}
|
||||
|
||||
hash.update(' ');
|
||||
|
||||
deps.push(...Object.keys(data.dependencies ?? {}));
|
||||
deps.push(...Object.keys(data.peerDependencies ?? {}));
|
||||
}
|
||||
|
||||
queue.push(...new Set(deps));
|
||||
}
|
||||
|
||||
return hash.digest('hex');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,6 +446,8 @@ Usage: backstage-cli repo lint [options]
|
||||
Options:
|
||||
--format <format>
|
||||
--since <ref>
|
||||
--successCache
|
||||
--successCacheDir <path>
|
||||
--fix
|
||||
-h, --help
|
||||
```
|
||||
|
||||
@@ -61,6 +61,14 @@ export function registerRepoCommand(program: Command) {
|
||||
'--since <ref>',
|
||||
'Only lint packages that changed since the specified ref',
|
||||
)
|
||||
.option(
|
||||
'--successCache',
|
||||
'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run',
|
||||
)
|
||||
.option(
|
||||
'--successCacheDir <path>',
|
||||
'Set the success cache location, (default: node_modules/.cache/backstage-cli)',
|
||||
)
|
||||
.option('--fix', 'Attempt to automatically fix violations')
|
||||
.action(lazy(() => import('./repo/lint').then(m => m.command)));
|
||||
|
||||
|
||||
@@ -16,8 +16,14 @@
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, OptionValues } from 'commander';
|
||||
import { relative as relativePath } from 'path';
|
||||
import { PackageGraph, BackstagePackageJson } from '@backstage/cli-node';
|
||||
import fs from 'fs-extra';
|
||||
import { createHash } from 'crypto';
|
||||
import { relative as relativePath, resolve as resolvePath } from 'path';
|
||||
import {
|
||||
PackageGraph,
|
||||
BackstagePackageJson,
|
||||
Lockfile,
|
||||
} from '@backstage/cli-node';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { runWorkerQueueThreads } from '../../lib/parallel';
|
||||
import { createScriptOptionsParser } from './optionsParser';
|
||||
@@ -30,9 +36,43 @@ function depCount(pkg: BackstagePackageJson) {
|
||||
return deps + devDeps;
|
||||
}
|
||||
|
||||
const CACHE_FILE_NAME = 'lint-cache.json';
|
||||
|
||||
type Cache = string[];
|
||||
|
||||
async function readCache(dir: string): Promise<Cache | undefined> {
|
||||
try {
|
||||
const data = await fs.readJson(resolvePath(dir, CACHE_FILE_NAME));
|
||||
if (!Array.isArray(data)) {
|
||||
return undefined;
|
||||
}
|
||||
if (data.some(x => typeof x !== 'string')) {
|
||||
return undefined;
|
||||
}
|
||||
return data as Cache;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeCache(dir: string, cache: Cache) {
|
||||
await fs.mkdirp(dir);
|
||||
await fs.writeJson(resolvePath(dir, CACHE_FILE_NAME), cache, { spaces: 2 });
|
||||
}
|
||||
|
||||
export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
let packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
const cacheDir = resolvePath(
|
||||
opts.successCacheDir ?? 'node_modules/.cache/backstage-cli',
|
||||
);
|
||||
const cacheContext = opts.successCache
|
||||
? {
|
||||
cache: await readCache(cacheDir),
|
||||
lockfile: await Lockfile.load(paths.resolveTargetRoot('yarn.lock')),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (opts.since) {
|
||||
const graph = PackageGraph.fromPackages(packages);
|
||||
packages = await graph.listChangedPackages({
|
||||
@@ -57,26 +97,66 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
|
||||
const parseLintScript = createScriptOptionsParser(cmd, ['package', 'lint']);
|
||||
|
||||
const items = await Promise.all(
|
||||
packages.map(async pkg => {
|
||||
const lintOptions = parseLintScript(pkg.packageJson.scripts?.lint);
|
||||
const base = {
|
||||
fullDir: pkg.dir,
|
||||
relativeDir: relativePath(paths.targetRoot, pkg.dir),
|
||||
lintOptions,
|
||||
parentHash: undefined,
|
||||
};
|
||||
|
||||
if (!cacheContext) {
|
||||
return base;
|
||||
}
|
||||
|
||||
const hash = createHash('sha1');
|
||||
|
||||
hash.update(
|
||||
cacheContext.lockfile.getDependencyTreeHash(pkg.packageJson.name),
|
||||
);
|
||||
hash.update('\0');
|
||||
hash.update(JSON.stringify(lintOptions));
|
||||
hash.update('\0');
|
||||
hash.update(process.version); // Node.js version
|
||||
hash.update('\0');
|
||||
hash.update('v1'); // The version of this implementation
|
||||
|
||||
return {
|
||||
...base,
|
||||
parentHash: hash.digest('hex'),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const resultsList = await runWorkerQueueThreads({
|
||||
items: packages.map(pkg => ({
|
||||
fullDir: pkg.dir,
|
||||
relativeDir: relativePath(paths.targetRoot, pkg.dir),
|
||||
lintOptions: parseLintScript(pkg.packageJson.scripts?.lint),
|
||||
})),
|
||||
items,
|
||||
workerData: {
|
||||
fix: Boolean(opts.fix),
|
||||
format: opts.format as string | undefined,
|
||||
shouldCache: Boolean(cacheContext),
|
||||
successCache: cacheContext?.cache,
|
||||
},
|
||||
workerFactory: async ({ fix, format }) => {
|
||||
workerFactory: async ({ fix, format, shouldCache, successCache }) => {
|
||||
const { ESLint } = require('eslint') as typeof import('eslint');
|
||||
const crypto = require('crypto') as typeof import('crypto');
|
||||
const recursiveReadDir =
|
||||
require('recursive-readdir') as typeof import('recursive-readdir');
|
||||
const { readFile } =
|
||||
require('fs/promises') as typeof import('fs/promises');
|
||||
const { relative: workerRelativePath } =
|
||||
require('path') as typeof import('path');
|
||||
|
||||
return async ({
|
||||
fullDir,
|
||||
relativeDir,
|
||||
lintOptions,
|
||||
parentHash,
|
||||
}): Promise<{
|
||||
relativeDir: string;
|
||||
resultText: string;
|
||||
sha?: string;
|
||||
resultText?: string;
|
||||
failed: boolean;
|
||||
}> => {
|
||||
// Bit of a hack to make file resolutions happen from the correct directory
|
||||
@@ -89,6 +169,35 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
fix,
|
||||
extensions: ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'],
|
||||
});
|
||||
|
||||
let sha: string | undefined = undefined;
|
||||
if (shouldCache) {
|
||||
const result = await recursiveReadDir(fullDir);
|
||||
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(parentHash!);
|
||||
hash.update('\0');
|
||||
|
||||
for (const path of result.sort()) {
|
||||
if (await eslint.isPathIgnored(path)) {
|
||||
continue;
|
||||
}
|
||||
hash.update(workerRelativePath(fullDir, path));
|
||||
hash.update('\0');
|
||||
hash.update(await readFile(path));
|
||||
hash.update('\0');
|
||||
hash.update(
|
||||
JSON.stringify(await eslint.calculateConfigForFile(path)),
|
||||
);
|
||||
hash.update('\0');
|
||||
}
|
||||
sha = await hash.digest('hex');
|
||||
if (successCache?.includes(sha)) {
|
||||
console.log(`Skipped ${relativeDir} due to cache hit`);
|
||||
return { relativeDir, sha, failed: false };
|
||||
}
|
||||
}
|
||||
|
||||
const formatter = await eslint.loadFormatter(format);
|
||||
|
||||
const results = await eslint.lintFiles(['.']);
|
||||
@@ -112,13 +221,21 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
relativeDir,
|
||||
resultText,
|
||||
failed,
|
||||
sha,
|
||||
};
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const outputSuccessCache = [];
|
||||
|
||||
let failed = false;
|
||||
for (const { relativeDir, resultText, failed: runFailed } of resultsList) {
|
||||
for (const {
|
||||
relativeDir,
|
||||
resultText,
|
||||
failed: runFailed,
|
||||
sha,
|
||||
} of resultsList) {
|
||||
if (runFailed) {
|
||||
console.log(chalk.red(`Lint failed in ${relativeDir}`));
|
||||
failed = true;
|
||||
@@ -129,9 +246,15 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
console.log();
|
||||
console.log(resultText.trimStart());
|
||||
}
|
||||
} else if (sha) {
|
||||
outputSuccessCache.push(sha);
|
||||
}
|
||||
}
|
||||
|
||||
if (cacheContext) {
|
||||
await writeCache(cacheDir, outputSuccessCache);
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -123,6 +123,8 @@ export async function makeRollupConfigs(
|
||||
format: 'commonjs',
|
||||
interop: 'compat',
|
||||
sourcemap: true,
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: `${targetDir}/src`,
|
||||
exports: 'named',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('backendModule factory', () => {
|
||||
`availability plugins${sep}test-backend-module-tester-two`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.ts.hbs',
|
||||
|
||||
@@ -89,7 +89,7 @@ describe('backendPlugin factory', () => {
|
||||
`availability plugins${sep}test-backend`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating index.ts.hbs',
|
||||
'templating package.json.hbs',
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('frontendPlugin factory', () => {
|
||||
`availability plugins${sep}test`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.tsx.hbs',
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('nodeLibraryPackage factory', () => {
|
||||
`availability ${joinPath('packages', expectedNodeLibraryPackageName)}`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.ts.hbs',
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('pluginCommon factory', () => {
|
||||
`availability plugins${sep}test-common`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.ts.hbs',
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('pluginNode factory', () => {
|
||||
`availability plugins${sep}test-node`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.ts.hbs',
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('pluginWeb factory', () => {
|
||||
`availability plugins${sep}test-react`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.ts.hbs',
|
||||
|
||||
@@ -89,7 +89,7 @@ describe('scaffolderModule factory', () => {
|
||||
`availability plugins${sep}scaffolder-backend-module-test`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.ts.hbs',
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('webLibraryPackage factory', () => {
|
||||
`availability ${joinPath('packages', expectedwebLibraryPackageName)}`,
|
||||
'creating temp dir',
|
||||
'Executing Template:',
|
||||
'copying .eslintrc.js',
|
||||
'templating .eslintrc.js.hbs',
|
||||
'templating README.md.hbs',
|
||||
'templating package.json.hbs',
|
||||
'templating index.ts.hbs',
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@types/jest": "*",
|
||||
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
/// <reference types="jest" />
|
||||
/// <reference types="react" />
|
||||
|
||||
import { AnalyticsApi } from '@backstage/frontend-plugin-api';
|
||||
import { AnalyticsEvent } from '@backstage/frontend-plugin-api';
|
||||
import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { ApiFactory } from '@backstage/frontend-plugin-api';
|
||||
import { AppNode } from '@backstage/frontend-plugin-api';
|
||||
import { AppNodeInstance } from '@backstage/frontend-plugin-api';
|
||||
import { ErrorWithContext } from '@backstage/test-utils';
|
||||
@@ -32,6 +34,15 @@ import { TestApiProviderProps } from '@backstage/test-utils';
|
||||
import { TestApiRegistry } from '@backstage/test-utils';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
|
||||
// @public
|
||||
export type ApiMock<TApi> = {
|
||||
factory: ApiFactory<TApi, TApi, {}>;
|
||||
} & {
|
||||
[Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return
|
||||
? TApi[Key] & jest.MockInstance<Return, Args>
|
||||
: TApi[Key];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export function createExtensionTester<T extends ExtensionDefinitionParameters>(
|
||||
subject: ExtensionDefinition<T>,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2024 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 { ApiFactory } from '@backstage/frontend-plugin-api';
|
||||
|
||||
/**
|
||||
* Represents a mocked version of an API, where you automatically have access to
|
||||
* the mocked versions of all of its methods along with a factory that returns
|
||||
* that same mock.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ApiMock<TApi> = {
|
||||
factory: ApiFactory<TApi, TApi, {}>;
|
||||
} & {
|
||||
[Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return
|
||||
? TApi[Key] & jest.MockInstance<Return, Args>
|
||||
: TApi[Key];
|
||||
};
|
||||
@@ -26,4 +26,5 @@ export {
|
||||
type MockStorageBucket,
|
||||
} from '@backstage/test-utils';
|
||||
|
||||
export { type ApiMock } from './ApiMock';
|
||||
export { MockAnalyticsApi } from './AnalyticsApi/MockAnalyticsApi';
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -22,6 +22,4 @@ export namespace catalogServiceMock {
|
||||
partialImpl?: Partial<CatalogApi> | undefined,
|
||||
) => ServiceMock<CatalogApi>;
|
||||
}
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -14,4 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Backend test helpers for the Catalog plugin.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { catalogServiceMock } from './testUtils/catalogServiceMock';
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./alpha": "./src/alpha/index.ts",
|
||||
"./testUtils": "./src/testUtils.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
@@ -39,6 +40,9 @@
|
||||
"alpha": [
|
||||
"src/alpha/index.ts"
|
||||
],
|
||||
"testUtils": [
|
||||
"src/testUtils.ts"
|
||||
],
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
@@ -64,6 +68,7 @@
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/frontend-plugin-api": "workspace:^",
|
||||
"@backstage/frontend-test-utils": "workspace:^",
|
||||
"@backstage/integration-react": "workspace:^",
|
||||
"@backstage/plugin-catalog-common": "workspace:^",
|
||||
"@backstage/plugin-permission-common": "workspace:^",
|
||||
@@ -85,7 +90,6 @@
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/core-app-api": "workspace:^",
|
||||
"@backstage/frontend-test-utils": "workspace:^",
|
||||
"@backstage/plugin-catalog-common": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-common": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
## API Report File for "@backstage/plugin-catalog-react"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { ApiFactory } from '@backstage/frontend-plugin-api';
|
||||
import { ApiMock } from '@backstage/frontend-test-utils';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { DefaultEntityFilters } from '@backstage/plugin-catalog-react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityListContextProps } from '@backstage/plugin-catalog-react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
|
||||
// @public
|
||||
export function catalogApiMock(options?: { entities?: Entity[] }): CatalogApi;
|
||||
|
||||
// @public
|
||||
export namespace catalogApiMock {
|
||||
const factory: (options?: {
|
||||
entities?: Entity[];
|
||||
}) => ApiFactory<CatalogApi, CatalogApi, {}>;
|
||||
const mock: (
|
||||
partialImpl?: Partial<CatalogApi> | undefined,
|
||||
) => ApiMock<CatalogApi>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export function MockEntityListContextProvider<
|
||||
T extends DefaultEntityFilters = DefaultEntityFilters,
|
||||
>(
|
||||
props: PropsWithChildren<{
|
||||
value?: Partial<EntityListContextProps<T>>;
|
||||
}>,
|
||||
): React_2.JSX.Element;
|
||||
```
|
||||
@@ -695,7 +695,7 @@ export function MissingAnnotationEmptyState(props: {
|
||||
// @public (undocumented)
|
||||
export type MissingAnnotationEmptyStateClassKey = 'code';
|
||||
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export function MockEntityListContextProvider<
|
||||
T extends DefaultEntityFilters = DefaultEntityFilters,
|
||||
>(
|
||||
@@ -887,6 +887,7 @@ export function useStarredEntity(
|
||||
// src/components/UserListPicker/UserListPicker.d.ts:5:1 - (ae-undocumented) Missing documentation for "CatalogReactUserListPickerClassKey".
|
||||
// src/components/UserListPicker/UserListPicker.d.ts:15:1 - (ae-undocumented) Missing documentation for "UserListPickerProps".
|
||||
// src/components/UserListPicker/UserListPicker.d.ts:20:22 - (ae-undocumented) Missing documentation for "UserListPicker".
|
||||
// src/deprecated.d.ts:7:1 - (ae-undocumented) Missing documentation for "MockEntityListContextProvider".
|
||||
// src/filters.d.ts:8:5 - (ae-undocumented) Missing documentation for "value".
|
||||
// src/filters.d.ts:10:5 - (ae-undocumented) Missing documentation for "getCatalogFilters".
|
||||
// src/filters.d.ts:11:5 - (ae-undocumented) Missing documentation for "toQueryValue".
|
||||
@@ -948,7 +949,6 @@ export function useStarredEntity(
|
||||
// src/hooks/useStarredEntity.d.ts:3:1 - (ae-undocumented) Missing documentation for "useStarredEntity".
|
||||
// src/overridableComponents.d.ts:6:1 - (ae-undocumented) Missing documentation for "CatalogReactComponentsNameToClassKey".
|
||||
// src/overridableComponents.d.ts:19:1 - (ae-undocumented) Missing documentation for "BackstageOverrides".
|
||||
// src/testUtils/providers.d.ts:4:1 - (ae-undocumented) Missing documentation for "MockEntityListContextProvider".
|
||||
// src/types.d.ts:3:1 - (ae-undocumented) Missing documentation for "EntityFilter".
|
||||
// src/types.d.ts:25:1 - (ae-undocumented) Missing documentation for "UserListFilterKind".
|
||||
// src/types.d.ts:27:1 - (ae-undocumented) Missing documentation for "EntityListPagination".
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
|
||||
import { fireEvent, render, waitFor, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { EntityAutocompletePicker } from './EntityAutocompletePicker';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { catalogApiRef } from '../../api';
|
||||
|
||||
@@ -24,7 +24,7 @@ import { capitalize } from 'lodash';
|
||||
import { default as React } from 'react';
|
||||
import { catalogApiRef } from '../../api';
|
||||
import { EntityKindFilter } from '../../filters';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { EntityKindPicker } from './EntityKindPicker';
|
||||
|
||||
const entities: Entity[] = [
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { EntityLifecycleFilter } from '../../filters';
|
||||
import { EntityLifecyclePicker } from './EntityLifecyclePicker';
|
||||
import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { EntityNamespaceFilter } from '../../filters';
|
||||
import { EntityNamespacePicker } from './EntityNamespacePicker';
|
||||
import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { EntityOwnerFilter } from '../../filters';
|
||||
import { EntityOwnerPicker } from './EntityOwnerPicker';
|
||||
import { ApiProvider } from '@backstage/core-app-api';
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { EntityErrorFilter, EntityOrphanFilter } from '../../filters';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { EntityProcessingStatusPicker } from './EntityProcessingStatusPicker';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import React from 'react';
|
||||
import { fireEvent, waitFor, screen } from '@testing-library/react';
|
||||
import { EntitySearchBar } from './EntitySearchBar';
|
||||
import { EntityTextFilter } from '../../filters';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
|
||||
describe('EntitySearchBar', () => {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { fireEvent, waitFor, screen, act } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { EntityTagFilter } from '../../filters';
|
||||
import { EntityTagPicker } from './EntityTagPicker';
|
||||
import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
|
||||
|
||||
@@ -18,7 +18,7 @@ import React from 'react';
|
||||
import { fireEvent, waitFor, screen, within } from '@testing-library/react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityTypePicker } from './EntityTypePicker';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { catalogApiRef } from '../../api';
|
||||
import { EntityKindFilter, EntityTypeFilter } from '../../filters';
|
||||
import { alertApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
+5
-10
@@ -14,13 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CatalogApi, Location } from '@backstage/catalog-client';
|
||||
import { Location } from '@backstage/catalog-client';
|
||||
import { Entity, ANNOTATION_ORIGIN_LOCATION } from '@backstage/catalog-model';
|
||||
import { catalogApiRef } from '../../api';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { useUnregisterEntityDialogState } from './useUnregisterEntityDialogState';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
|
||||
function defer<T>(): { promise: Promise<T>; resolve: (value: T) => void } {
|
||||
let resolve: (value: T) => void = () => {};
|
||||
@@ -31,13 +32,7 @@ function defer<T>(): { promise: Promise<T>; resolve: (value: T) => void } {
|
||||
}
|
||||
|
||||
describe('useUnregisterEntityDialogState', () => {
|
||||
const catalogApiMock = {
|
||||
getLocationByRef: jest.fn(),
|
||||
getEntities: jest.fn(),
|
||||
removeLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
};
|
||||
const catalogApi = catalogApiMock as Partial<CatalogApi> as CatalogApi;
|
||||
const catalogApi = catalogApiMock.mock();
|
||||
|
||||
const Wrapper = (props: { children?: React.ReactNode }) => (
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
@@ -58,8 +53,8 @@ describe('useUnregisterEntityDialogState', () => {
|
||||
resolveLocation = deferredLocation.resolve;
|
||||
resolveColocatedEntities = deferredColocatedEntities.resolve;
|
||||
|
||||
catalogApiMock.getLocationByRef.mockReturnValue(deferredLocation.promise);
|
||||
catalogApiMock.getEntities.mockReturnValue(
|
||||
catalogApi.getLocationByRef.mockReturnValue(deferredLocation.promise);
|
||||
catalogApi.getEntities.mockReturnValue(
|
||||
deferredColocatedEntities.promise.then(items => ({ items })),
|
||||
);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import React from 'react';
|
||||
import { fireEvent, waitFor, screen } from '@testing-library/react';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
import { UserListPicker, UserListPickerProps } from './UserListPicker';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import {
|
||||
EntityKindFilter,
|
||||
EntityNamespaceFilter,
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2024 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 React, {
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
DefaultEntityFilters,
|
||||
EntityListContext,
|
||||
EntityListContextProps,
|
||||
} from './hooks/useEntityListProvider';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Moved to `@backstage/plugin-catalog-react/testUtils`
|
||||
*/
|
||||
export function MockEntityListContextProvider<
|
||||
T extends DefaultEntityFilters = DefaultEntityFilters,
|
||||
>(
|
||||
props: PropsWithChildren<{
|
||||
value?: Partial<EntityListContextProps<T>>;
|
||||
}>,
|
||||
) {
|
||||
const { children, value } = props;
|
||||
|
||||
// Provides a default implementation that stores filter state, for testing components that
|
||||
// reflect filter state.
|
||||
const [filters, setFilters] = useState<T>(value?.filters ?? ({} as T));
|
||||
|
||||
const updateFilters = useCallback(
|
||||
(update: Partial<T> | ((prevFilters: T) => Partial<T>)) => {
|
||||
setFilters(prevFilters => {
|
||||
const newFilters =
|
||||
typeof update === 'function' ? update(prevFilters) : update;
|
||||
return { ...prevFilters, ...newFilters };
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Memoize the default values since pickers have useEffect triggers on these; naively defaulting
|
||||
// below with `?? <X>` breaks referential equality on subsequent updates.
|
||||
const defaultValues = useMemo(
|
||||
() => ({
|
||||
entities: [],
|
||||
backendEntities: [],
|
||||
queryParameters: {},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const resolvedValue: EntityListContextProps<T> = useMemo(
|
||||
() => ({
|
||||
entities: value?.entities ?? defaultValues.entities,
|
||||
backendEntities: value?.backendEntities ?? defaultValues.backendEntities,
|
||||
updateFilters: value?.updateFilters ?? updateFilters,
|
||||
filters,
|
||||
loading: value?.loading ?? false,
|
||||
queryParameters: value?.queryParameters ?? defaultValues.queryParameters,
|
||||
error: value?.error,
|
||||
totalItems:
|
||||
value?.totalItems ?? (value?.entities ?? defaultValues.entities).length,
|
||||
limit: value?.limit ?? 20,
|
||||
offset: value?.offset,
|
||||
setLimit: value?.setLimit ?? (() => {}),
|
||||
setOffset: value?.setOffset,
|
||||
paginationMode: value?.paginationMode ?? 'none',
|
||||
}),
|
||||
[value, defaultValues, filters, updateFilters],
|
||||
);
|
||||
|
||||
return (
|
||||
<EntityListContext.Provider value={resolvedValue}>
|
||||
{children}
|
||||
</EntityListContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -28,8 +28,8 @@ export * from './components';
|
||||
export * from './hooks';
|
||||
export * from './filters';
|
||||
export { entityRouteParams, entityRouteRef } from './routes';
|
||||
export * from './testUtils';
|
||||
export * from './types';
|
||||
export * from './overridableComponents';
|
||||
export { getEntityRelations, getEntitySourceLocation } from './utils';
|
||||
export type { EntitySourceLocation } from './utils';
|
||||
export * from './deprecated';
|
||||
|
||||
+10
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
* Copyright 2024 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.
|
||||
@@ -13,4 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { MockEntityListContextProvider } from './providers';
|
||||
|
||||
/**
|
||||
* Frontend test helpers for the Catalog plugin.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { catalogApiMock } from './testUtils/catalogApiMock';
|
||||
export { MockEntityListContextProvider } from './testUtils/MockEntityListContextProvider';
|
||||
+6
-2
@@ -24,9 +24,13 @@ import {
|
||||
DefaultEntityFilters,
|
||||
EntityListContext,
|
||||
EntityListContextProps,
|
||||
} from '../hooks/useEntityListProvider';
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Simplifies testing of code that uses the entity list hooks.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function MockEntityListContextProvider<
|
||||
T extends DefaultEntityFilters = DefaultEntityFilters,
|
||||
>(
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2024 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 { Entity } from '@backstage/catalog-model';
|
||||
import { catalogApiMock } from './catalogApiMock';
|
||||
|
||||
const entity1: Entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'CustomKind',
|
||||
metadata: {
|
||||
namespace: 'default',
|
||||
name: 'e1',
|
||||
uid: 'u1',
|
||||
},
|
||||
};
|
||||
|
||||
const entity2: Entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'CustomKind',
|
||||
metadata: {
|
||||
namespace: 'default',
|
||||
name: 'e2',
|
||||
uid: 'u2',
|
||||
},
|
||||
};
|
||||
|
||||
const entities = [entity1, entity2];
|
||||
|
||||
describe('catalogApiMock', () => {
|
||||
it('exports the expected functionality', async () => {
|
||||
const emptyFake = catalogApiMock();
|
||||
const notEmptyFake = catalogApiMock({ entities });
|
||||
|
||||
await expect(emptyFake.getEntities()).resolves.toEqual({ items: [] });
|
||||
await expect(notEmptyFake.getEntities()).resolves.toEqual({
|
||||
items: entities,
|
||||
});
|
||||
|
||||
const mock = catalogApiMock.mock();
|
||||
expect(mock.getEntities).toHaveBeenCalledTimes(0);
|
||||
expect(mock.getEntities()).toBeUndefined();
|
||||
mock.getEntities.mockResolvedValue({ items: entities });
|
||||
await expect(mock.getEntities()).resolves.toEqual({ items: entities });
|
||||
|
||||
const mock2 = catalogApiMock.mock({
|
||||
getEntities: async () => ({ items: [entity1] }),
|
||||
});
|
||||
await expect(mock2.getEntities()).resolves.toEqual({ items: [entity1] });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2024 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 {
|
||||
ApiFactory,
|
||||
ApiRef,
|
||||
createApiFactory,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { InMemoryCatalogClient } from '@backstage/catalog-client/testUtils';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { ApiMock } from '@backstage/frontend-test-utils';
|
||||
|
||||
/** @internal */
|
||||
function simpleMock<TApi>(
|
||||
ref: ApiRef<TApi>,
|
||||
mockFactory: () => jest.Mocked<TApi>,
|
||||
): (partialImpl?: Partial<TApi>) => ApiMock<TApi> {
|
||||
return partialImpl => {
|
||||
const mock = mockFactory();
|
||||
if (partialImpl) {
|
||||
for (const [key, impl] of Object.entries(partialImpl)) {
|
||||
if (typeof impl === 'function') {
|
||||
(mock as any)[key].mockImplementation(impl);
|
||||
} else {
|
||||
(mock as any)[key] = impl;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.assign(mock, {
|
||||
factory: createApiFactory({
|
||||
api: ref,
|
||||
deps: {},
|
||||
factory: () => mock,
|
||||
}),
|
||||
}) as ApiMock<TApi>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fake catalog client that handles entities in memory storage. Note
|
||||
* that this client may be severely limited in functionality, and advanced
|
||||
* functions may not be available at all.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function catalogApiMock(options?: { entities?: Entity[] }): CatalogApi {
|
||||
return new InMemoryCatalogClient(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection of mock functionality for the catalog service.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export namespace catalogApiMock {
|
||||
/**
|
||||
* Creates a fake catalog client that handles entities in memory storage. Note
|
||||
* that this client may be severely limited in functionality, and advanced
|
||||
* functions may not be available at all.
|
||||
*/
|
||||
export const factory = (options?: {
|
||||
entities?: Entity[];
|
||||
}): ApiFactory<CatalogApi, CatalogApi, {}> =>
|
||||
createApiFactory({
|
||||
api: catalogApiRef,
|
||||
deps: {},
|
||||
factory: () => new InMemoryCatalogClient(options),
|
||||
});
|
||||
/**
|
||||
* Creates a catalog client whose methods are mock functions, possibly with
|
||||
* some of them overloaded by the caller.
|
||||
*/
|
||||
export const mock = simpleMock(catalogApiRef, () => ({
|
||||
getEntities: jest.fn(),
|
||||
getEntitiesByRefs: jest.fn(),
|
||||
queryEntities: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
getEntityByRef: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityFacets: jest.fn(),
|
||||
getLocationById: jest.fn(),
|
||||
getLocationByRef: jest.fn(),
|
||||
addLocation: jest.fn(),
|
||||
removeLocationById: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
}));
|
||||
}
|
||||
@@ -21,8 +21,8 @@ import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
catalogApiRef,
|
||||
EntityKindFilter,
|
||||
MockEntityListContextProvider,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { ApiProvider } from '@backstage/core-app-api';
|
||||
import {
|
||||
MockErrorApi,
|
||||
|
||||
@@ -23,11 +23,11 @@ import { ApiProvider } from '@backstage/core-app-api';
|
||||
import {
|
||||
EntityKindFilter,
|
||||
entityRouteRef,
|
||||
MockEntityListContextProvider,
|
||||
MockStarredEntitiesApi,
|
||||
starredEntitiesApiRef,
|
||||
UserListFilter,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
|
||||
import { act, fireEvent, screen } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { CursorPaginatedCatalogTable } from './CursorPaginatedCatalogTable';
|
||||
@@ -22,8 +23,8 @@ import {
|
||||
DefaultEntityFilters,
|
||||
EntityKindFilter,
|
||||
EntityListContextProps,
|
||||
MockEntityListContextProvider,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
|
||||
describe('CursorPaginatedCatalogTable', () => {
|
||||
const data = new Array(100).fill(0).map((_, index) => {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { CatalogTableRow } from './types';
|
||||
@@ -20,8 +21,8 @@ import { renderInTestApp } from '@backstage/test-utils';
|
||||
import {
|
||||
DefaultEntityFilters,
|
||||
EntityListContextProps,
|
||||
MockEntityListContextProvider,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { OffsetPaginatedCatalogTable } from './OffsetPaginatedCatalogTable';
|
||||
|
||||
describe('OffsetPaginatedCatalogTable', () => {
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CompoundEntityRef, Entity } from '@backstage/catalog-model';
|
||||
import { useGetEntities } from './useGetEntities';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { getEntityRelations } from '@backstage/plugin-catalog-react';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
|
||||
const givenParentGroup = 'team.squad1';
|
||||
const givenLeafGroup = 'team.squad2';
|
||||
@@ -31,15 +31,14 @@ const givenUserEntity = {
|
||||
},
|
||||
} as Partial<Entity> as Entity;
|
||||
|
||||
const getEntitiesByRefsMock = jest.fn();
|
||||
const catalogApiMock: Pick<CatalogApi, 'getEntities' | 'getEntitiesByRefs'> = {
|
||||
const catalogApi = catalogApiMock.mock({
|
||||
getEntities: jest.fn(async () => Promise.resolve({ items: [] })),
|
||||
getEntitiesByRefs: getEntitiesByRefsMock,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@backstage/core-plugin-api', () => ({
|
||||
useApi: jest.fn(() => catalogApiMock),
|
||||
}));
|
||||
jest.mock('@backstage/core-plugin-api', () => {
|
||||
const actual = jest.requireActual('@backstage/core-plugin-api');
|
||||
return { ...actual, useApi: jest.fn(() => catalogApi) };
|
||||
});
|
||||
|
||||
const getEntityRelationsMock: jest.Mock<
|
||||
CompoundEntityRef[],
|
||||
@@ -50,7 +49,7 @@ jest.mock('@backstage/plugin-catalog-react', () => {
|
||||
catalogApiRef: {},
|
||||
getEntityRelations: jest.fn(entity => {
|
||||
return getEntityRelationsMock(entity);
|
||||
}) as typeof getEntityRelations,
|
||||
}) as any,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -77,16 +76,17 @@ describe('useGetEntities', () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
getEntitiesByRefsMock.mockImplementation(async ({ entityRefs: [ref] }) =>
|
||||
ref.includes(givenParentGroup)
|
||||
? { items: [givenParentGroupEntity] }
|
||||
: { items: [givenLeafGroupEntity] },
|
||||
catalogApi.getEntitiesByRefs.mockImplementation(
|
||||
async ({ entityRefs: [ref] }) =>
|
||||
ref.includes(givenParentGroup)
|
||||
? { items: [givenParentGroupEntity] }
|
||||
: { items: [givenLeafGroupEntity] },
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getEntityRelationsMock.mockRestore();
|
||||
getEntitiesByRefsMock.mockRestore();
|
||||
catalogApi.getEntitiesByRefs.mockRestore();
|
||||
});
|
||||
|
||||
describe('when given entity is a group', () => {
|
||||
@@ -98,7 +98,7 @@ describe('useGetEntities', () => {
|
||||
|
||||
it('should aggregate child ownership', async () => {
|
||||
await whenHookIsCalledWith(givenParentGroupEntity);
|
||||
expect(catalogApiMock.getEntities).toHaveBeenCalledWith(
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith(
|
||||
ownersFilter(
|
||||
`group:default/${givenParentGroup}`,
|
||||
`group:default/${givenLeafGroup}`,
|
||||
@@ -108,7 +108,7 @@ describe('useGetEntities', () => {
|
||||
|
||||
it('should retrieve child with their relations', async () => {
|
||||
await whenHookIsCalledWith(givenParentGroupEntity);
|
||||
expect(catalogApiMock.getEntitiesByRefs).toHaveBeenCalledWith({
|
||||
expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledWith({
|
||||
entityRefs: [`group:default/${givenLeafGroup}`],
|
||||
fields: ['kind', 'metadata.namespace', 'metadata.name', 'relations'],
|
||||
});
|
||||
@@ -121,8 +121,8 @@ describe('useGetEntities', () => {
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
getEntitiesByRefsMock.mockRestore();
|
||||
getEntitiesByRefsMock.mockImplementation(
|
||||
catalogApi.getEntitiesByRefs.mockRestore();
|
||||
catalogApi.getEntitiesByRefs.mockImplementation(
|
||||
async ({ entityRefs: [ref] }) => {
|
||||
if (ref.includes(givenParentGroup)) {
|
||||
return { items: [givenParentGroupEntity] };
|
||||
@@ -152,7 +152,7 @@ describe('useGetEntities', () => {
|
||||
});
|
||||
|
||||
await whenHookIsCalledWith(givenParentGroupEntity);
|
||||
expect(catalogApiMock.getEntities).toHaveBeenCalledWith(
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith(
|
||||
ownersFilter(
|
||||
`group:default/${givenParentGroup}`,
|
||||
`group:default/${givenIntermediateGroup}`,
|
||||
@@ -177,7 +177,7 @@ describe('useGetEntities', () => {
|
||||
});
|
||||
|
||||
await whenHookIsCalledWith(givenParentGroupEntity);
|
||||
expect(catalogApiMock.getEntities).toHaveBeenCalledWith(
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith(
|
||||
ownersFilter(
|
||||
`group:default/${givenParentGroup}`,
|
||||
`group:default/${givenIntermediateGroup}`,
|
||||
@@ -195,7 +195,7 @@ describe('useGetEntities', () => {
|
||||
]);
|
||||
|
||||
await whenHookIsCalledWith(givenUserEntity);
|
||||
expect(catalogApiMock.getEntities).toHaveBeenCalledWith(
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith(
|
||||
ownersFilter(
|
||||
`group:default/${givenLeafGroup}`,
|
||||
`user:default/${givenUser}`,
|
||||
@@ -219,14 +219,14 @@ describe('useGetEntities', () => {
|
||||
|
||||
it('given group entity should return directly owned entities', async () => {
|
||||
await whenHookIsCalledWith(givenLeafGroupEntity);
|
||||
expect(catalogApiMock.getEntities).toHaveBeenCalledWith(
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith(
|
||||
ownersFilter(`group:default/${givenLeafGroup}`),
|
||||
);
|
||||
});
|
||||
|
||||
it('given user entity should return directly owned entities', async () => {
|
||||
await whenHookIsCalledWith(givenUserEntity);
|
||||
expect(catalogApiMock.getEntities).toHaveBeenCalledWith(
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith(
|
||||
ownersFilter(`user:default/${givenUser}`),
|
||||
);
|
||||
});
|
||||
@@ -255,7 +255,7 @@ describe('useGetEntities', () => {
|
||||
? manyGroups.map(group => createGroupRefFromName(group.metadata.name))
|
||||
: [],
|
||||
);
|
||||
(catalogApiMock.getEntities as jest.Mock).mockClear();
|
||||
catalogApi.getEntities.mockClear();
|
||||
});
|
||||
|
||||
it('should handle 500+ relations without exceeding URL length limits', async () => {
|
||||
@@ -270,14 +270,13 @@ describe('useGetEntities', () => {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
const callArgs = (catalogApiMock.getEntities as jest.Mock).mock
|
||||
.calls[0][0];
|
||||
const callArgs = catalogApi.getEntities.mock.calls[0][0];
|
||||
|
||||
expect(
|
||||
callArgs.filter[0]['relations.ownedBy'].length,
|
||||
(callArgs!.filter as any)[0]['relations.ownedBy'].length,
|
||||
).toBeLessThanOrEqual(100);
|
||||
|
||||
const owners = callArgs.filter[0]['relations.ownedBy'];
|
||||
const owners = (callArgs!.filter as any)[0]['relations.ownedBy'];
|
||||
|
||||
expect(Array.isArray(owners)).toBeTruthy();
|
||||
expect(owners.length).toBeLessThanOrEqual(100);
|
||||
@@ -309,7 +308,7 @@ describe('useGetEntities', () => {
|
||||
)
|
||||
: [],
|
||||
);
|
||||
(catalogApiMock.getEntities as jest.Mock).mockClear();
|
||||
catalogApi.getEntities.mockClear();
|
||||
});
|
||||
|
||||
it('should batch the request to avoid exceeding header size limits', async () => {
|
||||
@@ -323,8 +322,7 @@ describe('useGetEntities', () => {
|
||||
await waitFor(() => expect(result.current.loading).toBe(false), {
|
||||
timeout: 5000,
|
||||
});
|
||||
const callArgs = (catalogApiMock.getEntities as jest.Mock).mock
|
||||
.calls[0][0];
|
||||
const callArgs = catalogApi.getEntities.mock.calls[0][0];
|
||||
|
||||
const url = new URL(
|
||||
`http://localhost/api/catalog/entities?${new URLSearchParams(
|
||||
@@ -334,7 +332,7 @@ describe('useGetEntities', () => {
|
||||
const headerSize = url.href.length;
|
||||
expect(headerSize).toBeLessThanOrEqual(16384);
|
||||
|
||||
const owners = callArgs.filter[0]['relations.ownedBy'];
|
||||
const owners = (callArgs!.filter as any)[0]['relations.ownedBy'];
|
||||
expect(Array.isArray(owners)).toBeTruthy();
|
||||
expect(owners.length).toBeLessThanOrEqual(100);
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ jest.mock('../scm', () => ({
|
||||
init: jest.fn(),
|
||||
add: jest.fn(),
|
||||
checkout: jest.fn(),
|
||||
branch: jest.fn(),
|
||||
commit: jest
|
||||
.fn()
|
||||
.mockResolvedValue('220f19cc36b551763d157f1b5e4a4b446165dbd6'),
|
||||
@@ -444,7 +445,7 @@ describe('createBranch', () => {
|
||||
});
|
||||
|
||||
it('create the branch', () => {
|
||||
expect(mockedGit.checkout).toHaveBeenCalledWith({
|
||||
expect(mockedGit.branch).toHaveBeenCalledWith({
|
||||
ref: 'trunk',
|
||||
dir: '/tmp/repo/dir/',
|
||||
});
|
||||
@@ -460,7 +461,7 @@ describe('createBranch', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockedGit.checkout).toHaveBeenCalledWith({
|
||||
expect(mockedGit.branch).toHaveBeenCalledWith({
|
||||
ref: 'trunk',
|
||||
dir: '/tmp/repo/dir/',
|
||||
});
|
||||
|
||||
@@ -178,7 +178,7 @@ export async function createBranch(options: {
|
||||
logger,
|
||||
});
|
||||
|
||||
await git.checkout({ dir, ref });
|
||||
await git.branch({ dir, ref });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,7 +25,8 @@ import React from 'react';
|
||||
import { Workflow } from './Workflow';
|
||||
import { analyticsApiRef } from '@backstage/core-plugin-api';
|
||||
import { ScaffolderApi, scaffolderApiRef } from '../../../api';
|
||||
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
|
||||
const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
|
||||
cancelTask: jest.fn(),
|
||||
@@ -38,14 +39,13 @@ const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
|
||||
listTasks: jest.fn(),
|
||||
autocomplete: jest.fn(),
|
||||
};
|
||||
const catalogApiMock: jest.Mocked<CatalogApi> = {
|
||||
getEntityByRef: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const catalogApi = catalogApiMock.mock();
|
||||
|
||||
const analyticsMock = new MockAnalyticsApi();
|
||||
const apis = TestApiRegistry.from(
|
||||
[scaffolderApiRef, scaffolderApiMock],
|
||||
[catalogApiRef, catalogApiMock],
|
||||
[catalogApiRef, catalogApi],
|
||||
[analyticsApiRef, analyticsMock],
|
||||
);
|
||||
|
||||
|
||||
+8
-9
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ApiProvider } from '@backstage/core-app-api';
|
||||
import { analyticsApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
@@ -30,8 +31,8 @@ import {
|
||||
import { TemplateWizardPage } from './TemplateWizardPage';
|
||||
import { rootRouteRef } from '../../../routes';
|
||||
import { ANNOTATION_EDIT_URL } from '@backstage/catalog-model';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
return {
|
||||
@@ -54,16 +55,14 @@ const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
|
||||
autocomplete: jest.fn(),
|
||||
};
|
||||
|
||||
const catalogApiMock: jest.Mocked<CatalogApi> = {
|
||||
getEntityByRef: jest.fn(),
|
||||
} as any;
|
||||
const catalogApi = catalogApiMock.mock();
|
||||
|
||||
const analyticsMock = new MockAnalyticsApi();
|
||||
const apis = TestApiRegistry.from(
|
||||
[scaffolderApiRef, scaffolderApiMock],
|
||||
[catalogApiRef, catalogApiMock],
|
||||
[catalogApiRef, catalogApi],
|
||||
[analyticsApiRef, analyticsMock],
|
||||
[catalogApiRef, catalogApiMock],
|
||||
[catalogApiRef, catalogApi],
|
||||
);
|
||||
|
||||
const entityRefResponse = {
|
||||
@@ -100,7 +99,7 @@ describe('TemplateWizardPage', () => {
|
||||
],
|
||||
title: 'React JSON Schema Form Test',
|
||||
});
|
||||
catalogApiMock.getEntityByRef.mockResolvedValue(entityRefResponse);
|
||||
catalogApi.getEntityByRef.mockResolvedValue(entityRefResponse);
|
||||
|
||||
const { findByRole, getByRole } = await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
@@ -147,7 +146,7 @@ describe('TemplateWizardPage', () => {
|
||||
});
|
||||
describe('scaffolder page context menu', () => {
|
||||
it('should render if editUrl is set to url', async () => {
|
||||
catalogApiMock.getEntityByRef.mockResolvedValue({
|
||||
catalogApi.getEntityByRef.mockResolvedValue({
|
||||
apiVersion: 'v1',
|
||||
kind: 'service',
|
||||
metadata: {
|
||||
@@ -177,7 +176,7 @@ describe('TemplateWizardPage', () => {
|
||||
expect(queryByTestId('menu-button')).toBeInTheDocument();
|
||||
});
|
||||
it('should not render if editUrl is undefined', async () => {
|
||||
catalogApiMock.getEntityByRef.mockResolvedValue({
|
||||
catalogApi.getEntityByRef.mockResolvedValue({
|
||||
apiVersion: 'v1',
|
||||
kind: 'service',
|
||||
metadata: {
|
||||
|
||||
@@ -22,8 +22,8 @@ import { TemplateTypePicker } from './TemplateTypePicker';
|
||||
import {
|
||||
catalogApiRef,
|
||||
EntityKindFilter,
|
||||
MockEntityListContextProvider,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { AlertApi, alertApiRef } from '@backstage/core-plugin-api';
|
||||
import { ApiProvider } from '@backstage/core-app-api';
|
||||
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
|
||||
|
||||
@@ -24,9 +24,9 @@ import {
|
||||
CatalogApi,
|
||||
catalogApiRef,
|
||||
starredEntitiesApiRef,
|
||||
MockEntityListContextProvider,
|
||||
MockStarredEntitiesApi,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { MockEntityListContextProvider } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import {
|
||||
MockStorageApi,
|
||||
renderInTestApp,
|
||||
|
||||
Reference in New Issue
Block a user