test(docs-infra): run tests in random order (and make them pass) (#31527)

This commit updates the necessary config files to run the angular.io and
docs tooling unit tests in random order (and fixes the tests that were
failing due to their dependence on the previous ordered execution).

Besides being a good idea anyway, running tests in random order is the
new [default behavior in jasmine@3.0.0][1], so this commit is in
preparation of upgrading jasmine to the latest version.

[1]: https://github.com/jasmine/jasmine/blob/v3.0.0/release_notes/3.0.md#breaking-changes

PR Close #31527
This commit is contained in:
George Kalpakas 2019-07-15 15:10:31 +03:00 committed by Miško Hevery
parent e8ae3c5f2e
commit d7ca263cc4
10 changed files with 192 additions and 155 deletions

View File

@ -10,17 +10,22 @@ module.exports = function (config) {
require('karma-chrome-launcher'), require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'), require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'), require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma') require('@angular-devkit/build-angular/plugins/karma'),
{'reporter:jasmine-seed': ['type', JasmineSeedReporter]},
], ],
client: { client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser clearContext: false, // leave Jasmine Spec Runner output visible in browser
jasmine: {
random: true,
seed: '',
},
}, },
coverageIstanbulReporter: { coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/site'), dir: require('path').join(__dirname, './coverage/site'),
reports: ['html', 'lcovonly', 'text-summary'], reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true fixWebpackSourcePaths: true,
}, },
reporters: ['progress', 'kjhtml'], reporters: ['progress', 'kjhtml', 'jasmine-seed'],
port: 9876, port: 9876,
colors: true, colors: true,
logLevel: config.LOG_INFO, logLevel: config.LOG_INFO,
@ -28,6 +33,18 @@ module.exports = function (config) {
browsers: ['Chrome'], browsers: ['Chrome'],
browserNoActivityTimeout: 60000, browserNoActivityTimeout: 60000,
singleRun: false, singleRun: false,
restartOnFileChange: true restartOnFileChange: true,
}); });
}; };
// Helpers
function JasmineSeedReporter(baseReporterDecorator) {
baseReporterDecorator(this);
this.onBrowserComplete = (browser, result) => {
const seed = result.order && result.order.random && result.order.seed;
if (seed) this.write(`${browser}: Randomized with seed ${seed}.\n`);
};
this.onRunComplete = () => undefined;
}

View File

