feat(ivy): improve debugging experience for styles/classes (#32753)

This patch introduces the `printTable()` and `printSources()`
methods to `DebugStylingContext` which can be used via the
`window.ng.getDebugNode` helpers when debugging an application.

PR Close #32753
This commit is contained in:
Matias Niemelä 2019-09-18 11:18:37 -07:00 committed by Andrew Kushnir
parent f8f7c1540a
commit 32f4544f34
5 changed files with 160 additions and 23 deletions

View File

@ -20,7 +20,7 @@ import {TQueries} from '../interfaces/query';
import {RComment, RElement, RNode} from '../interfaces/renderer'; import {RComment, RElement, RNode} from '../interfaces/renderer';
import {TStylingContext} from '../interfaces/styling'; import {TStylingContext} from '../interfaces/styling';
import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, HookData, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TData, TVIEW, TView as ITView, TView, T_HOST} from '../interfaces/view'; import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, HookData, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TData, TVIEW, TView as ITView, TView, T_HOST} from '../interfaces/view';
import {DebugStyling as DebugNewStyling, NodeStylingDebug} from '../styling/styling_debug'; import {DebugNodeStyling, NodeStylingDebug} from '../styling/styling_debug';
import {attachDebugObject} from '../util/debug_utils'; import {attachDebugObject} from '../util/debug_utils';
import {isStylingContext} from '../util/styling_utils'; import {isStylingContext} from '../util/styling_utils';
import {getTNode, unwrapRNode} from '../util/view_utils'; import {getTNode, unwrapRNode} from '../util/view_utils';
@ -341,8 +341,8 @@ export class LViewDebug {
export interface DebugNode { export interface DebugNode {
html: string|null; html: string|null;
native: Node; native: Node;
styles: DebugNewStyling|null; styles: DebugNodeStyling|null;
classes: DebugNewStyling|null; classes: DebugNodeStyling|null;
nodes: DebugNode[]|null; nodes: DebugNode[]|null;
component: LViewDebug|null; component: LViewDebug|null;
} }
@ -372,7 +372,7 @@ export function buildDebugNode(tNode: TNode, lView: LView, nodeIndex: number): D
const native = unwrapRNode(rawValue); const native = unwrapRNode(rawValue);
const componentLViewDebug = toDebug(readLViewValue(rawValue)); const componentLViewDebug = toDebug(readLViewValue(rawValue));
const styles = isStylingContext(tNode.styles) ? const styles = isStylingContext(tNode.styles) ?
new NodeStylingDebug(tNode.styles as any as TStylingContext, lView) : new NodeStylingDebug(tNode.styles as any as TStylingContext, lView, false) :
null; null;
const classes = isStylingContext(tNode.classes) ? const classes = isStylingContext(tNode.classes) ?
new NodeStylingDebug(tNode.classes as any as TStylingContext, lView, true) : new NodeStylingDebug(tNode.classes as any as TStylingContext, lView, true) :

View File

@ -498,7 +498,7 @@ function getContext(tNode: TNode, isClassBased: boolean): TStylingContext {
if (!isStylingContext(context)) { if (!isStylingContext(context)) {
context = allocTStylingContext(context as StylingMapArray | null); context = allocTStylingContext(context as StylingMapArray | null);
if (ngDevMode) { if (ngDevMode) {
attachStylingDebugObject(context as TStylingContext); attachStylingDebugObject(context as TStylingContext, isClassBased);
} }
if (isClassBased) { if (isClassBased) {
tNode.classes = context; tNode.classes = context;

View File

@ -10,7 +10,7 @@ import {RElement} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, TStylingConfig, TStylingContext, TStylingContextIndex} from '../interfaces/styling'; import {ApplyStylingFn, LStylingData, TStylingConfig, TStylingContext, TStylingContextIndex} from '../interfaces/styling';
import {getCurrentStyleSanitizer} from '../state'; import {getCurrentStyleSanitizer} from '../state';
import {attachDebugObject} from '../util/debug_utils'; import {attachDebugObject} from '../util/debug_utils';
import {allowDirectStyling as _allowDirectStyling, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasConfig, isContextLocked, isSanitizationRequired, isStylingContext} from '../util/styling_utils'; import {MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, allowDirectStyling as _allowDirectStyling, getBindingValue, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasConfig, isContextLocked, isSanitizationRequired, isStylingContext} from '../util/styling_utils';
import {applyStylingViaContext} from './bindings'; import {applyStylingViaContext} from './bindings';
import {activateStylingMapFeature} from './map_based_bindings'; import {activateStylingMapFeature} from './map_based_bindings';
@ -41,6 +41,12 @@ export interface DebugStylingContext {
/** The associated TStylingContext instance */ /** The associated TStylingContext instance */
entries: {[prop: string]: DebugStylingContextEntry}; entries: {[prop: string]: DebugStylingContextEntry};
/** A status report of all the sources within the context */
printSources(): void;
/** A status report of all the entire context as a table */
printTable(): void;
} }
@ -63,10 +69,10 @@ export interface DebugStylingConfig {
* A debug/testing-oriented summary of all styling entries within a `TStylingContext`. * A debug/testing-oriented summary of all styling entries within a `TStylingContext`.
*/ */
export interface DebugStylingContextEntry { export interface DebugStylingContextEntry {
/** The property (style or class property) that this tuple represents */ /** The property (style or class property) that this entry represents */
prop: string; prop: string;
/** The total amount of styling entries a part of this tuple */ /** The total amount of styling entries a part of this entry */
valuesCount: number; valuesCount: number;
/** /**
@ -145,8 +151,8 @@ export interface DebugNodeStylingEntry {
/** /**
* Instantiates and attaches an instance of `TStylingContextDebug` to the provided context * Instantiates and attaches an instance of `TStylingContextDebug` to the provided context
*/ */
export function attachStylingDebugObject(context: TStylingContext) { export function attachStylingDebugObject(context: TStylingContext, isClassBased: boolean) {
const debug = new TStylingContextDebug(context); const debug = new TStylingContextDebug(context, isClassBased);
attachDebugObject(context, debug); attachDebugObject(context, debug);
return debug; return debug;
} }
@ -158,14 +164,14 @@ export function attachStylingDebugObject(context: TStylingContext) {
* application has `ngDevMode` activated. * application has `ngDevMode` activated.
*/ */
class TStylingContextDebug implements DebugStylingContext { class TStylingContextDebug implements DebugStylingContext {
constructor(public readonly context: TStylingContext) {} constructor(public readonly context: TStylingContext, private _isClassBased: boolean) {}
get config(): DebugStylingConfig { return buildConfig(this.context); } get config(): DebugStylingConfig { return buildConfig(this.context); }
/** /**
* Returns a detailed summary of each styling entry in the context. * Returns a detailed summary of each styling entry in the context.
* *
* See `TStylingTupleSummary`. * See `DebugStylingContextEntry`.
*/ */
get entries(): {[prop: string]: DebugStylingContextEntry} { get entries(): {[prop: string]: DebugStylingContextEntry} {
const context = this.context; const context = this.context;
@ -202,6 +208,137 @@ class TStylingContextDebug implements DebugStylingContext {
} }
return entries; return entries;
} }
/**
* Prints a detailed summary of each styling source grouped together with each binding index in
* the context.
*/
printSources(): void {
let output = '\n';
const context = this.context;
const prefix = this._isClassBased ? 'class' : 'style';
const bindingsBySource: {
type: string,
entries: {binding: string, bindingIndex: number, value: any, bitMask: number}[]
}[] = [];
const totalColumns = getValuesCount(context);
const itemsPerRow = TStylingContextIndex.BindingsStartOffset + totalColumns;
for (let i = 0; i < totalColumns; i++) {
const isDefaultColumn = i === totalColumns - 1;
const hostBindingsMode = i !== TEMPLATE_DIRECTIVE_INDEX;
const type = getTypeFromColumn(i, totalColumns);
const entries: {binding: string, value: any, bindingIndex: number, bitMask: number}[] = [];
let j = TStylingContextIndex.ValuesStartPosition;
while (j < context.length) {
const value = getBindingValue(context, j, i);
if (isDefaultColumn || value > 0) {
const bitMask = getGuardMask(context, j, hostBindingsMode);
const bindingIndex = isDefaultColumn ? -1 : value as number;
const prop = getProp(context, j);
const isMapBased = prop === MAP_BASED_ENTRY_PROP_NAME;
const binding = `${prefix}${isMapBased ? '' : '.' + prop}`;
entries.push({binding, value, bindingIndex, bitMask});
}
j += itemsPerRow;
}
bindingsBySource.push(
{type, entries: entries.sort((a, b) => a.bindingIndex - b.bindingIndex)});
}
bindingsBySource.forEach(entry => {
output += `[${entry.type.toUpperCase()}]\n`;
output += repeat('-', entry.type.length + 2) + '\n';
let tab = ' ';
entry.entries.forEach(entry => {
const isDefault = typeof entry.value !== 'number';
const value = entry.value;
if (!isDefault || value !== null) {
output += `${tab}[${entry.binding}] = \`${value}\``;
output += '\n';
}
});
output += '\n';
});
/* tslint:disable */
console.log(output);
}
/**
* Prints a detailed table of the entire styling context.
*/
printTable(): void {
// IE (not Edge) is the only browser that doesn't support this feature. Because
// these debugging tools are not apart of the core of Angular (they are just
// extra tools) we can skip-out on older browsers.
if (!console.table) {
throw new Error('This feature is not supported in your browser');
}
const context = this.context;
const table: any[] = [];
const totalColumns = getValuesCount(context);
const itemsPerRow = TStylingContextIndex.BindingsStartOffset + totalColumns;
const totalProps = Math.floor(context.length / itemsPerRow);
let i = TStylingContextIndex.ValuesStartPosition;
while (i < context.length) {
const prop = getProp(context, i);
const isMapBased = prop === MAP_BASED_ENTRY_PROP_NAME;
const entry: {[key: string]: any} = {
prop,
'tpl mask': generateBitString(getGuardMask(context, i, false), isMapBased, totalProps),
'host mask': generateBitString(getGuardMask(context, i, true), isMapBased, totalProps),
};
for (let j = 0; j < totalColumns; j++) {
const key = getTypeFromColumn(j, totalColumns);
const value = getBindingValue(context, i, j);
entry[key] = value;
}
i += itemsPerRow;
table.push(entry);
}
/* tslint:disable */
console.table(table);
}
}
function generateBitString(value: number, isMapBased: boolean, totalProps: number) {
if (isMapBased || value > 1) {
return `0b${leftPad(value.toString(2), totalProps, '0')}`;
}
return null;
}
function leftPad(value: string, max: number, pad: string) {
return repeat(pad, max - value.length) + value;
}
function getTypeFromColumn(index: number, totalColumns: number) {
if (index === TEMPLATE_DIRECTIVE_INDEX) {
return 'template';
} else if (index === totalColumns - 1) {
return 'defaults';
} else {
return `dir #${index}`;
}
}
function repeat(c: string, times: number) {
let s = '';
for (let i = 0; i < times; i++) {
s += c;
}
return s;
} }
/** /**
@ -216,9 +353,9 @@ export class NodeStylingDebug implements DebugNodeStyling {
constructor( constructor(
context: TStylingContext|DebugStylingContext, private _data: LStylingData, context: TStylingContext|DebugStylingContext, private _data: LStylingData,
private _isClassBased?: boolean) { private _isClassBased: boolean) {
this._debugContext = isStylingContext(context) ? this._debugContext = isStylingContext(context) ?
new TStylingContextDebug(context as TStylingContext) : new TStylingContextDebug(context as TStylingContext, _isClassBased) :
(context as DebugStylingContext); (context as DebugStylingContext);
} }

View File

@ -12,7 +12,7 @@ import {DEFAULT_GUARD_MASK_VALUE, allocTStylingContext} from '../../../src/rende
describe('styling context', () => { describe('styling context', () => {
it('should register a series of entries into the context', () => { it('should register a series of entries into the context', () => {
const debug = makeContextWithDebug(); const debug = makeContextWithDebug(false);
const context = debug.context; const context = debug.context;
expect(debug.entries).toEqual({}); expect(debug.entries).toEqual({});
@ -52,7 +52,7 @@ describe('styling context', () => {
}); });
it('should only register the same binding index once per property', () => { it('should only register the same binding index once per property', () => {
const debug = makeContextWithDebug(); const debug = makeContextWithDebug(false);
const context = debug.context; const context = debug.context;
expect(debug.entries).toEqual({}); expect(debug.entries).toEqual({});
@ -70,7 +70,7 @@ describe('styling context', () => {
}); });
it('should overwrite a default value for an entry only if it is non-null', () => { it('should overwrite a default value for an entry only if it is non-null', () => {
const debug = makeContextWithDebug(); const debug = makeContextWithDebug(false);
const context = debug.context; const context = debug.context;
registerBinding(context, 1, 0, 'width', null); registerBinding(context, 1, 0, 'width', null);
@ -109,9 +109,9 @@ describe('styling context', () => {
}); });
}); });
function makeContextWithDebug() { function makeContextWithDebug(isClassBased: boolean) {
const ctx = allocTStylingContext(); const ctx = allocTStylingContext();
return attachStylingDebugObject(ctx); return attachStylingDebugObject(ctx, isClassBased);
} }
function buildGuardMask(...bindingIndices: number[]) { function buildGuardMask(...bindingIndices: number[]) {

View File

@ -13,10 +13,10 @@ describe('styling debugging tools', () => {
describe('NodeStylingDebug', () => { describe('NodeStylingDebug', () => {
it('should list out each of the values in the context paired together with the provided data', it('should list out each of the values in the context paired together with the provided data',
() => { () => {
const debug = makeContextWithDebug(); const debug = makeContextWithDebug(false);
const context = debug.context; const context = debug.context;
const data: any[] = []; const data: any[] = [];
const d = new NodeStylingDebug(context, data); const d = new NodeStylingDebug(context, data, false);
registerBinding(context, 0, 0, 'width', null); registerBinding(context, 0, 0, 'width', null);
expect(d.summary).toEqual({ expect(d.summary).toEqual({
@ -63,7 +63,7 @@ describe('styling debugging tools', () => {
}); });
}); });
function makeContextWithDebug() { function makeContextWithDebug(isClassBased: boolean) {
const ctx = allocTStylingContext(); const ctx = allocTStylingContext();
return attachStylingDebugObject(ctx); return attachStylingDebugObject(ctx, isClassBased);
} }