Merge pull request #9255 from backstage/rugvip/role-jest

cli: bump to jest 27
This commit is contained in:
Patrik Oldsberg
2022-03-25 13:03:44 +01:00
committed by GitHub
30 changed files with 824 additions and 626 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
The logger returned from `getVoidLogger` is now uses a silenced console transport instead.
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/cli': minor
---
**BREAKING**: Bump the version range of `jest` from `^26.0.1` to `^27.5.1`. You can find the complete list of breaking changes [here](https://github.com/facebook/jest/releases/tag/v27.0.0).
We strongly recommend to have completed the [package role migration](https://backstage.io/docs/tutorials/package-role-migration) before upgrading to this version, as the package roles are used to automatically determine the testing environment for each package. If you instead want to set an explicit test environment for each package, you can do so for example in the `"jest"` section in `package.json`. The default test environment for all packages is now `node`, which is also the new Jest default.
Note that one of the breaking changes of Jest 27 is that the `jsdom` environment no longer includes `setImmediate` and `clearImmediate`, which means you might need to update some of your frontend packages. Another notable change is that `jest.useFakeTimers` now defaults to the `'modern'` implementation, which also mocks microtasks.
+1
View File
@@ -170,6 +170,7 @@ memoized
microservice
microservices
microsite
microtasks
middleware
minikube
Minikube
-1
View File
@@ -108,7 +108,6 @@
"aws-sdk-mock": "^5.2.1",
"better-sqlite3": "^7.5.0",
"http-errors": "^2.0.0",
"jest": "^26.0.1",
"mock-fs": "^5.1.0",
"msw": "^0.35.0",
"mysql2": "^2.2.5",
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { PassThrough } from 'stream';
import * as winston from 'winston';
/**
@@ -24,6 +23,6 @@ import * as winston from 'winston';
*/
export function getVoidLogger(): winston.Logger {
return winston.createLogger({
transports: [new winston.transports.Stream({ stream: new PassThrough() })],
transports: [new winston.transports.Console({ silent: true })],
});
}
@@ -130,7 +130,7 @@ describe('AzureUrlReader', () => {
{
url: 'com/a/b/blob/master/path/to/c.yaml',
config: createConfig(),
error: 'Invalid URL: com/a/b/blob/master/path/to/c.yaml',
error: 'Invalid URL',
},
{
url: '',
-1
View File
@@ -51,7 +51,6 @@
"@backstage/backend-test-utils": "^0.1.23-next.0",
"@backstage/cli": "^0.16.1-next.0",
"@types/cron": "^1.7.3",
"jest": "^26.0.1",
"wait-for-expect": "^3.0.2"
},
"files": [
@@ -17,10 +17,10 @@
import { getVoidLogger } from '@backstage/backend-common';
import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
import { Duration } from 'luxon';
import waitForExpect from 'wait-for-expect';
import { migrateBackendTasks } from '../database/migrateBackendTasks';
import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl';
import { ConflictError, NotFoundError } from '@backstage/errors';
import { AbortSignal } from 'node-abort-controller';
jest.useFakeTimers();
@@ -56,6 +56,7 @@ describe('PluginTaskManagerImpl', () => {
const { manager } = await init(databaseId);
const fn = jest.fn();
const promise = new Promise(resolve => fn.mockImplementation(resolve));
await manager.scheduleTask({
id: 'task1',
timeout: Duration.fromMillis(5000),
@@ -63,9 +64,8 @@ describe('PluginTaskManagerImpl', () => {
fn,
});
await waitForExpect(() => {
expect(fn).toBeCalled();
});
await promise;
expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal));
},
60_000,
);
@@ -76,6 +76,7 @@ describe('PluginTaskManagerImpl', () => {
const { manager } = await init(databaseId);
const fn = jest.fn();
const promise = new Promise(resolve => fn.mockImplementation(resolve));
await manager.scheduleTask({
id: 'task2',
timeout: Duration.fromMillis(5000),
@@ -83,9 +84,8 @@ describe('PluginTaskManagerImpl', () => {
fn,
});
await waitForExpect(() => {
expect(fn).toBeCalled();
});
await promise;
expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal));
},
60_000,
);
@@ -98,6 +98,7 @@ describe('PluginTaskManagerImpl', () => {
const { manager } = await init(databaseId);
const fn = jest.fn();
const promise = new Promise(resolve => fn.mockImplementation(resolve));
await manager.scheduleTask({
id: 'task1',
timeout: Duration.fromMillis(5000),
@@ -109,9 +110,8 @@ describe('PluginTaskManagerImpl', () => {
await manager.triggerTask('task1');
jest.advanceTimersByTime(5000);
await waitForExpect(() => {
expect(fn).toBeCalled();
});
await promise;
expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal));
},
60_000,
);
@@ -171,6 +171,7 @@ describe('PluginTaskManagerImpl', () => {
const { manager } = await init(databaseId);
const fn = jest.fn();
const promise = new Promise(resolve => fn.mockImplementation(resolve));
await manager
.createScheduledTaskRunner({
timeout: Duration.fromMillis(5000),
@@ -181,9 +182,8 @@ describe('PluginTaskManagerImpl', () => {
fn,
});
await waitForExpect(() => {
expect(fn).toBeCalled();
});
await promise;
expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal));
},
60_000,
);
@@ -131,11 +131,11 @@ describe('TaskWorker', () => {
cadence: '* * * * * *',
timeoutAfterDuration: Duration.fromMillis(60000).toISO(),
};
const worker = new TaskWorker('task1', fn, knex, logger);
const checkFrequency = Duration.fromObject({ milliseconds: 100 });
const worker = new TaskWorker('task1', fn, knex, logger, checkFrequency);
worker.start(settings);
waitForExpect(() => {
await waitForExpect(() => {
expect(fn).toBeCalledTimes(3);
});
},
+9 -13
View File
@@ -24,7 +24,7 @@ import { TaskFunction, TaskSettingsV2, taskSettingsV2Schema } from './types';
import { delegateAbortController, nowPlus, sleep } from './util';
import { CronTime } from 'cron';
const WORK_CHECK_FREQUENCY = Duration.fromObject({ seconds: 5 });
const DEFAULT_WORK_CHECK_FREQUENCY = Duration.fromObject({ seconds: 5 });
/**
* Performs the actual work of a task.
@@ -32,17 +32,13 @@ const WORK_CHECK_FREQUENCY = Duration.fromObject({ seconds: 5 });
* @private
*/
export class TaskWorker {
private readonly taskId: string;
private readonly fn: TaskFunction;
private readonly knex: Knex;
private readonly logger: Logger;
constructor(taskId: string, fn: TaskFunction, knex: Knex, logger: Logger) {
this.taskId = taskId;
this.fn = fn;
this.knex = knex;
this.logger = logger;
}
constructor(
private readonly taskId: string,
private readonly fn: TaskFunction,
private readonly knex: Knex,
private readonly logger: Logger,
private readonly workCheckFrequency: Duration = DEFAULT_WORK_CHECK_FREQUENCY,
) {}
async start(settings: TaskSettingsV2, options?: { signal?: AbortSignal }) {
try {
@@ -63,7 +59,7 @@ export class TaskWorker {
break;
}
await sleep(WORK_CHECK_FREQUENCY, options?.signal);
await sleep(this.workCheckFrequency, options?.signal);
}
this.logger.info(`Task worker finished: ${this.taskId}`);
} catch (e) {
+1 -2
View File
@@ -46,8 +46,7 @@
"uuid": "^8.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.16.1-next.0",
"jest": "^26.0.1"
"@backstage/cli": "^0.16.1-next.0"
},
"files": [
"dist"
+25
View File
@@ -36,6 +36,25 @@ const transformIgnorePattern = [
'typescript',
].join('|');
// Provides additional config that's based on the role of the target package
function getRoleConfig(role) {
switch (role) {
case 'frontend':
case 'web-library':
case 'common-library':
case 'frontend-plugin':
case 'frontend-plugin-module':
return { testEnvironment: 'jsdom' };
case 'cli':
case 'backend':
case 'node-library':
case 'backend-plugin':
case 'backend-plugin-module':
default:
return { testEnvironment: 'node' };
}
}
async function getProjectConfig(targetPath, displayName) {
const configJsPath = path.resolve(targetPath, 'jest.config.js');
const configTsPath = path.resolve(targetPath, 'jest.config.ts');
@@ -50,6 +69,7 @@ async function getProjectConfig(targetPath, displayName) {
// All configs are merged together to create the final config, with longer paths taking precedence.
// The merging of the configs is shallow, meaning e.g. all transforms are replaced if new ones are defined.
const pkgJsonConfigs = [];
let closestPkgJson = undefined;
let currentPath = targetPath;
// Some sanity check to avoid infinite loop
@@ -59,6 +79,9 @@ async function getProjectConfig(targetPath, displayName) {
if (exists) {
try {
const data = fs.readJsonSync(packagePath);
if (!closestPkgJson) {
closestPkgJson = data;
}
if (data.jest) {
pkgJsonConfigs.unshift(data.jest);
}
@@ -115,6 +138,8 @@ async function getProjectConfig(targetPath, displayName) {
testMatch: ['**/*.test.{js,jsx,ts,tsx,mjs,cjs}'],
transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`],
...getRoleConfig(closestPkgJson?.backstage?.role),
};
// Use src/setupTests.ts as the default location for configuring test env
+1 -1
View File
@@ -85,7 +85,7 @@
"handlebars": "^4.7.3",
"html-webpack-plugin": "^5.3.1",
"inquirer": "^8.2.0",
"jest": "^26.0.1",
"jest": "^27.5.1",
"jest-css-modules": "^2.1.0",
"jest-transform-yaml": "^1.0.0",
"json-schema": "^0.4.0",
@@ -220,7 +220,7 @@ describe('GoogleAnalytics', () => {
});
// Wait for any/all promises involved to settle.
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setTimeout(resolve));
// A pageview should have been fired immediately.
const [command, data] = ReactGA.testModeAPI.calls[1];
@@ -251,7 +251,7 @@ describe('GoogleAnalytics', () => {
});
// Wait for any/all promises involved to settle.
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setTimeout(resolve));
// A pageview should have been fired immediately.
const [command, data] = ReactGA.testModeAPI.calls[1];
@@ -290,7 +290,7 @@ describe('GoogleAnalytics', () => {
});
// Wait for any/all promises involved to settle.
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setTimeout(resolve));
// User ID should have been set after the pageview.
const [setCommand, setData] = ReactGA.testModeAPI.calls[2];
@@ -318,7 +318,7 @@ describe('GoogleAnalytics', () => {
});
// Wait for any/all promises involved to settle.
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setTimeout(resolve));
// A pageview should have been fired immediately.
const [command, data] = ReactGA.testModeAPI.calls[1];
@@ -369,7 +369,7 @@ describe('GoogleAnalytics', () => {
});
// Wait for any/all promises involved to settle.
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setTimeout(resolve));
// User ID should have been set first.
const [setCommand, setData] = ReactGA.testModeAPI.calls[1];
@@ -44,6 +44,8 @@ jest.mock('@google-cloud/firestore', () => ({
Firestore: jest.fn().mockImplementation(() => firestoreMock),
}));
jest.useFakeTimers('legacy');
describe('FirestoreKeyStore', () => {
const key = {
kid: '123',
@@ -69,11 +71,6 @@ describe('FirestoreKeyStore', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('can create an instance without settings', async () => {
@@ -345,7 +345,7 @@ describe('PlaceholderProcessor', () => {
},
),
).rejects.toThrow(
'Placeholder $text could not form a URL out of ./a/b/catalog-info.yaml and ../c/catalog-info.yaml, TypeError: Invalid base URL: ./a/b/catalog-info.yaml',
/^Placeholder \$text could not form a URL out of \.\/a\/b\/catalog-info\.yaml and \.\.\/c\/catalog-info\.yaml, TypeError \[ERR_INVALID_URL\]/,
);
expect(read).not.toBeCalled();
+1 -2
View File
@@ -61,8 +61,7 @@
"@backstage/cli": "^0.16.1-next.0",
"@types/aws4": "^1.5.1",
"supertest": "^6.1.3",
"aws-sdk-mock": "^5.2.1",
"bdd-lazy-var": "^2.6.0"
"aws-sdk-mock": "^5.2.1"
},
"files": [
"dist",
@@ -16,7 +16,6 @@
import AWS from 'aws-sdk';
import AWSMock from 'aws-sdk-mock';
import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator';
import { get, def } from 'bdd-lazy-var';
describe('AwsIamKubernetesAuthTranslator tests', () => {
let role: any = undefined;
@@ -48,7 +47,7 @@ describe('AwsIamKubernetesAuthTranslator tests', () => {
jest.resetAllMocks();
});
def('subject', () => {
function executeTranslation() {
AWSMock.mock('STS', 'assumeRole', (_params: any, callback: Function) => {
callback(null, assumeResponse);
});
@@ -71,7 +70,7 @@ describe('AwsIamKubernetesAuthTranslator tests', () => {
mockedCredentials = undefined;
return response;
});
}
it('returns a signed url for AWS credentials', async () => {
// These credentials are not real.
@@ -81,8 +80,8 @@ describe('AwsIamKubernetesAuthTranslator tests', () => {
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
);
const subject = await get('subject');
expect(subject.serviceAccountToken).toBeDefined();
const response = await executeTranslation();
expect(response.serviceAccountToken).toBeDefined();
});
describe('When the role is assumed', () => {
@@ -96,15 +95,17 @@ describe('AwsIamKubernetesAuthTranslator tests', () => {
describe('When the role is valid', () => {
it('returns a signed url for AWS credentials', async () => {
const subject = await get('subject');
expect(subject.serviceAccountToken).toBeDefined();
const response = await executeTranslation();
expect(response.serviceAccountToken).toBeDefined();
});
});
describe('When the role is invalid', () => {
it('returns the original AWS credentials', async () => {
assumeResponse = undefined;
await expect(get('subject')).rejects.toThrow(/Unable to assume role:/);
await expect(executeTranslation()).rejects.toThrow(
/Unable to assume role:/,
);
});
});
});
@@ -112,7 +113,9 @@ describe('AwsIamKubernetesAuthTranslator tests', () => {
describe('When no AWS creds are available', () => {
it('throws unable to get AWS credentials', async () => {
mockedCredentials = new Error();
await expect(get('subject')).rejects.toThrow('No AWS credentials found.');
await expect(executeTranslation()).rejects.toThrow(
'No AWS credentials found.',
);
});
});
@@ -123,7 +126,7 @@ describe('AwsIamKubernetesAuthTranslator tests', () => {
'AKIAIOSFODNN7EXAMPLE',
undefinedSecret,
);
await expect(get('subject')).rejects.toThrow(
await expect(executeTranslation()).rejects.toThrow(
'Invalid AWS credentials found.',
);
});
@@ -25,6 +25,9 @@ import { ConfigReader } from '@backstage/config';
import { TaskContext, TaskSecrets } from './types';
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
// The Stream module is lazy loaded, so make sure it's in the module cache before mocking fs
void winston.transports.Stream;
const realFiles = Object.fromEntries(
[
resolvePackagePath(
@@ -187,7 +187,7 @@ describe('StorageTaskBroker', () => {
.mockRejectedValue(new Error('nah m8'));
const intervalId = setInterval(() => {
broker.vacuumTasks({ timeoutS: 2 }).catch(fail);
broker.vacuumTasks({ timeoutS: 2 });
}, 500);
for (;;) {
@@ -42,8 +42,5 @@
"dist",
"config.d.ts"
],
"jest": {
"testEnvironment": "node"
},
"configSchema": "config.d.ts"
}
@@ -18,7 +18,11 @@ import { runPeriodically } from './runPeriodically';
jest.useFakeTimers();
describe('runPeriodically', () => {
const flushPromises = () => new Promise(setImmediate);
const flushPromises = async () => {
const promise = new Promise(resolve => process.nextTick(resolve));
jest.runAllTicks();
await promise;
};
const advanceTimersByTime = async (time: number) => {
jest.advanceTimersByTime(time);
// Advancing the time with jest doesn't run all promises, but only sync code
@@ -21,19 +21,22 @@ import { LocalStoredShortcuts } from './LocalStoredShortcuts';
import { ShortcutApi } from './ShortcutApi';
describe('LocalStoredShortcuts', () => {
// eslint-disable-next-line jest/no-done-callback
it('should observe shortcuts', async done => {
it('should observe shortcuts', async () => {
const shortcutApi: ShortcutApi = new LocalStoredShortcuts(
MockStorageApi.create(),
);
const shortcut: Shortcut = { id: 'id', title: 'title', url: '/url' };
await shortcutApi.add(shortcut);
shortcutApi.shortcut$().subscribe(data => {
expect(data).toEqual(
expect.arrayContaining([{ ...shortcut, id: expect.anything() }]),
);
done();
await new Promise<void>(resolve => {
const subscription = shortcutApi.shortcut$().subscribe(data => {
expect(data).toEqual(
expect.arrayContaining([{ ...shortcut, id: expect.anything() }]),
);
subscription.unsubscribe();
resolve();
});
});
});
@@ -26,7 +26,6 @@ import { DatabaseManager, getVoidLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
import { TaskScheduler } from '@backstage/backend-tasks';
import waitForExpect from 'wait-for-expect';
jest.useFakeTimers();
@@ -145,7 +144,7 @@ describe('FactRetrieverEngine', () => {
}
it.each(databases.eachSupportedId())(
'Should update fact retriever schemas on initialization',
'Should update fact retriever schemas on initialization with %s',
async databaseId => {
const schemaAssertionCallback = jest.fn((def: FactSchemaDefinition) => {
expect(def.id).toEqual('test_factretriever');
@@ -170,7 +169,7 @@ describe('FactRetrieverEngine', () => {
);
it.each(databases.eachSupportedId())(
'Should insert facts when scheduled step is run',
'Should insert facts when scheduled step is run with %s',
async databaseId => {
function insertCallback({
facts,
@@ -193,7 +192,14 @@ describe('FactRetrieverEngine', () => {
});
}
engine = await createEngine(databaseId, insertCallback, () => {});
const handler = jest.fn();
engine = await createEngine(
databaseId,
insertCallback,
() => {},
undefined,
{ ...testFactRetriever, handler },
);
await engine.schedule();
const job: FactRetrieverRegistration = engine.getJobRegistration(
testFactRetriever.id,
@@ -203,13 +209,15 @@ describe('FactRetrieverEngine', () => {
await engine.triggerJob(job.factRetriever.id);
jest.advanceTimersByTime(5000);
await waitForExpect(() => {
expect(testFactRetriever.handler).toHaveBeenCalledWith(
expect.objectContaining({
entityFilter: testFactRetriever.entityFilter,
}),
);
});
const handlerParam = await new Promise(resolve =>
handler.mockImplementation(resolve),
);
await expect(handlerParam).toEqual(
expect.objectContaining({
entityFilter: testFactRetriever.entityFilter,
}),
);
},
60_000,
);
@@ -116,10 +116,11 @@ const additionalFacts = [
];
describe('Tech Insights database', () => {
const databases = TestDatabases.create();
let store: TechInsightsStore;
let testDbClient: Knex<any, unknown[]>;
beforeAll(async () => {
testDbClient = await TestDatabases.create().init('SQLITE_3');
testDbClient = await databases.init('SQLITE_3');
store = (
await initializePersistenceContext(testDbClient, {
@@ -51,7 +51,7 @@ describe('RadarComponent', () => {
jest.useFakeTimers();
const errorApi = { post: () => {} };
const { getByTestId, queryByTestId } = render(
const { getByTestId, findByTestId } = render(
<ThemeProvider theme={lightTheme}>
<TestApiProvider
apis={[
@@ -73,7 +73,7 @@ describe('RadarComponent', () => {
});
expect(getByTestId('progress')).toBeInTheDocument();
await waitFor(() => queryByTestId('tech-radar-svg'));
await findByTestId('tech-radar-svg');
jest.useRealTimers();
});
@@ -59,7 +59,7 @@ describe('RadarPage', () => {
svgProps: { 'data-testid': 'tech-radar-svg' },
};
const { getByTestId, queryByTestId } = render(
const { getByTestId, findByTestId } = render(
wrapInTestApp(
<ThemeProvider theme={lightTheme}>
<TestApiProvider apis={[[techRadarApiRef, mockClient]]}>
@@ -74,7 +74,7 @@ describe('RadarPage', () => {
});
expect(getByTestId('progress')).toBeInTheDocument();
await waitFor(() => queryByTestId('tech-radar-svg'));
await findByTestId('tech-radar-svg');
jest.useRealTimers();
});
@@ -86,7 +86,7 @@ describe('RadarPage', () => {
};
jest.spyOn(mockClient, 'load');
const { getByText, getByTestId } = await renderInTestApp(
const { getByText, findByTestId } = await renderInTestApp(
<ThemeProvider theme={lightTheme}>
<TestApiProvider apis={[[techRadarApiRef, mockClient]]}>
<RadarPage {...techRadarProps} />
@@ -94,12 +94,10 @@ describe('RadarPage', () => {
</ThemeProvider>,
);
await waitFor(() => getByTestId('tech-radar-svg'));
await expect(findByTestId('tech-radar-svg')).resolves.toBeInTheDocument();
expect(
getByText('Pick the recommended technologies for your projects'),
).toBeInTheDocument();
expect(getByTestId('tech-radar-svg')).toBeInTheDocument();
expect(mockClient.load).toBeCalledWith(undefined);
});
@@ -112,7 +110,7 @@ describe('RadarPage', () => {
};
jest.spyOn(mockClient, 'load');
const { getByTestId } = await renderInTestApp(
const { findByTestId } = await renderInTestApp(
<ThemeProvider theme={lightTheme}>
<TestApiProvider apis={[[techRadarApiRef, mockClient]]}>
<RadarPage {...techRadarProps} />
@@ -120,9 +118,7 @@ describe('RadarPage', () => {
</ThemeProvider>,
);
await waitFor(() => getByTestId('tech-radar-svg'));
expect(getByTestId('tech-radar-svg')).toBeInTheDocument();
await expect(findByTestId('tech-radar-svg')).resolves.toBeInTheDocument();
expect(mockClient.load).toBeCalledWith('myId');
});
@@ -34,6 +34,7 @@ class GetBBoxPolyfill {
Object.defineProperty(window.Element.prototype, 'getBBox', {
writable: false,
configurable: true,
value: () => ({ x, y, width, height }),
});
}
+2 -2
View File
@@ -221,7 +221,7 @@ describe('TechDocsStorageClient', () => {
const promise = storageApi.syncEntityDocs(mockEntity).then();
// flush the event loop
await new Promise(setImmediate);
await new Promise(r => setTimeout(r));
const instance = MockedEventSource.mock
.instances[0] as jest.Mocked<EventSource>;
@@ -248,7 +248,7 @@ describe('TechDocsStorageClient', () => {
const promise = storageApi.syncEntityDocs(mockEntity).then();
// flush the event loop
await new Promise(setImmediate);
await new Promise(r => setTimeout(r));
const instance = MockedEventSource.mock
.instances[0] as jest.Mocked<EventSource>;
+682 -528
View File
File diff suppressed because it is too large Load Diff