Skip to content

Commit

Permalink
Beef up unit tests for areas with low coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
clbond committed Apr 17, 2017
1 parent 7528327 commit d5f372f
Show file tree
Hide file tree
Showing 26 changed files with 225 additions and 52 deletions.
2 changes: 1 addition & 1 deletion examples/cli/package.json
Expand Up @@ -6,7 +6,7 @@
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"postbuild": "ng-render",
"postbuild": "ng-render --preboot=preboot.json",
"lint": "ng lint"
},
"private": true,
Expand Down
3 changes: 3 additions & 0 deletions examples/cli/preboot.json
@@ -0,0 +1,3 @@
{
"appRoot": "app-root"
}
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "angular-ssr",
"version": "0.1.72",
"version": "0.1.73",
"description": "Angular server-side rendering implementation",
"main": "build/index.js",
"typings": "build/index.d.ts",
Expand Down
4 changes: 2 additions & 2 deletions source/application/builder/builder-base.ts
Expand Up @@ -3,7 +3,7 @@ import {ApplicationBuilder} from './builder';
import {ApplicationBootstrapper, ApplicationStateReader, Postprocessor, VariantsMap} from '../contracts';
import {ConfigurationException} from '../../exception';
import {FileReference, fileFromString} from '../../filesystem';
import {Preboot, PrebootConfiguration} from '../preboot';
import {PrebootQueryable, PrebootConfiguration} from '../preboot';
import {RenderOperation} from '../operation';
import {Route} from '../../route';

Expand Down Expand Up @@ -61,7 +61,7 @@ export abstract class ApplicationBuilderBase<V> implements ApplicationBuilder<V>

preboot(config?: PrebootConfiguration) {
if (config != null) {
this.operation.preboot = config as Preboot;
this.operation.preboot = config as PrebootQueryable;
}
return this.operation.preboot as PrebootConfiguration;
}
Expand Down
91 changes: 91 additions & 0 deletions source/application/builder/tests/from-module.ts
Expand Up @@ -196,4 +196,95 @@ describe('ApplicationFromModule', () => {
application.dispose();
}
});

it('can apply postprocessor to rendered document string', async () => {
const application = loadApplicationFixtureFromModule(BasicInlineModule,
b => b.postprocess((document, rendered) => rendered.replace('Hello!', 'What up sucka!')));

try {
const snapshots = await application.prerender();

return await new Promise((resolve, reject) => {
snapshots.subscribe(
snapshot => {
const expr = /<application ng-version="([^"]+)"><div>What up sucka!<\/div><\/application>/;
expect(snapshot.exceptions).not.toBeNull();
expect(snapshot.exceptions.length).toBe(0);
expect(snapshot.variant).toBeUndefined();
expect(snapshot.applicationState).toBeUndefined();
expect(expr.test(trimDocument(snapshot.renderedDocument))).toBeTruthy();
resolve();
},
exception => reject(exception));
});
}
finally {
application.dispose();
}
});

it('can inject preboot initialization code into rendered document', async () => {
const application = loadApplicationFixtureFromModule(BasicInlineModule,
b => b.preboot({appRoot: 'application'}));

try {
const snapshots = await application.prerender();

return await new Promise((resolve, reject) => {
snapshots.subscribe(
snapshot => {
const expr = /prebootstrap\(\).init\({(.*),"appRoot":"application"}\);/;
expect(snapshot.exceptions).not.toBeNull();
expect(snapshot.exceptions.length).toBe(0);
expect(snapshot.variant).toBeUndefined();
expect(snapshot.applicationState).toBeUndefined();
expect(expr.test(trimDocument(snapshot.renderedDocument))).toBeTruthy();
resolve();
},
exception => reject(exception));
});
}
finally {
application.dispose();
}
});

