Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(angular): switch to using jasmine-marbles for certain symbols #11896

Merged
merged 1 commit into from Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/angular/migrations.json
Expand Up @@ -172,6 +172,12 @@
"version": "14.6.0-beta.0",
"description": "Update the @angular/cli package version to ~14.2.0.",
"factory": "./src/migrations/update-14-6-0/update-angular-cli"
},
"switch-to-jasmine-marbles": {
"cli": "nx",
"version": "15.0.0-beta.0",
"description": "Update the usages of @nrwl/angular/testing to import jasmine-marbles symbols from jasmine-marbles itself.",
"factory": "./src/migrations/update-15-0-0/switch-to-jasmine-marbles"
}
},
"packageJsonUpdates": {
Expand Down
1 change: 0 additions & 1 deletion packages/angular/ng-package.json
Expand Up @@ -12,7 +12,6 @@
"chalk",
"chokidar",
"ignore",
"jasmine-marbles",
"minimatch",
"rxjs-for-await",
"webpack-merge",
Expand Down
1 change: 0 additions & 1 deletion packages/angular/package.json
Expand Up @@ -51,7 +51,6 @@
"chokidar": "^3.5.1",
"http-server": "^14.1.0",
"ignore": "^5.0.4",
"jasmine-marbles": "~0.8.4",
"magic-string": "~0.26.2",
"minimatch": "3.0.5",
"semver": "7.3.4",
Expand Down
@@ -0,0 +1,162 @@
import switchToJasmineMarbles from './switch-to-jasmine-marbles';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import {
addProjectConfiguration,
DependencyType,
ProjectGraph,
readJson,
} from '@nrwl/devkit';
import { jasmineMarblesVersion } from '../../utils/versions';

let projectGraph: ProjectGraph;
jest.mock('@nrwl/devkit', () => ({
...jest.requireActual<any>('@nrwl/devkit'),
readCachedProjectGraph: jest.fn().mockImplementation(() => projectGraph),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => projectGraph),
}));