@ -630,6 +630,11 @@ describe('AppComponent', () => {
}; };
beforeEach(() => {
tocContainer = null;
toc = null;
});
it('should show/hide `<aio-toc>` based on `hasFloatingToc`', () => { it('should show/hide `<aio-toc>` based on `hasFloatingToc`', () => {
expect(tocContainer).toBeFalsy(); expect(tocContainer).toBeFalsy();
expect(toc).toBeFalsy(); expect(toc).toBeFalsy();

View File

@ -5,6 +5,8 @@ import { CodeExampleComponent } from './code-example.component';
import { CodeExampleModule } from './code-example.module'; import { CodeExampleModule } from './code-example.module';
import { Logger } from 'app/shared/logger.service'; import { Logger } from 'app/shared/logger.service';
import { MockLogger } from 'testing/logger.service'; import { MockLogger } from 'testing/logger.service';
import { MockPrettyPrinter } from 'testing/pretty-printer.service';
import { PrettyPrinter } from './pretty-printer.service';
describe('CodeExampleComponent', () => { describe('CodeExampleComponent', () => {
let hostComponent: HostComponent; let hostComponent: HostComponent;
@ -19,6 +21,7 @@ describe('CodeExampleComponent', () => {
], ],
providers: [ providers: [
{ provide: Logger, useClass: MockLogger }, { provide: Logger, useClass: MockLogger },
{ provide: PrettyPrinter, useClass: MockPrettyPrinter },
] ]
}); });

View File

@ -2,10 +2,12 @@ import { Component, ViewChild, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Logger } from 'app/shared/logger.service'; import { Logger } from 'app/shared/logger.service';
import { MockLogger } from 'testing/logger.service'; import { MockLogger } from 'testing/logger.service';
import { MockPrettyPrinter } from 'testing/pretty-printer.service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { CodeTabsComponent } from './code-tabs.component'; import { CodeTabsComponent } from './code-tabs.component';
import { CodeTabsModule } from './code-tabs.module'; import { CodeTabsModule } from './code-tabs.module';
import { PrettyPrinter } from './pretty-printer.service';
describe('CodeTabsComponent', () => { describe('CodeTabsComponent', () => {
let fixture: ComponentFixture<HostComponent>; let fixture: ComponentFixture<HostComponent>;
@ -19,6 +21,7 @@ describe('CodeTabsComponent', () => {
schemas: [ NO_ERRORS_SCHEMA ], schemas: [ NO_ERRORS_SCHEMA ],
providers: [ providers: [
{ provide: Logger, useClass: MockLogger }, { provide: Logger, useClass: MockLogger },
{ provide: PrettyPrinter, useClass: MockPrettyPrinter },
] ]
}); });

View File

@ -1,114 +1,104 @@
import { Component, ViewChild, AfterViewInit } from '@angular/core'; import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { first } from 'rxjs/operators';
import { CodeComponent } from './code.component'; import { CodeComponent } from './code.component';
import { CodeModule } from './code.module'; import { CodeModule } from './code.module';
import { CopierService } from 'app/shared//copier.service'; import { CopierService } from 'app/shared//copier.service';
import { Logger } from 'app/shared/logger.service'; import { Logger } from 'app/shared/logger.service';
import { MockPrettyPrinter } from 'testing/pretty-printer.service';
import { PrettyPrinter } from './pretty-printer.service'; import { PrettyPrinter } from './pretty-printer.service';
const oneLineCode = 'const foo = "bar";'; const oneLineCode = 'const foo = "bar";';
const smallMultiLineCode = ` const smallMultiLineCode =
&lt;hero-details&gt; `&lt;hero-details&gt;
&lt;h2&gt;Bah Dah Bing&lt;/h2&gt; &lt;h2&gt;Bah Dah Bing&lt;/h2&gt;
&lt;hero-team&gt; &lt;hero-team&gt;
&lt;h3&gt;NYC Team&lt;/h3&gt; &lt;h3&gt;NYC Team&lt;/h3&gt;
&lt;/hero-team&gt; &lt;/hero-team&gt;
&lt;/hero-details&gt;`; &lt;/hero-details&gt;`;
const bigMultiLineCode = smallMultiLineCode + smallMultiLineCode + smallMultiLineCode; const bigMultiLineCode = `${smallMultiLineCode}\n${smallMultiLineCode}\n${smallMultiLineCode}`;
describe('CodeComponent', () => { describe('CodeComponent', () => {
let hostComponent: HostComponent; let hostComponent: HostComponent;
let fixture: ComponentFixture<HostComponent>; let fixture: ComponentFixture<HostComponent>;
// WARNING: Chance of cross-test pollution
// CodeComponent injects PrettyPrintService
// Once PrettyPrintService runs once _anywhere_, its ctor loads `prettify.js`
// which sets `window['prettyPrintOne']`
// That global survives these tests unless
// we take strict measures to wipe it out in the `afterAll`
// and make sure THAT runs after the tests by making component creation async
afterAll(() => {
delete (window as any)['prettyPrint'];
delete (window as any)['prettyPrintOne'];
});
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ NoopAnimationsModule, CodeModule ], imports: [ NoopAnimationsModule, CodeModule ],
declarations: [ HostComponent ], declarations: [ HostComponent ],
providers: [ providers: [
PrettyPrinter,
CopierService, CopierService,
{provide: Logger, useClass: TestLogger } { provide: Logger, useClass: TestLogger },
{ provide: PrettyPrinter, useClass: MockPrettyPrinter },
] ]
}).compileComponents(); });
});
// Must be async because
// CodeComponent creates PrettyPrintService which async loads `prettify.js`.
// If not async, `afterAll` finishes before tests do!
beforeEach(async(() => {
fixture = TestBed.createComponent(HostComponent); fixture = TestBed.createComponent(HostComponent);
hostComponent = fixture.componentInstance; hostComponent = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
})); });
describe('pretty printing', () => { describe('pretty printing', () => {
const untilCodeFormatted = () => { const getFormattedCode = () => fixture.nativeElement.querySelector('code').innerHTML;
const emitter = hostComponent.codeComponent.codeFormatted;
return emitter.pipe(first()).toPromise();
};
const hasLineNumbers = async () => {
// presence of `<li>`s are a tell-tale for line numbers
await untilCodeFormatted();
return 0 < fixture.nativeElement.querySelectorAll('li').length;
};
it('should format a one-line code sample', async () => { it('should format a one-line code sample without linenums by default', () => {
hostComponent.setCode(oneLineCode); hostComponent.setCode(oneLineCode);
await untilCodeFormatted(); expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: false): ${oneLineCode}`);
// 'pln' spans are a tell-tale for syntax highlighting
const spans = fixture.nativeElement.querySelectorAll('span.pln');
expect(spans.length).toBeGreaterThan(0, 'formatted spans');
}); });
it('should format a one-line code sample without linenums by default', async () => { it('should add line numbers to one-line code sample when linenums is `true`', () => {
hostComponent.setCode(oneLineCode); hostComponent.setCode(oneLineCode);
expect(await hasLineNumbers()).toBe(false); hostComponent.linenums = true;
fixture.detectChanges();
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: true): ${oneLineCode}`);
}); });
it('should add line numbers to one-line code sample when linenums set true', async () => { it('should add line numbers to one-line code sample when linenums is `\'true\'`', () => {
hostComponent.setCode(oneLineCode);
hostComponent.linenums = 'true'; hostComponent.linenums = 'true';
fixture.detectChanges(); fixture.detectChanges();
expect(await hasLineNumbers()).toBe(true); expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: true): ${oneLineCode}`);
}); });
it('should format a small multi-line code without linenums by default', async () => { it('should format a small multi-line code without linenums by default', async () => {
hostComponent.setCode(smallMultiLineCode); hostComponent.setCode(smallMultiLineCode);
expect(await hasLineNumbers()).toBe(false); expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: false): ${smallMultiLineCode}`);
}); });
it('should add line numbers to a big multi-line code by default', async () => { it('should add line numbers to a big multi-line code by default', async () => {
hostComponent.setCode(bigMultiLineCode); hostComponent.setCode(bigMultiLineCode);
expect(await hasLineNumbers()).toBe(true); expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: true): ${bigMultiLineCode}`);
}); });
it('should format big multi-line code without linenums when linenums set false', async () => { it('should format big multi-line code without linenums when linenums is `false`', async () => {
hostComponent.setCode(bigMultiLineCode);
hostComponent.linenums = false; hostComponent.linenums = false;
fixture.detectChanges(); fixture.detectChanges();
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: false): ${bigMultiLineCode}`);
});
it('should format big multi-line code without linenums when linenums is `\'false\'`', async () => {
hostComponent.setCode(bigMultiLineCode); hostComponent.setCode(bigMultiLineCode);
expect(await hasLineNumbers()).toBe(false); hostComponent.linenums = 'false';
fixture.detectChanges();
expect(getFormattedCode()).toBe(
`Formatted code (language: auto, linenums: false): ${bigMultiLineCode}`);
}); });
}); });
@ -117,9 +107,16 @@ describe('CodeComponent', () => {
hostComponent.linenums = false; hostComponent.linenums = false;
fixture.detectChanges(); fixture.detectChanges();
hostComponent.setCode(' abc\n let x = text.split(\'\\n\');\n ghi\n\n jkl\n'); hostComponent.setCode(`
abc
let x = text.split('\\n');
ghi
jkl
`);
const codeContent = fixture.nativeElement.querySelector('code').textContent; const codeContent = fixture.nativeElement.querySelector('code').textContent;
expect(codeContent).toEqual('abc\n let x = text.split(\'\\n\');\nghi\n\njkl'); expect(codeContent).toEqual(
'Formatted code (language: auto, linenums: false): abc\n let x = text.split(\'\\n\');\nghi\n\njkl');
}); });
it('should trim whitespace from the code before rendering', () => { it('should trim whitespace from the code before rendering', () => {

View File

@ -0,0 +1,13 @@
// The actual `PrettyPrinter` service has to load `prettify.js`, which (a) is slow and (b) pollutes
// the global `window` object (which in turn may affect other tests).
// This is a mock implementation that does not load `prettify.js` and does not pollute the global
// scope.
import { of } from 'rxjs';
export class MockPrettyPrinter {
formatCode(code: string, language?: string, linenums?: number | boolean) {
const linenumsStr = (linenums === undefined) ? '' : `, linenums: ${linenums}`;
return of(`Formatted code (language: ${language || 'auto'}${linenumsStr}): ${code}`);
}
}

View File

@ -4,7 +4,7 @@ var Dgeni = require('dgeni');
describe('convertToJson processor', () => { describe('convertToJson processor', () => {
var dgeni, injector, processor, log; var dgeni, injector, processor, log;
beforeAll(function() { beforeEach(function() {
dgeni = new Dgeni([testPackage('angular-base-package')]); dgeni = new Dgeni([testPackage('angular-base-package')]);
injector = dgeni.configureInjector(); injector = dgeni.configureInjector();
processor = injector.get('convertToJsonProcessor'); processor = injector.get('convertToJsonProcessor');

View File

@ -2,13 +2,12 @@ var testPackage = require('../../helpers/test-package');
var Dgeni = require('dgeni'); var Dgeni = require('dgeni');
describe('renderExamples processor', () => { describe('renderExamples processor', () => {
var injector, processor, exampleMap, collectExamples, log; var injector, processor, collectExamples, exampleMap, log;
beforeEach(function() { beforeEach(function() {
const dgeni = new Dgeni([testPackage('examples-package', true)]); const dgeni = new Dgeni([testPackage('examples-package', true)]);
injector = dgeni.configureInjector(); injector = dgeni.configureInjector();
exampleMap = injector.get('exampleMap');
processor = injector.get('renderExamples'); processor = injector.get('renderExamples');
collectExamples = injector.get('collectExamples'); collectExamples = injector.get('collectExamples');
exampleMap = injector.get('exampleMap'); exampleMap = injector.get('exampleMap');

View File

@ -7,113 +7,113 @@ const DEFAULT_PLASTER = '. . .';
const {mapObject} = require('../../helpers/utils'); const {mapObject} = require('../../helpers/utils');
module.exports = function regionParser() { module.exports = function regionParser() {
regionParserImpl.regionMatchers = {
ts: inlineC,
js: inlineC,
es6: inlineC,
dart: inlineC,
html: html,
svg: html,
css: blockC,
yaml: inlineHash,
yml: inlineHash,
jade: inlineCOnly,
pug: inlineCOnly,
json: inlineC,
'json.annotated': inlineC
};
return regionParserImpl; return regionParserImpl;
};
regionParserImpl.regionMatchers = { /**
ts: inlineC, * @param contents string
js: inlineC, * @param fileType string
es6: inlineC, * @returns {contents: string, regions: {[regionName: string]: string}}
dart: inlineC, */
html: html, function regionParserImpl(contents, fileType) {
svg: html, const regionMatcher = regionParserImpl.regionMatchers[fileType];
css: blockC, const openRegions = [];
yaml: inlineHash, const regionMap = {};
yml: inlineHash,
jade: inlineCOnly,
pug: inlineCOnly,
json: inlineC,
'json.annotated': inlineC
};
/** if (regionMatcher) {
* @param contents string let plaster = regionMatcher.createPlasterComment(DEFAULT_PLASTER);
* @param fileType string const lines = contents.split(/\r?\n/).filter((line, index) => {
* @returns {contents: string, regions: {[regionName: string]: string}} const startRegion = line.match(regionMatcher.regionStartMatcher);
*/ const endRegion = line.match(regionMatcher.regionEndMatcher);
function regionParserImpl(contents, fileType) { const updatePlaster = line.match(regionMatcher.plasterMatcher);
const regionMatcher = regionParserImpl.regionMatchers[fileType];
const openRegions = [];
const regionMap = {};
if (regionMatcher) { // start region processing
let plaster = regionMatcher.createPlasterComment(DEFAULT_PLASTER); if (startRegion) {
const lines = contents.split(/\r?\n/).filter((line, index) => { // open up the specified region
const startRegion = line.match(regionMatcher.regionStartMatcher); const regionNames = getRegionNames(startRegion[1]);
const endRegion = line.match(regionMatcher.regionEndMatcher); if (regionNames.length === 0) {
const updatePlaster = line.match(regionMatcher.plasterMatcher); regionNames.push('');
}
regionNames.forEach(regionName => {
const region = regionMap[regionName];
if (region) {
if (region.open) {
throw new RegionParserError(
`Tried to open a region, named "${regionName}", that is already open`, index);
}
region.open = true;
if (plaster) {
region.lines.push(plaster);
}
} else {
regionMap[regionName] = {lines: [], open: true};
}
openRegions.push(regionName);
});
// start region processing // end region processing
if (startRegion) { } else if (endRegion) {
// open up the specified region if (openRegions.length === 0) {
const regionNames = getRegionNames(startRegion[1]); throw new RegionParserError('Tried to close a region when none are open', index);
if (regionNames.length === 0) { }
regionNames.push(''); // close down the specified region (or most recent if no name is given)
} const regionNames = getRegionNames(endRegion[1]);
regionNames.forEach(regionName => { if (regionNames.length === 0) {
const region = regionMap[regionName]; regionNames.push(openRegions[openRegions.length - 1]);
if (region) { }
if (region.open) {
regionNames.forEach(regionName => {
const region = regionMap[regionName];
if (!region || !region.open) {
throw new RegionParserError( throw new RegionParserError(
`Tried to open a region, named "${regionName}", that is already open`, index); `Tried to close a region, named "${regionName}", that is not open`, index);
} }
region.open = true; region.open = false;
if (plaster) { removeLast(openRegions, regionName);
region.lines.push(plaster); });
}
} else {
regionMap[regionName] = {lines: [], open: true};
}
openRegions.push(regionName);
});
// end region processing // doc plaster processing
} else if (endRegion) { } else if (updatePlaster) {
if (openRegions.length === 0) { const plasterString = updatePlaster[1].trim();
throw new RegionParserError('Tried to close a region when none are open', index); plaster = plasterString ? regionMatcher.createPlasterComment(plasterString) : '';
}
// close down the specified region (or most recent if no name is given) // simple line of content processing
const regionNames = getRegionNames(endRegion[1]); } else {
if (regionNames.length === 0) { openRegions.forEach(regionName => regionMap[regionName].lines.push(line));
regionNames.push(openRegions[openRegions.length - 1]); // do not filter out this line from the content
return true;
} }
regionNames.forEach(regionName => { // this line contained an annotation so let's filter it out
const region = regionMap[regionName]; return false;
if (!region || !region.open) { });
throw new RegionParserError( if (!regionMap['']) {
`Tried to close a region, named "${regionName}", that is not open`, index); regionMap[''] = {lines};
}
region.open = false;
removeLast(openRegions, regionName);
});
// doc plaster processing
} else if (updatePlaster) {
const plasterString = updatePlaster[1].trim();
plaster = plasterString ? regionMatcher.createPlasterComment(plasterString) : '';
// simple line of content processing
} else {
openRegions.forEach(regionName => regionMap[regionName].lines.push(line));
// do not filter out this line from the content
return true;
} }
return {
// this line contained an annotation so let's filter it out contents: lines.join('\n'),
return false; regions: mapObject(regionMap, (regionName, region) => leftAlign(region.lines).join('\n'))
}); };
if (!regionMap['']) { } else {
regionMap[''] = {lines}; return {contents, regions: {}};
} }
return {
contents: lines.join('\n'),
regions: mapObject(regionMap, (regionName, region) => leftAlign(region.lines).join('\n'))
};
} else {
return {contents, regions: {}};
} }
} };
function getRegionNames(input) { function getRegionNames(input) {
return (input.trim() === '') ? [] : input.split(',').map(name => name.trim()); return (input.trim() === '') ? [] : input.split(',').map(name => name.trim());

View File

@ -13,5 +13,5 @@
const Jasmine = require('jasmine'); const Jasmine = require('jasmine');
const jasmine = new Jasmine({ projectBaseDir: __dirname }); const jasmine = new Jasmine({ projectBaseDir: __dirname });
jasmine.loadConfig({ spec_files: ['**/*.spec.js'] }); jasmine.loadConfig({ random: true, spec_files: ['**/*.spec.js'] });
jasmine.execute(); jasmine.execute();