it('can apply postprocessor to DOM', async () => {
const application = loadApplicationFixtureFromModule(BasicInlineModule,
b => {
b.postprocess((document, rendered) => {
const element = document.createElement('style');
element.setAttribute('type', 'text/css');
element.textContent = `
body {
margin: 1em;
}`;
document.head.appendChild(element);
});
});

try {
const snapshots = await application.prerender();

return await new Promise((resolve, reject) => {
snapshots.subscribe(
snapshot => {
const expr1 = /<application ng-version="([^"]+)"><div>Hello!<\/div><\/application>/;
const expr2 = /margin: 1em/;
expect(snapshot.exceptions).not.toBeNull();
expect(snapshot.exceptions.length).toBe(0);
expect(snapshot.variant).toBeUndefined();
expect(snapshot.applicationState).toBeUndefined();
const trimmed = trimDocument(snapshot.renderedDocument);
expect(expr1.test(trimmed)).toBeTruthy();
expect(expr2.test(trimmed)).toBeTruthy();
resolve();
},
exception => reject(exception));
});
}
finally {
application.dispose();
}
});
});
2 changes: 1 addition & 1 deletion source/application/compiler/ngc/build.ts
Expand Up @@ -102,7 +102,7 @@ const bareSource = (source: string): string => {
return source;
}