describe('switchToJasmineMarbles', () => {
it('should correctly migrate a file that is using imports from nrwl/angular/testing that exist in jasmine-marbles', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();

projectGraph = {
nodes: {},
dependencies: {
test: [
{
type: DependencyType.static,
source: 'test',
target: 'npm:@nrwl/angular',
},
],
},
};

addProjectConfiguration(tree, 'test', {
name: 'test',
root: '',
});

tree.write(
'test/a/b/mytest.spec.ts',
`import {hot, cold} from '@nrwl/angular/testing';`
);
tree.write(
'test/c/d/mytest.spec.ts',
`import {hot, getTestScheduler} from '@nrwl/angular/testing';`
);
tree.write(
'test/e/mytest.spec.ts',
`import {getTestScheduler, time} from '@nrwl/angular/testing';`
);

// ACT
await switchToJasmineMarbles(tree);

// ASSERT
expect(tree.read('test/a/b/mytest.spec.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"
import {hot,cold} from 'jasmine-marbles';"
`);
expect(tree.read('test/c/d/mytest.spec.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"
import {hot,getTestScheduler} from 'jasmine-marbles';"
`);
expect(tree.read('test/e/mytest.spec.ts', 'utf-8')).toMatchInlineSnapshot(`
"
import {getTestScheduler,time} from 'jasmine-marbles';"
`);
});

it('should correctly migrate and split imports from nrwl/angular/testing that exist in jasmine-marbles and nrwl/angular/testing', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
projectGraph = {
nodes: {},
dependencies: {
test: [
{
type: DependencyType.static,
source: 'test',
target: 'npm:@nrwl/angular',
},
],
},
};

addProjectConfiguration(tree, 'test', {
name: 'test',
root: '',
});
tree.write(
'a/b/mytest.spec.ts',
`import {hot, cold, readFirst} from '@nrwl/angular/testing';`
);
tree.write(
'c/d/mytest.spec.ts',
`import {hot, getTestScheduler, readAll} from '@nrwl/angular/testing';`
);
tree.write(
'e/mytest.spec.ts',
`import {getTestScheduler, time, readAll, readFirst} from '@nrwl/angular/testing';`
);

// ACT
await switchToJasmineMarbles(tree);

// ASSERT
expect(tree.read('a/b/mytest.spec.ts', 'utf-8')).toMatchInlineSnapshot(`
"import {readFirst} from '@nrwl/angular/testing';
import {hot,cold} from 'jasmine-marbles';"
`);
expect(tree.read('c/d/mytest.spec.ts', 'utf-8')).toMatchInlineSnapshot(`
"import {readAll} from '@nrwl/angular/testing';
import {hot,getTestScheduler} from 'jasmine-marbles';"
`);
expect(tree.read('e/mytest.spec.ts', 'utf-8')).toMatchInlineSnapshot(`
"import {readAll,readFirst} from '@nrwl/angular/testing';
import {getTestScheduler,time} from 'jasmine-marbles';"
`);
});

it('should add jasmine-marbles as a dependency if it does not exist but uses jasmine-marbles symbols in files', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
projectGraph = {
nodes: {},
dependencies: {
test: [
{
type: DependencyType.static,
source: 'test',
target: 'npm:@nrwl/angular',
},
],
},
};

addProjectConfiguration(tree, 'test', {
name: 'test',
root: '',
});
tree.write(
'a/b/mytest.spec.ts',
`import {hot, cold, readFirst} from '@nrwl/angular/testing';`
);

// ACT
await switchToJasmineMarbles(tree);

// ASSERT

const jasmineMarblesDependency = readJson(tree, 'package.json')
.devDependencies['jasmine-marbles'];
expect(jasmineMarblesDependency).toBeTruthy();
expect(jasmineMarblesDependency).toBe(jasmineMarblesVersion);
});
});
@@ -0,0 +1,151 @@
import type { Tree } from '@nrwl/devkit';
import {
addDependenciesToPackageJson,
createProjectGraphAsync,
readCachedProjectGraph,
readJson,
readProjectConfiguration,
visitNotIgnoredFiles,
} from '@nrwl/devkit';
import { extname } from 'path';
import { tsquery } from '@phenomnomnominal/tsquery';
import { jasmineMarblesVersion } from '@nrwl/angular/src/utils/versions';

export default async function switchToJasmineMarbles(tree: Tree) {
const usesJasmineMarbles = await replaceJasmineMarbleUsagesInFiles(tree);
addJasmineMarblesDevDependencyIfUsed(tree, usesJasmineMarbles);
}

