diff --git a/aio/karma.conf.js b/aio/karma.conf.js index e580e5bfdd..f6832bf81a 100644 --- a/aio/karma.conf.js +++ b/aio/karma.conf.js @@ -10,17 +10,22 @@ module.exports = function (config) { require('karma-chrome-launcher'), require('karma-jasmine-html-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: { - 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: { dir: require('path').join(__dirname, './coverage/site'), reports: ['html', 'lcovonly', 'text-summary'], - fixWebpackSourcePaths: true + fixWebpackSourcePaths: true, }, - reporters: ['progress', 'kjhtml'], + reporters: ['progress', 'kjhtml', 'jasmine-seed'], port: 9876, colors: true, logLevel: config.LOG_INFO, @@ -28,6 +33,18 @@ module.exports = function (config) { browsers: ['Chrome'], browserNoActivityTimeout: 60000, 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; +} diff --git a/aio/src/app/app.component.spec.ts b/aio/src/app/app.component.spec.ts index 00f44c7476..54b069ba84 100644 --- a/aio/src/app/app.component.spec.ts +++ b/aio/src/app/app.component.spec.ts @@ -630,6 +630,11 @@ describe('AppComponent', () => { }; + beforeEach(() => { + tocContainer = null; + toc = null; + }); + it('should show/hide `` based on `hasFloatingToc`', () => { expect(tocContainer).toBeFalsy(); expect(toc).toBeFalsy(); diff --git a/aio/src/app/custom-elements/code/code-example.component.spec.ts b/aio/src/app/custom-elements/code/code-example.component.spec.ts index 54431dad80..1183e623de 100644 --- a/aio/src/app/custom-elements/code/code-example.component.spec.ts +++ b/aio/src/app/custom-elements/code/code-example.component.spec.ts @@ -5,6 +5,8 @@ import { CodeExampleComponent } from './code-example.component'; import { CodeExampleModule } from './code-example.module'; import { Logger } from 'app/shared/logger.service'; import { MockLogger } from 'testing/logger.service'; +import { MockPrettyPrinter } from 'testing/pretty-printer.service'; +import { PrettyPrinter } from './pretty-printer.service'; describe('CodeExampleComponent', () => { let hostComponent: HostComponent; @@ -19,6 +21,7 @@ describe('CodeExampleComponent', () => { ], providers: [ { provide: Logger, useClass: MockLogger }, + { provide: PrettyPrinter, useClass: MockPrettyPrinter }, ] }); diff --git a/aio/src/app/custom-elements/code/code-tabs.component.spec.ts b/aio/src/app/custom-elements/code/code-tabs.component.spec.ts index a7be5e8f4c..b922219a10 100644 --- a/aio/src/app/custom-elements/code/code-tabs.component.spec.ts +++ b/aio/src/app/custom-elements/code/code-tabs.component.spec.ts @@ -2,10 +2,12 @@ import { Component, ViewChild, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Logger } from 'app/shared/logger.service'; import { MockLogger } from 'testing/logger.service'; +import { MockPrettyPrinter } from 'testing/pretty-printer.service'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { CodeTabsComponent } from './code-tabs.component'; import { CodeTabsModule } from './code-tabs.module'; +import { PrettyPrinter } from './pretty-printer.service'; describe('CodeTabsComponent', () => { let fixture: ComponentFixture; @@ -19,6 +21,7 @@ describe('CodeTabsComponent', () => { schemas: [ NO_ERRORS_SCHEMA ], providers: [ { provide: Logger, useClass: MockLogger }, + { provide: PrettyPrinter, useClass: MockPrettyPrinter }, ] }); diff --git a/aio/src/app/custom-elements/code/code.component.spec.ts b/aio/src/app/custom-elements/code/code.component.spec.ts index 320542fbe0..1e9937169a 100644 --- a/aio/src/app/custom-elements/code/code.component.spec.ts +++ b/aio/src/app/custom-elements/code/code.component.spec.ts @@ -1,114 +1,104 @@ 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 { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { first } from 'rxjs/operators'; import { CodeComponent } from './code.component'; import { CodeModule } from './code.module'; import { CopierService } from 'app/shared//copier.service'; import { Logger } from 'app/shared/logger.service'; +import { MockPrettyPrinter } from 'testing/pretty-printer.service'; import { PrettyPrinter } from './pretty-printer.service'; const oneLineCode = 'const foo = "bar";'; -const smallMultiLineCode = ` -<hero-details> +const smallMultiLineCode = +`<hero-details> <h2>Bah Dah Bing</h2> <hero-team> <h3>NYC Team</h3> </hero-team> </hero-details>`; -const bigMultiLineCode = smallMultiLineCode + smallMultiLineCode + smallMultiLineCode; +const bigMultiLineCode = `${smallMultiLineCode}\n${smallMultiLineCode}\n${smallMultiLineCode}`; describe('CodeComponent', () => { let hostComponent: HostComponent; let fixture: ComponentFixture; - // 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(() => { TestBed.configureTestingModule({ imports: [ NoopAnimationsModule, CodeModule ], declarations: [ HostComponent ], providers: [ - PrettyPrinter, 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); hostComponent = fixture.componentInstance; fixture.detectChanges(); - })); + }); describe('pretty printing', () => { - const untilCodeFormatted = () => { - const emitter = hostComponent.codeComponent.codeFormatted; - return emitter.pipe(first()).toPromise(); - }; - const hasLineNumbers = async () => { - // presence of `
  • `s are a tell-tale for line numbers - await untilCodeFormatted(); - return 0 < fixture.nativeElement.querySelectorAll('li').length; - }; + const getFormattedCode = () => fixture.nativeElement.querySelector('code').innerHTML; - it('should format a one-line code sample', async () => { + it('should format a one-line code sample without linenums by default', () => { hostComponent.setCode(oneLineCode); - await untilCodeFormatted(); - - // 'pln' spans are a tell-tale for syntax highlighting - const spans = fixture.nativeElement.querySelectorAll('span.pln'); - expect(spans.length).toBeGreaterThan(0, 'formatted spans'); + expect(getFormattedCode()).toBe( + `Formatted code (language: auto, linenums: false): ${oneLineCode}`); }); - 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); - 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'; 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 () => { 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 () => { 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; 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); - 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; 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; - 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', () => { diff --git a/aio/src/testing/pretty-printer.service.ts b/aio/src/testing/pretty-printer.service.ts new file mode 100644 index 0000000000..20179f46c4 --- /dev/null +++ b/aio/src/testing/pretty-printer.service.ts @@ -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}`); + } +} diff --git a/aio/tools/transforms/angular-base-package/processors/convertToJson.spec.js b/aio/tools/transforms/angular-base-package/processors/convertToJson.spec.js index b68f1527de..7fe623aaca 100644 --- a/aio/tools/transforms/angular-base-package/processors/convertToJson.spec.js +++ b/aio/tools/transforms/angular-base-package/processors/convertToJson.spec.js @@ -4,7 +4,7 @@ var Dgeni = require('dgeni'); describe('convertToJson processor', () => { var dgeni, injector, processor, log; - beforeAll(function() { + beforeEach(function() { dgeni = new Dgeni([testPackage('angular-base-package')]); injector = dgeni.configureInjector(); processor = injector.get('convertToJsonProcessor'); @@ -72,4 +72,4 @@ describe('convertToJson processor', () => { processor.$process(docs); expect(log.warn).toHaveBeenCalledWith('Title property expected - doc (test-doc) '); }); -}); \ No newline at end of file +}); diff --git a/aio/tools/transforms/examples-package/processors/render-examples.spec.js b/aio/tools/transforms/examples-package/processors/render-examples.spec.js index baab8e3343..e72b515600 100644 --- a/aio/tools/transforms/examples-package/processors/render-examples.spec.js +++ b/aio/tools/transforms/examples-package/processors/render-examples.spec.js @@ -2,13 +2,12 @@ var testPackage = require('../../helpers/test-package'); var Dgeni = require('dgeni'); describe('renderExamples processor', () => { - var injector, processor, exampleMap, collectExamples, log; + var injector, processor, collectExamples, exampleMap, log; beforeEach(function() { const dgeni = new Dgeni([testPackage('examples-package', true)]); injector = dgeni.configureInjector(); - exampleMap = injector.get('exampleMap'); processor = injector.get('renderExamples'); collectExamples = injector.get('collectExamples'); exampleMap = injector.get('exampleMap'); diff --git a/aio/tools/transforms/examples-package/services/region-parser.js b/aio/tools/transforms/examples-package/services/region-parser.js index cf6be977a8..e4c1f365dd 100644 --- a/aio/tools/transforms/examples-package/services/region-parser.js +++ b/aio/tools/transforms/examples-package/services/region-parser.js @@ -7,113 +7,113 @@ const DEFAULT_PLASTER = '. . .'; const {mapObject} = require('../../helpers/utils'); 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; -}; -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 -}; + /** + * @param contents string + * @param fileType string + * @returns {contents: string, regions: {[regionName: string]: string}} + */ + function regionParserImpl(contents, fileType) { + const regionMatcher = regionParserImpl.regionMatchers[fileType]; + const openRegions = []; + const regionMap = {}; -/** - * @param contents string - * @param fileType string - * @returns {contents: string, regions: {[regionName: string]: string}} - */ -function regionParserImpl(contents, fileType) { - const regionMatcher = regionParserImpl.regionMatchers[fileType]; - const openRegions = []; - const regionMap = {}; + if (regionMatcher) { + let plaster = regionMatcher.createPlasterComment(DEFAULT_PLASTER); + const lines = contents.split(/\r?\n/).filter((line, index) => { + const startRegion = line.match(regionMatcher.regionStartMatcher); + const endRegion = line.match(regionMatcher.regionEndMatcher); + const updatePlaster = line.match(regionMatcher.plasterMatcher); - if (regionMatcher) { - let plaster = regionMatcher.createPlasterComment(DEFAULT_PLASTER); - const lines = contents.split(/\r?\n/).filter((line, index) => { - const startRegion = line.match(regionMatcher.regionStartMatcher); - const endRegion = line.match(regionMatcher.regionEndMatcher); - const updatePlaster = line.match(regionMatcher.plasterMatcher); + // start region processing + if (startRegion) { + // open up the specified region + const regionNames = getRegionNames(startRegion[1]); + if (regionNames.length === 0) { + 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 - if (startRegion) { - // open up the specified region - const regionNames = getRegionNames(startRegion[1]); - if (regionNames.length === 0) { - regionNames.push(''); - } - regionNames.forEach(regionName => { - const region = regionMap[regionName]; - if (region) { - if (region.open) { + // end region processing + } else if (endRegion) { + if (openRegions.length === 0) { + throw new RegionParserError('Tried to close a region when none are open', index); + } + // close down the specified region (or most recent if no name is given) + const regionNames = getRegionNames(endRegion[1]); + if (regionNames.length === 0) { + regionNames.push(openRegions[openRegions.length - 1]); + } + + regionNames.forEach(regionName => { + const region = regionMap[regionName]; + if (!region || !region.open) { 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; - if (plaster) { - region.lines.push(plaster); - } - } else { - regionMap[regionName] = {lines: [], open: true}; - } - openRegions.push(regionName); - }); + region.open = false; + removeLast(openRegions, regionName); + }); - // end region processing - } else if (endRegion) { - if (openRegions.length === 0) { - throw new RegionParserError('Tried to close a region when none are open', index); - } - // close down the specified region (or most recent if no name is given) - const regionNames = getRegionNames(endRegion[1]); - if (regionNames.length === 0) { - regionNames.push(openRegions[openRegions.length - 1]); + // 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; } - regionNames.forEach(regionName => { - const region = regionMap[regionName]; - if (!region || !region.open) { - throw new RegionParserError( - `Tried to close a region, named "${regionName}", that is not open`, index); - } - 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; + // this line contained an annotation so let's filter it out + return false; + }); + if (!regionMap['']) { + regionMap[''] = {lines}; } - - // this line contained an annotation so let's filter it out - return false; - }); - if (!regionMap['']) { - regionMap[''] = {lines}; + return { + contents: lines.join('\n'), + regions: mapObject(regionMap, (regionName, region) => leftAlign(region.lines).join('\n')) + }; + } else { + 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) { return (input.trim() === '') ? [] : input.split(',').map(name => name.trim()); diff --git a/aio/tools/transforms/test.js b/aio/tools/transforms/test.js index 48967ddd45..b8d3671461 100644 --- a/aio/tools/transforms/test.js +++ b/aio/tools/transforms/test.js @@ -13,5 +13,5 @@ const Jasmine = require('jasmine'); const jasmine = new Jasmine({ projectBaseDir: __dirname }); -jasmine.loadConfig({ spec_files: ['**/*.spec.js'] }); +jasmine.loadConfig({ random: true, spec_files: ['**/*.spec.js'] }); jasmine.execute();