chore(typedoc-plugin-appium): refactored some stuff that didn't make sense

- better test organization
- plugin now exports a couple `Promise`-returning functions for programmatic consumption. TypeDoc doesn't use these, but they're helpful to test with
- simplified `omitDefaultReflections()`; moved into `converter/builder` module since it's a post-conversion thing
This commit is contained in:
Christopher Hiller
2023-01-11 14:47:43 -08:00
parent ad1dce5545
commit 75183ba38b
11 changed files with 374 additions and 227 deletions

View File

@@ -4,17 +4,17 @@
* @module
*/
import _ from 'lodash';
import pluralize from 'pluralize';
import {Context} from 'typedoc';
import {ContainerReflection, Context, DeclarationReflection, ProjectReflection} from 'typedoc';
import {isParentReflection} from '../guards';
import {AppiumPluginLogger} from '../logger';
import {
AppiumPluginReflectionKind,
CommandData,
ModuleCommands,
CommandReflection,
ExtensionReflection,
ExecMethodData,
ExtensionReflection,
ModuleCommands,
ParentReflection,
ProjectCommands,
Route,
@@ -116,8 +116,8 @@ export function createReflections(
const log = parentLog.createChildLogger('builder');
const {project} = ctx;
if (_.isEmpty(projectCmds)) {
log.warn('Nothing to do.');
if (!projectCmds.size) {
log.error('No reflections to create; nothing to do.');
return [];
}
@@ -138,3 +138,23 @@ export function createReflections(
return cmdsRefl;
});
}
/**
* Removes any reflection _not_ created by this plugin from the TypeDoc refl _except_ those
* created by this plugin.
* @param project - Current TypeDoc project
* @param refl - A {@linkcode ContainerReflection} to remove children from; defaults to `project`
* @returns A set of removed {@linkcode DeclarationReflection DeclarationReflections}
*/
export function omitDefaultReflections(
project: ProjectReflection,
refl: ContainerReflection = project
): Set<DeclarationReflection> {
let removed = new Set<DeclarationReflection>();
for (const childRefl of refl.getChildrenByKind(~(AppiumPluginReflectionKind.Extension as any))) {
project.removeReflection(childRefl);
removed.add(childRefl);
}
return removed;
}

View File

@@ -14,13 +14,9 @@
* @module
*/
import {Context, ProjectReflection, ReflectionKind} from 'typedoc';
import {Context} from 'typedoc';
import {AppiumPluginLogger} from '../logger';
import {
AppiumPluginReflectionKind,
AppiumPluginReflectionKindGroupTitles,
ProjectCommands,
} from '../model';
import {ProjectCommands} from '../model';
import {BuiltinExternalDriverConverter} from './builtin-external-driver';
import {BuiltinMethodMapConverter} from './builtin-method-map';
import {ExternalConverter} from './external';
@@ -53,39 +49,9 @@ export function convertCommands(ctx: Context, parentLog: AppiumPluginLogger): Pr
return new ProjectCommands(allCommands);
}
/**
* Removes any reflection _not_ created by this plugin from the TypeDoc project _except_ the main
* project and its module children (if any).
*
* This includes removal of groups from the main project.
* @param project - Current TypeDoc project
* @returns Project w/o the stuff in it. It is mutated in place.
*/
export function omitDefaultReflections(project: ProjectReflection): ProjectReflection {
// find all modules under the project, then anything not created by this plugin, and remove it
for (const module of project.getChildrenByKind(ReflectionKind.Module)) {
for (const child of module.getChildrenByKind(~(AppiumPluginReflectionKind.Extension as any))) {
project.removeReflection(child);
}
}
// find anything under the project itself not created by this plugin (except modules) and remove it
for (const child of project.getChildrenByKind(
~(AppiumPluginReflectionKind.Extension as any) & ~ReflectionKind.Module
)) {
project.removeReflection(child);
}
/// remove all groups except those created for the ReflectionKinds in this plugin
project.groups = project.groups?.filter((group) =>
AppiumPluginReflectionKindGroupTitles.has(group.title)
);
return project;
}
export * from '../model/builtin-commands';
export * from './base-converter';
export * from './builder';
export * from '../model/builtin-commands';
export * from './builtin-external-driver';
export * from './builtin-method-map';
export * from './comment';