async function replaceJasmineMarbleUsagesInFiles(tree: Tree) {
let usesJasmineMarbles = false;

const projectGraph = await (() => {
try {
return readCachedProjectGraph();
} catch {
return createProjectGraphAsync();
}
})();

const dirsToTraverse = Object.entries(projectGraph.dependencies)
.filter(([, dep]) =>
dep.some(({ target }) => target === 'npm:@nrwl/angular')
)
.map(([projectName]) => readProjectConfiguration(tree, projectName).root);

for (const dir of dirsToTraverse) {
visitNotIgnoredFiles(tree, dir, (path) => {
if (extname(path) !== '.ts') {
return;
}

const fileContents = tree.read(path, 'utf-8');
if (!fileContents.includes('@nrwl/angular/testing')) {
return;
}

const NRWL_ANGULAR_TESTING_IMPORT_SELECTOR =
'ImportDeclaration:has(StringLiteral[value="@nrwl/angular/testing"])';
const ast = tsquery.ast(fileContents);
const nrwlAngularTestingImportNodes = tsquery(
ast,
NRWL_ANGULAR_TESTING_IMPORT_SELECTOR,
{ visitAllChildren: true }
);

if (
!nrwlAngularTestingImportNodes ||
nrwlAngularTestingImportNodes.length === 0
) {
return;
}

const jasmineMarblesExportsRegex = new RegExp(
/(hot|cold|getTestScheduler|time)/
);
if (
!jasmineMarblesExportsRegex.test(
nrwlAngularTestingImportNodes[0].getText()
)
) {
return;
}

const IMPORT_SPECIFIERS_SELECTOR = 'NamedImports > ImportSpecifier';
const importSpecifierNodes = tsquery(
nrwlAngularTestingImportNodes[0],
IMPORT_SPECIFIERS_SELECTOR,
{ visitAllChildren: true }
);

if (!importSpecifierNodes || importSpecifierNodes.length === 0) {
return;
}

const validNrwlTestingImports = [];
const validJasmineMarbleImports = [];
for (const node of importSpecifierNodes) {
const importSymbol = node.getText();
if (jasmineMarblesExportsRegex.test(importSymbol)) {
validJasmineMarbleImports.push(importSymbol);
} else {
validNrwlTestingImports.push(importSymbol);
}
}

if (!usesJasmineMarbles && validJasmineMarbleImports.length > 0) {
usesJasmineMarbles = true;
}

const newFileContents = `${fileContents.slice(
0,
nrwlAngularTestingImportNodes[0].getStart()
)}${
validNrwlTestingImports.length > 0
? `import {${validNrwlTestingImports.join(
','
)}} from '@nrwl/angular/testing';`
: ''
}
${
validJasmineMarbleImports.length > 0
? `import {${validJasmineMarbleImports.join(
','
)}} from 'jasmine-marbles';${fileContents.slice(
nrwlAngularTestingImportNodes[0].getEnd(),
-1
)}`
: ''
}`;

tree.write(path, newFileContents);
});
}
return usesJasmineMarbles;
}

function addJasmineMarblesDevDependencyIfUsed(
tree: Tree,
usesJasmineMarbles: boolean
) {
if (!usesJasmineMarbles) {
return;
}

const pkgJson = readJson(tree, 'package.json');
const jasmineMarblesDependency = pkgJson.dependencies['jasmine-marbles'];
const jasmineMarblesDevDependency =
pkgJson.devDependencies['jasmine-marbles'];

if (jasmineMarblesDependency || jasmineMarblesDevDependency) {
return;
}

addDependenciesToPackageJson(
tree,
{},
{
'jasmine-marbles': jasmineMarblesVersion,
}
);
}
1 change: 1 addition & 0 deletions packages/angular/src/utils/versions.ts
Expand Up @@ -30,3 +30,4 @@ export const jasmineSpecReporterVersion = '~7.0.0';
export const typesJasmineVersion = '~4.0.0';
export const typesJasminewd2Version = '~2.0.3';
export const typesNodeVersion = '16.11.7';
export const jasmineMarblesVersion = '^0.9.2';
24 changes: 0 additions & 24 deletions packages/angular/testing/index.ts
@@ -1,25 +1 @@
import {
cold as rxjsMarblesCold,
hot as rxjsMarblesHot,
getTestScheduler as rxjsMarblesTestScheduler,
time as rxjsMarblesTime,
} from 'jasmine-marbles';

/**
* @deprecated Import from 'jasmine-marbles' instead. Will be removed in Nx v15.
*/
export const cold = rxjsMarblesCold;
/**
* @deprecated Import from 'jasmine-marbles' instead. Will be removed in Nx v15.
*/
export const hot = rxjsMarblesHot;
/**
* @deprecated Import from 'jasmine-marbles' instead. Will be removed in Nx v15.
*/
export const getTestScheduler = rxjsMarblesTestScheduler;
/**
* @deprecated Import from 'jasmine-marbles' instead. Will be removed in Nx v15.
*/
export const time = rxjsMarblesTime;

export { readAll, readFirst } from './src/testing-utils';