if (/\.ngfactory\.(ts|js)$/.test(source) === false) {
if (/\.ngfactory\.(ts|js)$/i.test(source) === false) {
source = source.replace(/\.(js|ts)$/, String());
source = source.replace(/\.ngfactory$/, String());

Expand Down
2 changes: 1 addition & 1 deletion source/application/compiler/ngc/compiler.ts
Expand Up @@ -62,7 +62,7 @@ export class NgcCompiler implements ApplicationCompiler {

this.project.applicationModule = loadApplicationModule(
program,
this.project.basePath.toString(),
this.project.basePath,
this.project.applicationModule);

const roots = this.roots(program);
Expand Down
5 changes: 2 additions & 3 deletions source/application/compiler/options.ts
Expand Up @@ -10,9 +10,8 @@ import {
} from 'typescript';

import {CompilerException} from '../../exception';

import {ModuleDeclaration, Project} from '../project';

import {PathReference} from '../../filesystem';
import {discoverRootModule} from '../static';

export interface CompilationOptions {
Expand Down Expand Up @@ -51,7 +50,7 @@ const adjustOptions = (baseOptions?: CompilerOptions): CompilerOptions => {

const testHeuristic = (filename: string) => /(e2e|\.?(spec|tests?)\.)/.test(filename);

export const loadApplicationModule = (program: Program, basePath: string, module: ModuleDeclaration): ModuleDeclaration => {
export const loadApplicationModule = (program: Program, basePath: PathReference, module: ModuleDeclaration): ModuleDeclaration => {
const invalid = () => !module || !module.source || !module.symbol;

if (invalid()) {
Expand Down
2 changes: 1 addition & 1 deletion source/application/compiler/webpack/compiler.ts
Expand Up @@ -29,7 +29,7 @@ export class WebpackCompiler implements ApplicationCompiler {

this.project.applicationModule = loadApplicationModule(
program,
this.project.basePath.toString(),
this.project.basePath,
this.project.applicationModule);

const entries = {
Expand Down
2 changes: 1 addition & 1 deletion source/application/index.ts
@@ -1,7 +1,7 @@
export * from './builder';
export * from './compiler';
export * from './static';
export * from './contracts';
export * from './operation';
export * from './preboot';
export * from './project';
export * from './static';
4 changes: 2 additions & 2 deletions source/application/operation.ts
Expand Up @@ -6,7 +6,7 @@ import {
VariantsMap
} from './contracts';

import {Preboot} from './preboot';
import {PrebootQueryable} from './preboot';

import {Route} from '../route';

Expand Down Expand Up @@ -53,7 +53,7 @@ export interface RenderOperation {
postprocessors: Array<Postprocessor>;

// Optional preboot configuration, if preboot integration is desired
preboot: Preboot;
preboot: PrebootQueryable;
}

export interface RenderVariantOperation<V> {
Expand Down
Expand Up @@ -11,11 +11,6 @@ export type PrebootBaseOptions = {

export type PrebootConfiguration = (PrebootApplicationRoot | PrebootSeparateRoots) & PrebootBaseOptions;

// NOTE(bond): This is an internal interface that we use to query the configuration. For APIs that
// accept a configuration object from an API consumer, those should use {@link PrebootConfiguration}
// as it has the proper union rules about requiring either appRoot or serverClientRoot.
export interface Preboot extends PrebootApplicationRoot, PrebootSeparateRoots, PrebootBaseOptions {}

export interface EventSelector {
selector: string;
events: Array<string>;
Expand All @@ -25,3 +20,5 @@ export interface EventSelector {
action?: (node: Node, event: Event) => void;
noReplay?: boolean;
}

export type PrebootQueryable = PrebootApplicationRoot & PrebootSeparateRoots & PrebootBaseOptions;
2 changes: 2 additions & 0 deletions source/application/preboot/index.ts
@@ -0,0 +1,2 @@
export * from './contract';
export * from './schema';
File renamed without changes.
41 changes: 41 additions & 0 deletions source/application/preboot/tests/schema.ts
@@ -0,0 +1,41 @@
import {PrebootConfiguration} from '../contract';

import {validatePrebootOptionsAgainstSchema} from './../schema';

describe('Preboot schema validator', () => {
it('can validate a comprehensive preboot configuration', () => {
const config: PrebootConfiguration = {
appRoot: 'application',
eventSelectors: [
{selector: 'input,textarea', events: ['keypress', 'keyup', 'keydown', 'input', 'change']},
{selector: 'select,option', events: ['change']},
{selector: 'input', events: ['keyup'], preventDefault: true, keyCodes: [13], freeze: true},
{selector: 'input,textarea', events: ['focusin', 'focusout', 'mousedown', 'mouseup'], noReplay: true},
{selector: 'input[type="submit"],button', events: ['click'], preventDefault: true, freeze: true}
],
buffer: true,
uglify: true,
noInlineCache: true
};
const validated = validatePrebootOptionsAgainstSchema(config);
expect(validated).not.toBeNull();
expect(validated.valid).toBeTruthy();
expect(validated.errors.length).toBe(0);
});

it('fails when configuration is missing both appRoot and serverClientRoot', () => {
const config = {appRoot: null, serverClientRoot: null};
const validated = validatePrebootOptionsAgainstSchema(config);
expect(validated).not.toBeNull();
expect(validated.valid).toBeFalsy();
expect(validated.errors.length).toBeGreaterThan(0);
});

it('fails when both appRoot and serverClientRoot are specified', () => {
const config = {appRoot: 'application1', serverClientRoot: {server: 'application2', client: 'application1'}};
const validated = validatePrebootOptionsAgainstSchema(config);
expect(validated).not.toBeNull();
expect(validated.valid).toBeFalsy();
expect(validated.errors.length).toBeGreaterThan(0);
});
});
2 changes: 1 addition & 1 deletion source/application/static/modules/collect.ts
Expand Up @@ -36,7 +36,7 @@ export const collectModules = (basePath: PathReference, program: Program): Array

if (imported.symbol === 'NgModule') {
modules.push({
source: sourceFile.fileName,
source: sourceFile.fileName.replace(/\.(ts|js)$/i, String()),
symbol: node.name.text,
});
}
Expand Down
7 changes: 4 additions & 3 deletions source/application/static/modules/root.ts
Expand Up @@ -9,13 +9,14 @@ import {
import chalk = require('chalk');

import {ModuleDeclaration} from '../../project';
import {PathReference} from '../../../filesystem';
import {StaticAnalysisException} from '../../../exception';
import {importClause, exportClause} from '../find';
import {BootstrapFunctions} from '../../../static';
import {isExternalModule} from '../predicates';
import {traverse} from '../traverse';

export const discoverRootModule = (basePath: string, program: Program): ModuleDeclaration => {
export const discoverRootModule = (basePath: PathReference, program: Program): ModuleDeclaration => {
const identifiers = Object.keys(BootstrapFunctions).map(k => BootstrapFunctions[k]).join('|');

const expression = new RegExp(`(\.${identifiers})`);
Expand Down Expand Up @@ -51,12 +52,12 @@ export const discoverRootModule = (basePath: string, program: Program): ModuleDe
});

for (const identifier of Array.from(bootstrapIdentifiers)) {
const imported = importClause(basePath, sourceFile, identifier);
const imported = importClause(basePath.toString(), sourceFile, identifier);
if (imported) {
candidates.push(imported);
}
else {
const declaration = exportClause(basePath, sourceFile, identifier);
const declaration = exportClause(basePath.toString(), sourceFile, identifier);
if (declaration) {
const descriptions = [
'Pairing bootstrapModule or bootstrapModuleFactory with the root @NgModule in the same file will not work',
Expand Down
38 changes: 25 additions & 13 deletions source/application/static/modules/tests/modules.ts
Expand Up @@ -2,31 +2,30 @@ import {join} from 'path';

import {
CompilerOptions,
Program,
SourceFile,
ScriptTarget,
createCompilerHost,
createProgram,
createSourceFile,
} from 'typescript';

import {discoverRootModule} from '../root';
import {collectModules, discoverRootModule} from '..';

import {pathFromRandomId} from '../../../../filesystem';
import {PathReference, pathFromRandomId} from '../../../../filesystem';

import {randomId} from '../../../../static';

describe('discoverRootModule', () => {
it('can discover root application module from TypeScript source', () => {
const root = pathFromRandomId().toString();

const moduleFile = sourceToSourceFile(root, `
describe('Module discovery', () => {
const createExampleProgram = (root: PathReference): [string, Program] => {
const moduleFile = sourceToSourceFile(root.toString(), `
import {NgModule} from '@angular/core';
@NgModule()
export class RootModule {}
`);

const mainFile = sourceToSourceFile(root, `
const mainFile = sourceToSourceFile(root.toString(), `
import {RootModule} from './${moduleFile.fileName.replace(/\.ts$/, String())}';
platformBrowserDynamic().bootstrapModule(RootModule);
Expand All @@ -51,14 +50,27 @@ describe('discoverRootModule', () => {

host.fileExists = filename => filename === moduleFile.fileName || filename === mainFile.fileName;

const program = createProgram([moduleFile.fileName, mainFile.fileName], options, host);
return [moduleFile.fileName, createProgram([moduleFile.fileName, mainFile.fileName], options, host)];
};

it('can locate root application module in a project', () => {
const root = pathFromRandomId();
const [moduleFile, program] = createExampleProgram(root);
const descriptor = discoverRootModule(root, program);
expect(descriptor).not.toBeNull();
expect(descriptor.source).toEqual(moduleFile.fileName.replace(/(^(\\|\/)|\.ts$)/g, String()));
expect(descriptor.symbol).toBe('RootModule');
expect(descriptor.source).toEqual(moduleFile.replace(/(^(\\|\/)|\.ts$)/g, String()));
expect(descriptor.symbol).toEqual('RootModule');
});

it('can discover all @NgModule classes in a project', () => {
const root = pathFromRandomId();
const [moduleFile, program] = createExampleProgram(root);
const modules = collectModules(root, program);
expect(modules).not.toBeNull();
expect(modules.length).toBe(1);
expect(modules[0].source).toEqual(moduleFile.replace(/\.(ts|js)$/i, String()));
expect(modules[0].symbol).toEqual('RootModule');
});
});

const sourceToSourceFile = (root: string, code: string): SourceFile =>
createSourceFile(join(root, `${randomId()}.ts`), code, ScriptTarget.ES5, true);
const sourceToSourceFile = (root: string, code: string): SourceFile => createSourceFile(join(root, `${randomId()}.ts`), code, ScriptTarget.ES5, true);
1 change: 0 additions & 1 deletion source/bin/options/index.ts
@@ -1,3 +1,2 @@
export * from './options';
export * from './parse';
export * from './preboot';

0 comments on commit d5f372f

Please sign in to comment.