View File

@@ -1,8 +1,12 @@
/**
* Utilities for the various converters.
* @module
*/
import {DeclarationReflection, LiteralType, ProjectReflection, ReflectionKind} from 'typedoc';
import {
isAsyncMethodDeclarationReflection,
isMethodDefParamNamesDeclarationReflection,
isParentReflection,
isReflectionWithReflectedType,
} from '../guards';
import {ParentReflection} from '../model';

View File

@@ -1,22 +1,40 @@
import {Application, Context, Converter} from 'typedoc';
import {Application, Context, Converter, DeclarationReflection} from 'typedoc';
import {convertCommands, createReflections, omitDefaultReflections} from './converter';
import {AppiumPluginLogger, AppiumPluginParentLogger} from './logger';
import {NS} from './model';
import {ExtensionReflection, NS, ProjectCommands} from './model';
import {configureOptions, declarations} from './options';
import {AppiumTheme, THEME_NAME} from './theme';
/**
* Loads the Appium TypeDoc plugin
* Loads the Appium TypeDoc plugin.
*
* @param app - TypeDoc Application
* @returns Unused by TypeDoc, but can be consumed programmatically.
*/
export function load(app: Application) {
export function load(
app: Application
): Promise<[PromiseSettledResult<ConvertResult>, PromiseSettledResult<PostProcessResult>]> {
// register our custom theme. the user still has to choose it
app.renderer.defineTheme(THEME_NAME, AppiumTheme);
configureOptions(app);
app.converter
.on(Converter.EVENT_RESOLVE_BEGIN, (ctx: Context) => {
return Promise.allSettled([convert(app), postProcess(app)]);
}
/**
* Finds commands and creates new reflections for them, adding them to the project.
*
* Resolves after {@linkcode Converter.EVENT_RESOLVE_BEGIN} emits and when it's finished.
* @param app Typedoc Application
* @returns A {@linkcode ConvertResult} receipt from the conversion
*/
export async function convert(app: Application): Promise<ConvertResult> {
return new Promise((resolve) => {
app.converter.once(Converter.EVENT_RESOLVE_BEGIN, (ctx: Context) => {
let extensionReflections: ExtensionReflection[] | undefined;
let projectCommands: ProjectCommands | undefined;
// we don't want to do this work if we're not using the custom theme!
const log = new AppiumPluginLogger(app.logger, NS);
@@ -24,21 +42,70 @@ export function load(app: Application) {
// it's a safeguard nonetheless.
if (app.renderer.themeName === THEME_NAME) {
// this queries the declarations created by TypeDoc and extracts command information
const moduleCommands = convertCommands(ctx, log);
projectCommands = convertCommands(ctx, log);
// this creates new custom reflections from the data we gathered and registers them
// with TypeDoc
createReflections(ctx, log, moduleCommands);
extensionReflections = createReflections(ctx, log, projectCommands);
} else {
log.warn('Not using the Appium theme; skipping command reflection creation');
}
})
.on(Converter.EVENT_RESOLVE_END, (ctx: Context) => {
resolve({ctx, extensionReflections, projectCommands});
});
});
}
/**
* Resolved value of {@linkcode convert}
*/
export interface ConvertResult {
/**
* Context at time of {@linkcode Context.EVENT_RESOLVE_BEGIN}
*/
ctx: Context;
/**
* Raw data structure containing everything about commands in the project
*/
projectCommands?: ProjectCommands;
/**
* List of custom reflections created by the plugin
*/
extensionReflections?: ExtensionReflection[];
}
/**
* Optionally omits the default TypeDoc reflections from the project based on the `outputModules` option.
*
* Resolves after {@linkcode Converter.EVENT_RESOLVE_END} emits and when it's finished.
* @param app Typedoc application
* @returns Typedoc `Context` at the time of the {@linkcode Converter.EVENT_RESOLVE_END} event
*/
export async function postProcess(app: Application): Promise<PostProcessResult> {
return new Promise((resolve) => {
app.converter.once(Converter.EVENT_RESOLVE_END, (ctx: Context) => {
let removed: Set<DeclarationReflection> | undefined;
// if the `outputModules` option is false, then we want to remove all the usual TypeDoc reflections.
if (!app.options.getValue(declarations.outputModules.name)) {
omitDefaultReflections(ctx.project);
removed = omitDefaultReflections(ctx.project);
}
resolve({ctx, removed});
});
});
}
/**
* Result of {@linkcode postProcess}
*/
export interface PostProcessResult {
/**
* A list of {@linkcode DeclarationReflection DeclarationReflections} which were removed from the
* project, if any.
*/
removed?: Set<DeclarationReflection>;
/**
* Context at time of {@linkcode Context.EVENT_RESOLVE_END}
*/
ctx: Context;
}
export * from './options';

View File

@@ -1,35 +0,0 @@
import {Application, Context, Converter} from 'typedoc';
import {convertCommands, createReflections, stats} from './converter';
import {AppiumPluginLogger, AppiumPluginParentLogger} from './logger';
import {AppiumTheme, THEME_NAME} from './theme';
/**
* Loads the Appium TypeDoc plugin
* @param app - TypeDoc Application
*/
export function load(app: Application) {
// register our custom theme. the user still has to choose it
app.renderer.defineTheme(THEME_NAME, AppiumTheme);
app.converter.on(Converter.EVENT_RESOLVE_BEGIN, (ctx: Context) => {
// we don't want to do this work if we're not using the custom theme!
const log = new AppiumPluginLogger(app.logger, 'appium');
// note: THEME_NAME is flimsy, but we cannot use instanceof due to the AppiumTheme closure
if (app.renderer.themeName === THEME_NAME) {
// this queries the declarations created by TypeDoc and extracts command information
const moduleCommands = convertCommands(ctx, log);
// this creates new custom reflections from the data we gathered and registers them
// with TypeDoc
createReflections(ctx, log, moduleCommands);
log.info(`${stats}`);
} else {
log.warn('Not using the Appium theme; skipping command reflection creation');
}
});
}
export * from './theme';
export type {AppiumPluginLogger, AppiumPluginParentLogger};

View File

@@ -1,103 +0,0 @@
import path from 'node:path';
import {createSandbox, SinonSandbox} from 'sinon';
import {Context, Converter} from 'typedoc';
import {
BuiltinExternalDriverConverter,
BuiltinMethodMapConverter,
createReflections,
ExternalConverter,
NAME_BUILTIN_COMMAND_MODULE,
NAME_TYPES_MODULE,
} from '../../../lib/converter';
import {AppiumPluginLogger} from '../../../lib/logger';
import {
AppiumPluginReflectionKind,
CommandReflection,
ExtensionReflection,
ProjectCommands,
} from '../../../lib/model';
import {initAppForPkgs, NAME_FAKE_DRIVER_MODULE, ROOT_TSCONFIG} from '../helpers';
const {expect} = chai;
describe('command data builder', function () {
let sandbox: SinonSandbox;
beforeEach(function () {
sandbox = createSandbox();
});
afterEach(function () {
sandbox.restore();
});
describe('createReflections()', function () {
let moduleCommands: ProjectCommands;
let ctx: Context;
let log: AppiumPluginLogger;
let cmdsRefls!: ExtensionReflection[];
before(async function () {
const app = await initAppForPkgs(
ROOT_TSCONFIG,
NAME_TYPES_MODULE,
NAME_FAKE_DRIVER_MODULE,
NAME_BUILTIN_COMMAND_MODULE
);
log = new AppiumPluginLogger(app.logger, 'appium-test');
({moduleCommands, ctx} = await new Promise((resolve) => {
// this is really way too similar to what happens in the plugin itself
app.converter.once(Converter.EVENT_RESOLVE_BEGIN, (ctx: Context) => {
const builtinConverter = new BuiltinExternalDriverConverter(
ctx,
log.createChildLogger('builtin-types')
);
const knownMethods = builtinConverter.convert();
const builtinMethodMapConverter = new BuiltinMethodMapConverter(
ctx,
log.createChildLogger('builtin-methods'),
knownMethods
);
const builtinSource = builtinMethodMapConverter.convert();
const fakeDriverConverter = new ExternalConverter(
ctx,
log.createChildLogger('fake-driver'),
knownMethods,
builtinSource?.moduleCmds
);
resolve({moduleCommands: fakeDriverConverter.convert(), ctx});
});
app.converter.convert(app.getEntryPoints()!);
}));
expect(() => (cmdsRefls = createReflections(ctx, log, moduleCommands))).not.to.throw();
});
it('should sort commands in ascending order by route', function () {
for (const cmdsRefl of cmdsRefls) {
let lastRoute = '';
for (const cmdRefl of cmdsRefl.getChildrenByKind(
AppiumPluginReflectionKind.Command as any
) as CommandReflection[]) {
if (lastRoute) {
expect(cmdRefl.route.localeCompare(lastRoute)).to.greaterThanOrEqual(0);
}
lastRoute = cmdRefl.route;
}
}
});
it('should sort exec methods in ascending order by script', function () {
for (const cmdsRefl of cmdsRefls) {
let lastScript = '';
for (const cmdRefl of cmdsRefl.getChildrenByKind(
AppiumPluginReflectionKind.ExecuteMethod as any
) as CommandReflection[]) {
if (lastScript) {
expect(cmdRefl.script!.localeCompare(lastScript)).to.greaterThanOrEqual(0);
}
lastScript = cmdRefl.script!;
}
}
});
});
});

View File

@@ -0,0 +1,26 @@
import {expect} from 'chai';
import {
KnownMethods,
BuiltinExternalDriverConverter,
NAME_TYPES_MODULE,
deriveComment,
} from '../../../lib/converter';
import {initConverter} from '../helpers';
describe('deriveComment()', function () {
let knownMethods: KnownMethods;
before(async function () {
const converter = await initConverter(BuiltinExternalDriverConverter, NAME_TYPES_MODULE);
knownMethods = converter.convert();
expect(knownMethods.size).to.be.greaterThan(0);
expect(knownMethods.get('activateApp')!.comment).to.exist;
});
describe('when not provided a reflection nor `Comment` parameter', function () {
it('should derive the comment from KnownMethods', function () {
const commentData = deriveComment('activateApp', knownMethods);
expect(commentData).to.exist.and.to.have.keys('comment', 'commentSource');
});
});
});

View File

@@ -0,0 +1,45 @@
import {createSandbox, SinonSandbox} from 'sinon';
import {Context, Converter} from 'typedoc';
import {
convertCommands,
NAME_BUILTIN_COMMAND_MODULE,
NAME_TYPES_MODULE,
} from '../../../lib/converter';
import {AppiumPluginLogger} from '../../../lib/logger';
import {ProjectCommands} from '../../../lib/model';
import {initAppForPkgs, NAME_FAKE_DRIVER_MODULE, ROOT_TSCONFIG} from '../helpers';
const {expect} = chai;
describe('converter', function () {
let sandbox: SinonSandbox;
beforeEach(function () {
sandbox = createSandbox();
});
afterEach(function () {
sandbox.restore();
});
let ctx: Context;
let log: AppiumPluginLogger;
before(async function () {
const app = await initAppForPkgs(
ROOT_TSCONFIG,
NAME_TYPES_MODULE,
NAME_FAKE_DRIVER_MODULE,
NAME_BUILTIN_COMMAND_MODULE
);
ctx = await new Promise((resolve) => {
app.converter.once(Converter.EVENT_RESOLVE_BEGIN, (ctx: Context) => {
resolve(ctx);
});
app.converter.convert(app.getEntryPoints()!);
});
log = new AppiumPluginLogger(app.logger, 'appium-test');
});
describe('convertCommands()', function () {
it('should return a non-empty ProjectCommands Map', () => {
expect(convertCommands(ctx, log)).to.be.an.instanceof(ProjectCommands).and.not.to.be.empty;
});
});
});

View File

@@ -0,0 +1,178 @@
import {createSandbox, SinonSandbox} from 'sinon';
import {
Application,
Context,
DeclarationReflection,
ProjectReflection,
ReflectionKind,
} from 'typedoc';
import {
AppiumTheme,
configureOptions,
convert,
ConvertResult,
postProcess,
PostProcessResult,
THEME_NAME,
} from '../../../lib';
import {NAME_BUILTIN_COMMAND_MODULE, NAME_TYPES_MODULE} from '../../../lib/converter';
import {
AppiumPluginReflectionKind,
CommandReflection,
ExtensionReflection,
ProjectCommands,
} from '../../../lib/model';
import {initAppForPkgs, NAME_FAKE_DRIVER_MODULE, ROOT_TSCONFIG} from '../helpers';
const {expect} = chai;
describe('plugin', function () {
let sandbox: SinonSandbox;
/**
* Typedoc app
*/
let app: Application;
/**
* All project commands as returned by the converters
*/
let projectCommands: ProjectCommands;
/**
* Result of {@linkcode convert}
*/
let resolveBeginCtxPromise: Promise<ConvertResult>;
/**
* Result of {@linkcode postProcess}
*/
let resolveEndCtxPromise: Promise<PostProcessResult>;
/**
* Whatever {@linkcode Context} we're using to test
*/
let ctx: Context;
/**
* Array of {@linkcode ExtensionReflection} instances as in a {@linkcode ConvertResult}
*/
let extensionReflections!: ExtensionReflection[];
/**
* Creates a new TypeDoc application and/or resets it
*/
async function reset() {
app = await initAppForPkgs(
ROOT_TSCONFIG,
NAME_TYPES_MODULE,
NAME_FAKE_DRIVER_MODULE,
NAME_BUILTIN_COMMAND_MODULE
);
// the load() function of the plugin does this stuff
app.renderer.defineTheme(THEME_NAME, AppiumTheme);
configureOptions(app);
}
before(async function () {
await reset();
});
beforeEach(function () {
sandbox = createSandbox();
});
afterEach(function () {
sandbox.restore();
});
describe('convert()', function () {
before(async function () {
resolveBeginCtxPromise = convert(app);
app.convert();
const result = await resolveBeginCtxPromise;
projectCommands = result.projectCommands!;
extensionReflections = result.extensionReflections!;
});
it('should sort commands in ascending order by route', function () {
for (const cmdsRefl of extensionReflections) {
let lastRoute = '';
for (const cmdRefl of cmdsRefl.getChildrenByKind(
AppiumPluginReflectionKind.Command as any
) as CommandReflection[]) {
if (lastRoute) {
expect(cmdRefl.route.localeCompare(lastRoute)).to.greaterThanOrEqual(0);
}
lastRoute = cmdRefl.route;
}
}
});
it('should sort exec methods in ascending order by script', function () {
for (const cmdsRefl of extensionReflections) {
let lastScript = '';
for (const cmdRefl of cmdsRefl.getChildrenByKind(
AppiumPluginReflectionKind.ExecuteMethod as any
) as CommandReflection[]) {
if (lastScript) {
expect(cmdRefl.script!.localeCompare(lastScript)).to.greaterThanOrEqual(0);
}
lastScript = cmdRefl.script!;
}
}
});
describe('when called with an empty ProjectCommands', function () {
it('should log an error and return an empty array', function () {});
});
});
describe('postProcess()', function () {
let project: ProjectReflection;
let removed: Set<DeclarationReflection> | undefined;
describe('when the `outputModules` option is false', function () {
before(async function () {
await reset();
app.options.setValue('outputModules', false);
resolveBeginCtxPromise = convert(app);
resolveEndCtxPromise = postProcess(app);
app.convert();
[, {ctx, removed}] = await Promise.all([resolveBeginCtxPromise, resolveEndCtxPromise]);
({project} = ctx);
});
it('should mutate the project', function () {
const childRefls = project.getChildrenByKind(AppiumPluginReflectionKind.Extension as any);
expect(childRefls).to.have.lengthOf(project.children!.length).and.to.not.be.empty;
expect(project.getChildrenByKind(ReflectionKind.Module)).to.be.empty;
});
it('should remove DeclarationReflections', function () {
expect(removed).not.to.be.empty;
});
});
describe('when the `outputModules` option is true', function () {
before(async function () {
await reset();
app.options.setValue('outputModules', true);
resolveBeginCtxPromise = convert(app);
resolveEndCtxPromise = postProcess(app);
app.convert();
[, {ctx, removed}] = await Promise.all([resolveBeginCtxPromise, resolveEndCtxPromise]);
({project} = ctx);
});
it('should not remove anything from the project', function () {
const defaultRefls = project.getChildrenByKind(
~(AppiumPluginReflectionKind.Extension as any)
);
expect(defaultRefls).to.not.be.empty;
const pluginRefls = project.getChildrenByKind(AppiumPluginReflectionKind.Extension as any);
expect(pluginRefls).to.not.be.empty;
});
it('should not remove DeclarationReflections', function () {
expect(removed).not.to.exist;
});
});
});
});

View File

@@ -1,7 +1,5 @@
import {createSandbox, SinonSandbox} from 'sinon';
import {
Application,
Comment,
DeclarationReflection,
LiteralType,
ReflectionKind,
@@ -9,24 +7,19 @@ import {
TypeOperatorType,
} from 'typedoc';
import {
BuiltinExternalDriverConverter,
convertCommandParams,
deriveComment,
KnownMethods,
MethodDefParamNamesDeclarationReflection,
MethodDefParamsPropDeclarationReflection,
NAME_OPTIONAL,
NAME_PARAMS,
NAME_REQUIRED,
NAME_TYPES_MODULE,
TupleTypeWithLiteralElements,
TypeOperatorTypeWithTupleTypeWithLiteralElements,
} from '../../../lib/converter';
import {initConverter} from '../helpers';
const {expect} = chai;
describe('converter - method map', function () {
describe('utils', function () {
let sandbox: SinonSandbox;
beforeEach(function () {
@@ -37,10 +30,6 @@ describe('converter - method map', function () {
sandbox.restore();
});
describe('convertMethodMap()', function () {});
describe('convertExecuteMethodMap()', function () {});
describe('convertCommandParams()', function () {
describe('when not provided a MethodDefParamsPropDeclarationReflection', function () {
it('should return an empty array', function () {
@@ -133,22 +122,4 @@ describe('converter - method map', function () {
});
});
});
describe('deriveComment()', function () {
let knownMethods: KnownMethods;
before(async function () {
const converter = await initConverter(BuiltinExternalDriverConverter, NAME_TYPES_MODULE);
knownMethods = converter.convert();
expect(knownMethods.size).to.be.greaterThan(0);
expect(knownMethods.get('activateApp')!.comment).to.exist;
});
describe('when not provided a reflection nor `Comment` parameter', function () {
it('should derive the comment from KnownMethods', function () {
const commentData = deriveComment('activateApp', knownMethods);
expect(commentData).to.exist.and.to.have.keys('comment', 'commentSource');
});
});
});
});

View File

@@ -3,6 +3,7 @@ import readPkg from 'read-pkg';
import {Constructor} from 'type-fest';
import {Application, Context, Converter, LogLevel, TSConfigReader} from 'typedoc';
import ts from 'typescript';
import {THEME_NAME} from '../../lib';
import {BaseConverter} from '../../lib/converter';
import {AppiumPluginLogger} from '../../lib/logger';
@@ -23,22 +24,29 @@ export async function getEntryPoint(pkgName: string): Promise<string> {
export function getTypedocApp(
tsconfig: string,
entryPoints: string[] = [],
logger?: TestLogger
opts: GetTypedocAppOpts = {}
): Application {
const app = new Application();
app.options.addReader(new TSConfigReader());
const {logger, plugin = ['none']} = opts;
app.bootstrap({
excludeExternals: true,
tsconfig,
plugin: ['none'],
plugin,
logLevel: process.env._FORCE_LOGS ? LogLevel.Verbose : LogLevel.Info,
logger,
entryPoints,
entryPointStrategy: entryPoints.length > 1 ? 'packages' : 'resolve',
theme: THEME_NAME,
});
return app;
}
export interface GetTypedocAppOpts {
logger?: TestLogger;
plugin?: string[];
}
export function getConverterProgram(app: Application): ts.Program {
const program = ts.createProgram(app.options.getFileNames(), app.options.getCompilerOptions());
const errors = ts.getPreEmitDiagnostics(program);
@@ -49,13 +57,13 @@ export function getConverterProgram(app: Application): ts.Program {
/**
*
* @param pkgName Name of package to get program for
* @param pkgName Name of package to get Application for
* @returns Object with stuff you need to do things
*/
export async function initAppForPkg(pkgName: string, logger?: TestLogger): Promise<Application> {
const entryPoint = await getEntryPoint(pkgName);
const tsconfig = require.resolve(`${pkgName}/tsconfig.json`);
return getTypedocApp(tsconfig, [entryPoint], logger);
return getTypedocApp(tsconfig, [entryPoint], {logger});
}
export async function initAppForPkgs(