From 769fa7ee63c746b667009bd4b821f5fb26d55c56 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Wed, 21 Sep 2022 11:25:30 -0700 Subject: [PATCH 1/3] stress test: add random tree generator RandomTree is a class for generating trees with random depth and breadth. The class is agnostic of UI technology and so can be used to generate trees for rendering in React, Web Components, vanilla DOM or any other tool. RandomTree is a generic class and allows trees with different properties to be created. This commit creates a "selector tree" that generates a tree with many attributes like CSS classes, data-* attributes and CSS selectors. Internally RandomTree uses a seedable random number generator for randomness so the results should always be deterministic. --- apps/stress-test/package.json | 3 +- .../src/shared/css/RandomSelector.ts | 137 +++++++++++++++++ .../src/shared/tree/RandomSelectorTreeNode.ts | 139 ++++++++++++++++++ .../stress-test/src/shared/tree/RandomTree.ts | 107 ++++++++++++++ apps/stress-test/src/shared/utils/random.ts | 24 +++ .../stress-test/src/shared/utils/testUtils.ts | 11 ++ yarn.lock | 5 + 7 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 apps/stress-test/src/shared/css/RandomSelector.ts create mode 100644 apps/stress-test/src/shared/tree/RandomSelectorTreeNode.ts create mode 100644 apps/stress-test/src/shared/tree/RandomTree.ts create mode 100644 apps/stress-test/src/shared/utils/random.ts create mode 100644 apps/stress-test/src/shared/utils/testUtils.ts diff --git a/apps/stress-test/package.json b/apps/stress-test/package.json index 0de90bde47e82a..9b9abe95b7d2af 100644 --- a/apps/stress-test/package.json +++ b/apps/stress-test/package.json @@ -15,6 +15,7 @@ "@fluentui/web-components": "^2.5.6", "@microsoft/fast-element": "^1.10.4", "react": "17.0.2", - "react-dom": "17.0.2" + "react-dom": "17.0.2", + "random-seedable": "1.0.8" } } diff --git a/apps/stress-test/src/shared/css/RandomSelector.ts b/apps/stress-test/src/shared/css/RandomSelector.ts new file mode 100644 index 00000000000000..caa8248bdcd67a --- /dev/null +++ b/apps/stress-test/src/shared/css/RandomSelector.ts @@ -0,0 +1,137 @@ +import { random, Random } from '../utils/random'; + +export const defaultSelectorTypes = [ + 'class', + 'tag', + 'nth-child', + 'pseudo-element', + 'not-class', + 'not-attribute', + 'attribute-name', + 'attribute-value', +] as const; + +type SelectorType = typeof defaultSelectorTypes[number]; + +type SelectorParams = { + seed?: number; + selectorTypes?: SelectorType[]; + tags?: string[]; + classNames?: string[]; + attributeNames?: string[]; + attributeValues?: string[]; +}; + +export class RandomSelector { + private rando: Random; + private readonly selectorTypes: SelectorType[]; + private tags: string[]; + private classNames: string[]; + private attributeNames: string[]; + private attributeValues: string[]; + + constructor({ seed, selectorTypes, tags, classNames, attributeNames, attributeValues }: SelectorParams = {}) { + this.rando = random(seed); + this.selectorTypes = selectorTypes ?? ((defaultSelectorTypes as unknown) as SelectorType[]); + this.tags = tags ?? []; + this.classNames = classNames ?? []; + this.attributeNames = attributeNames ?? []; + this.attributeValues = attributeValues ?? []; + } + + public randomSelector = (selectorTypes?: SelectorType[]): string => { + const selectorType = this._randomSelectorType(selectorTypes); + + switch (selectorType) { + case 'class': + return this._classSelector(); + + case 'tag': + return this._tagSelector(); + + case 'nth-child': + return this._nthChildSelector(); + + case 'pseudo-element': + return this._pseudoElement(); + + case 'not-class': + return this._notClass(); + + case 'not-attribute': + return this._notAttribute(); + + case 'attribute-name': + return this._attributeName(); + + case 'attribute-value': + return this._attributeValue(); + } + }; + + private _randomSelectorType = (selectorTypes?: SelectorType[]): SelectorType => { + return this.rando.choice(selectorTypes ?? this.selectorTypes); + }; + + private _classSelector = (): string => { + const selector = this._randomChoice(this.classNames) ?? this._randomString('random-classname'); + return `.${selector}`; + }; + + private _tagSelector = (): string => { + return this._randomChoice(this.tags) ?? this._randomString('random-tag'); + }; + + private _nthChildSelector = (): string => { + const choices = [':first-child', ':last-child', ':nth-child']; + const selector = this.rando.choice(choices); + + if (selector === ':nth-child') { + return `${selector}(${this.rando.range(1, 15)})`; + } + + return selector; + }; + + private _pseudoElement = (): string => { + const choices = ['::after', '::before', /*'::part',*/ '::placeholder', '::slotted']; + const selector = this.rando.choice(choices); + + return selector; + }; + + private _notClass = (): string => { + return `:not(${this._classSelector()})`; + }; + + private _notAttribute = (): string => { + return `:not(${this._attributeName()})`; + }; + + private _attributeName = (): string => { + return `[${this._randomAttributeName()}]`; + }; + + private _attributeValue = (): string => { + const name = this._randomAttributeName(); + const value = this._randomChoice(this.attributeValues) ?? this._randomString('random-attr-value'); + + return `[${name}=${value}]`; + }; + + private _randomChoice = (choices: string[]): string | undefined => { + if (choices.length > 0) { + return this.rando.choice(choices); + } + + return undefined; + }; + + private _randomAttributeName = (): string => { + return this._randomChoice(this.attributeNames) ?? this._randomString('data-random-attr'); + }; + + private _randomString = (prefix: string = 'random-string'): string => { + return `${prefix}-${this.rando.integer().toString(16)}`; + }; +} diff --git a/apps/stress-test/src/shared/tree/RandomSelectorTreeNode.ts b/apps/stress-test/src/shared/tree/RandomSelectorTreeNode.ts new file mode 100644 index 00000000000000..28b0cb2f87c27f --- /dev/null +++ b/apps/stress-test/src/shared/tree/RandomSelectorTreeNode.ts @@ -0,0 +1,139 @@ +import { RandomSelector } from '../css/RandomSelector'; +import { TreeNode, TreeNodeCreateCallback } from './RandomTree'; +import { random } from '../utils/random'; + +export type Attribute = { + key: string; + value: string | undefined; + selector: string; +}; + +export type RandomSelectorTreeNode = { + name: string; + classNames: string[]; + attributes: Attribute[]; + siblings: string[]; + pseudos: string[]; +}; + +export type SelectorTreeNode = TreeNode; + +const { coin, choice } = random(); +const randomSelector = new RandomSelector(); + +const chances: { [key: string]: number } = { + not: 0.05, + addClassName: 0.5, + addAttribute: 0.25, + buildDescendentSelector: 0.75, + addSibling: 0.25, + addPseudo: 0.25, + useDescendantCombinator: 0.2, +}; + +const buildDescendentSelector = ( + node: TreeNode | null, + selector: string = '', +): string => { + if (!node) { + return selector; + } + + selector = ( + maybeNot(choice(getSelectorsFromNode(node))) + + (coin(chances.useDescendantCombinator) ? ' > ' : ' ') + + selector + ).trim(); + + if (coin(chances.buildDescendentSelector)) { + selector = buildDescendentSelector(node.parent, selector); + } + + return selector; +}; + +const getNodeClassNames = () => { + const nodeSelectors = [randomSelector.randomSelector(['class'])]; + if (coin(chances.addClassName)) { + nodeSelectors.push(randomSelector.randomSelector(['class'])); + } + + return nodeSelectors; +}; + +const maybeNot = (selector: string): string => { + if (coin(chances.not)) { + return `:not(${selector})`; + } + + return selector; +}; + +const getAttributes = () => { + const attributes = []; + if (coin(chances.addAttribute)) { + const selector = randomSelector.randomSelector(['attribute-name', 'attribute-value']); + const [key, value] = selector.replace(/(\[|\])/g, '').split('='); + attributes.push({ key, value, selector }); + } + + return attributes; +}; + +const getSiblingSelectors = () => { + const siblings = []; + + if (coin(chances.addSibling)) { + siblings.push(randomSelector.randomSelector(['nth-child'])); + } + + return siblings; +}; + +const getPseudoSelectors = () => { + const pseudo = []; + + if (coin(chances.addPsuedo)) { + pseudo.push(randomSelector.randomSelector(['pseudo-element'])); + } + + return pseudo; +}; + +const getSelectorsFromNode = (node: TreeNode): string[] => { + return [ + ...node.value.classNames, + ...node.value.attributes.map(attr => attr.selector), + ...node.value.siblings, + ...node.value.pseudos, + ]; +}; + +export type RandomSelectorTreeCreator = (selectors: string[]) => TreeNodeCreateCallback; + +export const selectorTreeCreator: RandomSelectorTreeCreator = selectors => { + const createSelectorTree: TreeNodeCreateCallback = (parent, depth, breadth) => { + const node = { + value: { + name: `${depth}-${breadth}`, + classNames: getNodeClassNames(), + attributes: getAttributes(), + siblings: getSiblingSelectors(), + pseudos: getPseudoSelectors(), + }, + children: [], + parent, + }; + + if (coin(chances.buildDescendentSelector)) { + const descendentSelector = buildDescendentSelector(node); + selectors.push(descendentSelector); + } else { + selectors.push(...getSelectorsFromNode(node)); + } + + return node; + }; + + return createSelectorTree; +}; diff --git a/apps/stress-test/src/shared/tree/RandomTree.ts b/apps/stress-test/src/shared/tree/RandomTree.ts new file mode 100644 index 00000000000000..acd53db3600b75 --- /dev/null +++ b/apps/stress-test/src/shared/tree/RandomTree.ts @@ -0,0 +1,107 @@ +import { random, Random } from '../utils/random'; +import { TestFixture } from '../utils/testUtils'; + +type TreeParams = { + minDepth?: number; + maxDepth?: number; + minBreadth?: number; + maxBreadth?: number; + seed?: number; +}; + +export type TreeNode = { + value: T; + children: TreeNode[]; + parent: TreeNode | null; +}; + +export type TreeNodeCreateCallback = (parent: TreeNode | null, depth: number, breath: number) => TreeNode; +export type TreeNodeVisitCallback = (node: TreeNode) => void; + +/** + * Exponential decay function. + * See: https://www.cuemath.com/exponential-decay-formula/ + * @param a - Initial amount + * @param r - Rate of decay + * @param x - Decay interval + * @returns Current amount after applying decay function. + */ +const decay = (a: number, r: number, x: number): number => { + return a * Math.pow(1 - r, x); +}; + +export class RandomTree { + private numNodes: number; + private minDepth: number; + private maxDepth: number; + private minBreadth: number; + private maxBreadth: number; + + private rando: Random; + + constructor({ minDepth = 1, maxDepth = 15, minBreadth = 1, maxBreadth = 15, seed }: TreeParams = {}) { + this.minDepth = minDepth; + this.maxDepth = maxDepth; + this.minBreadth = minBreadth; + this.maxBreadth = maxBreadth; + + this.rando = random(seed); + this.numNodes = 0; + } + + public build = (createNode: TreeNodeCreateCallback): TreeNode => { + this.numNodes = 1; + const root = createNode(null, 0, 0); + return this._doBuild(createNode, root, 1); + }; + + public fromFixture = (fixture: TestFixture['tree'], parent: TreeNode | null = null): TreeNode => { + const root: TreeNode = { + value: fixture.value, + children: [], + parent, + }; + + for (const child of fixture.children) { + root.children.push(this.fromFixture(child, root)); + } + + return root; + }; + + private _randomDepth = (max?: number): number => { + return this.rando.range(this.minDepth, max ?? this.maxDepth); + }; + + private _randomBreadth = (max?: number): number => { + return this.rando.range(this.minBreadth, max ?? this.maxBreadth); + }; + + private _doBuild = ( + createNode: TreeNodeCreateCallback, + parent: TreeNode, + currentDepth: number, + currentBreadth: number = this.maxBreadth, + ): TreeNode => { + const breadth = this._randomBreadth(currentBreadth); + const depth = this._randomDepth(Math.max(this.maxDepth - currentDepth, this.minDepth)); + + for (let i = 0; i < breadth; i++) { + this.numNodes++; + const node = createNode(parent, currentDepth, breadth); + + parent.children.push(node); + + if (currentDepth < depth) { + this._doBuild( + createNode, + node, + currentDepth + 1, + Math.max(Math.ceil(decay(this.maxBreadth, 0.005, this.numNodes)), this.minBreadth), + ); + } + } + + return parent; + }; +} diff --git a/apps/stress-test/src/shared/utils/random.ts b/apps/stress-test/src/shared/utils/random.ts new file mode 100644 index 00000000000000..b04b3d8d1320c0 --- /dev/null +++ b/apps/stress-test/src/shared/utils/random.ts @@ -0,0 +1,24 @@ +import { LCG } from 'random-seedable'; + +const defaultSeed = 4212021; + +export type Random = { + coin: (pTrue: number) => boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + choice: (choices: any[]) => any; + range: (min: number, max: number) => number; + integer: () => number; +}; + +export type RandomFn = (seed?: number) => Random; + +export const random: RandomFn = seed => { + const rando: LCG = new LCG(seed ?? defaultSeed); + + return { + coin: (pTrue = 0.5) => rando.coin(pTrue), + choice: choices => rando.choice(choices), + range: (min, max) => rando.randRange(min, max), + integer: () => rando.int(), + }; +}; diff --git a/apps/stress-test/src/shared/utils/testUtils.ts b/apps/stress-test/src/shared/utils/testUtils.ts new file mode 100644 index 00000000000000..95299cd229054d --- /dev/null +++ b/apps/stress-test/src/shared/utils/testUtils.ts @@ -0,0 +1,11 @@ +export type TestFixture = { + tree: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any; + children: TestFixture['tree'][]; + }; + selectors: string[]; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type TestRenderer = (node: any, depth: number, index: number) => any; diff --git a/yarn.lock b/yarn.lock index df23a510368cab..70d9478d5f1063 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22573,6 +22573,11 @@ randexp@0.4.6: discontinuous-range "1.0.0" ret "~0.1.10" +random-seedable@1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/random-seedable/-/random-seedable-1.0.8.tgz#d233ce5d49f64a20be398bbc3faa695cc39b8aa8" + integrity sha512-f6gzvNhAnZBht1Prn0e/tpukUNhkANntFF42uIdWDPriyEATYaRpyH8A9bYaGecUB3AL+dXeYtBUggy18fe3rw== + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" From c5e7b6af6be72d435f0323792b7b7bed7bbc75e4 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Wed, 21 Sep 2022 11:34:09 -0700 Subject: [PATCH 2/3] stress test: add tree renderer implementations Adds different tree rendering implementations for the UI libraries we are currently focused on. --- .../src/shared/react/ReactSelectorTree.tsx | 33 ++++++++++ .../src/shared/react/ReactTree.tsx | 39 +++++++++++ apps/stress-test/src/shared/react/types.ts | 12 ++++ .../src/shared/utils/testOptions.ts | 58 +++++++++++++++++ .../src/shared/vanilla/VanillaSelectorTree.ts | 33 ++++++++++ .../src/shared/vanilla/VanillaTree.ts | 25 ++++++++ apps/stress-test/src/shared/vanilla/types.ts | 3 + .../src/shared/wc/WCSelectorTree.ts | 64 +++++++++++++++++++ apps/stress-test/src/shared/wc/WCTree.ts | 64 +++++++++++++++++++ 9 files changed, 331 insertions(+) create mode 100644 apps/stress-test/src/shared/react/ReactSelectorTree.tsx create mode 100644 apps/stress-test/src/shared/react/ReactTree.tsx create mode 100644 apps/stress-test/src/shared/react/types.ts create mode 100644 apps/stress-test/src/shared/utils/testOptions.ts create mode 100644 apps/stress-test/src/shared/vanilla/VanillaSelectorTree.ts create mode 100644 apps/stress-test/src/shared/vanilla/VanillaTree.ts create mode 100644 apps/stress-test/src/shared/vanilla/types.ts create mode 100644 apps/stress-test/src/shared/wc/WCSelectorTree.ts create mode 100644 apps/stress-test/src/shared/wc/WCTree.ts diff --git a/apps/stress-test/src/shared/react/ReactSelectorTree.tsx b/apps/stress-test/src/shared/react/ReactSelectorTree.tsx new file mode 100644 index 00000000000000..7dd0df4d8bb2ca --- /dev/null +++ b/apps/stress-test/src/shared/react/ReactSelectorTree.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { TreeNode } from '../tree/RandomTree'; +import { ReactTree } from './ReactTree'; +import { RandomSelectorTreeNode, SelectorTreeNode } from '../tree/RandomSelectorTreeNode'; +import { ReactSelectorTreeComponentRenderer } from './types'; + +type ReactSelectorTreeProps = { + tree?: TreeNode; + componentRenderer: ReactSelectorTreeComponentRenderer; +}; + +const buildRenderer = (componentRenderer: ReactSelectorTreeComponentRenderer) => { + const renderer = (node: SelectorTreeNode, depth: number, index: number): JSX.Element => { + const { value } = node; + + const className = value.classNames.map(cn => cn.substring(1)).join(' '); + const attrs = value.attributes.reduce((map, attr) => { + map[attr.key] = attr.value ?? ''; + return map; + }, {} as { [key: string]: string }); + + return ( +
+ {componentRenderer(node, depth, index)} +
+ ); + }; + + return renderer; +}; +export const ReactSelectorTree: React.FC = ({ tree, componentRenderer }) => { + return <>{tree ? : null}; +}; diff --git a/apps/stress-test/src/shared/react/ReactTree.tsx b/apps/stress-test/src/shared/react/ReactTree.tsx new file mode 100644 index 00000000000000..a27deb12802577 --- /dev/null +++ b/apps/stress-test/src/shared/react/ReactTree.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { TreeNode } from '../tree/RandomTree'; + +export type ReactTreeItemRenderer = (node: T, depth: number, index: number) => JSX.Element; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ReactTreeProps> = { + tree: T; + itemRenderer: ReactTreeItemRenderer; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ReactTreeNodeProps> = { + root: T; + renderer: ReactTreeItemRenderer; + depth?: number; + index?: number; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const ReactTreeNode = >(props: ReactTreeNodeProps): JSX.Element => { + const { root, renderer, depth = 0, index = 0, ...others } = props; + + return ( +
+ {renderer(root, depth, index)} + {root.children.map((child, i) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return ; + })} +
+ ); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const ReactTree = >({ tree, itemRenderer }: ReactTreeProps): JSX.Element => { + return ; +}; diff --git a/apps/stress-test/src/shared/react/types.ts b/apps/stress-test/src/shared/react/types.ts new file mode 100644 index 00000000000000..96eab1e699dc30 --- /dev/null +++ b/apps/stress-test/src/shared/react/types.ts @@ -0,0 +1,12 @@ +import { TestOptions } from '../utils/testOptions'; +import { RandomSelectorTreeNode, SelectorTreeNode } from '../tree/RandomSelectorTreeNode'; +import { TreeNode } from '../tree/RandomTree'; + +export type ReactSelectorTreeComponentRenderer = (node: SelectorTreeNode, depth: number, index: number) => JSX.Element; + +export type TestProps = { + componentRenderer: ReactSelectorTreeComponentRenderer; + tree: TreeNode; + selectors: string[]; + testOptions: TestOptions; +}; diff --git a/apps/stress-test/src/shared/utils/testOptions.ts b/apps/stress-test/src/shared/utils/testOptions.ts new file mode 100644 index 00000000000000..99d2a168475638 --- /dev/null +++ b/apps/stress-test/src/shared/utils/testOptions.ts @@ -0,0 +1,58 @@ +export type TestOptions = { + test: string; + fixtureName: string; + rendererName: string; + [key: string]: string | number; +}; + +export type GetTestOptionsFn = () => TestOptions; + +let params: TestOptions; +export const getTestOptions = () => { + if (params) { + return params; + } + params = {} as TestOptions; + + if (typeof window === 'undefined') { + return params; + } + + const searchParams = new URLSearchParams(window.location.search); + + let test = searchParams.get('test'); + if (!test) { + test = 'manual'; + } + + let numStartNodes = Number(searchParams.get('numStartNodes')); + if (!numStartNodes || isNaN(numStartNodes)) { + numStartNodes = 100; + } + + let numAddNodes = Number(searchParams.get('numAddNodes')); + if (!numAddNodes || isNaN(numAddNodes)) { + numAddNodes = 100; + } + + let numRemoveNodes = Number(searchParams.get('numRemoveNodes')); + if (!numRemoveNodes || isNaN(numRemoveNodes)) { + numRemoveNodes = 99; + } + + params.test = test as TestOptions['test']; + params.numStartNodes = numStartNodes; + params.numAddNodes = numAddNodes; + params.numRemoveNodes = numRemoveNodes; + + const ignore = ['numStartNodes', 'numAddNodes', 'numRemoveNodes']; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + for (const [key, value] of searchParams.entries()) { + if (!ignore.includes(key)) { + params[key] = value; + } + } + + return params; +}; diff --git a/apps/stress-test/src/shared/vanilla/VanillaSelectorTree.ts b/apps/stress-test/src/shared/vanilla/VanillaSelectorTree.ts new file mode 100644 index 00000000000000..308fbd70abe436 --- /dev/null +++ b/apps/stress-test/src/shared/vanilla/VanillaSelectorTree.ts @@ -0,0 +1,33 @@ +import { TestOptions } from '../utils/testOptions'; +import { SelectorTreeNode } from '../tree/RandomSelectorTreeNode'; +import { DOMSelectorTreeComponentRenderer } from './types'; +import { renderVanillaTree } from './VanillaTree'; + +const itemRenderer = (componentRenderer: DOMSelectorTreeComponentRenderer) => ( + node: SelectorTreeNode, + depth: number, + index: number, +): HTMLElement => { + const { value } = node; + + const div = document.createElement('div'); + div.classList.add(...value.classNames.map(cn => cn.substring(1))); + value.attributes.forEach(attr => { + div.setAttribute(attr.key, attr.value ?? ''); + }); + + div.style.marginLeft = `${depth * 10}px`; + div.appendChild(componentRenderer(node, depth, index)); + + return div; +}; + +export const renderVanillaSelectorTree = ( + tree: SelectorTreeNode, + _selectors: string[], + componentRenderer: DOMSelectorTreeComponentRenderer, + _testOptions: TestOptions, +): HTMLElement => { + const vanillaTree = renderVanillaTree(tree, itemRenderer(componentRenderer)); + return vanillaTree; +}; diff --git a/apps/stress-test/src/shared/vanilla/VanillaTree.ts b/apps/stress-test/src/shared/vanilla/VanillaTree.ts new file mode 100644 index 00000000000000..8939b00b10c9bb --- /dev/null +++ b/apps/stress-test/src/shared/vanilla/VanillaTree.ts @@ -0,0 +1,25 @@ +import { TreeNode } from '../tree/RandomTree'; + +export type VanillaTreeItemRenderer = (node: T, depth: number, index: number) => HTMLElement; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const renderVanillaTree = >( + tree: T, + itemRenderer: VanillaTreeItemRenderer, + depth: number = 0, + index: number = 0, +): HTMLElement => { + const root = document.createElement('div'); + root.classList.add('vanilla-tree-node'); + + root.appendChild(itemRenderer(tree, depth, index)); + + tree.children.forEach((child, i) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const node = renderVanillaTree(child, itemRenderer, depth + 1, i + 1); + root.appendChild(node); + }); + + return root; +}; diff --git a/apps/stress-test/src/shared/vanilla/types.ts b/apps/stress-test/src/shared/vanilla/types.ts new file mode 100644 index 00000000000000..3700ac33581877 --- /dev/null +++ b/apps/stress-test/src/shared/vanilla/types.ts @@ -0,0 +1,3 @@ +import { SelectorTreeNode } from '../tree/RandomSelectorTreeNode'; + +export type DOMSelectorTreeComponentRenderer = (node: SelectorTreeNode, depth: number, index: number) => HTMLElement; diff --git a/apps/stress-test/src/shared/wc/WCSelectorTree.ts b/apps/stress-test/src/shared/wc/WCSelectorTree.ts new file mode 100644 index 00000000000000..957d862e5d6214 --- /dev/null +++ b/apps/stress-test/src/shared/wc/WCSelectorTree.ts @@ -0,0 +1,64 @@ +import { TestOptions } from '../utils/testOptions'; +import { SelectorTreeNode } from '../tree/RandomSelectorTreeNode'; +import { DOMSelectorTreeComponentRenderer } from '../vanilla/types'; +import { WCTree } from './WCTree'; + +const template = document.createElement('template'); +template.innerHTML = `
`; + +export class WCSelectorTree extends HTMLElement { + private _root: HTMLElement | ShadowRoot; + private _testOptions: TestOptions; + private _tree: SelectorTreeNode | null; + private _componentRenderer: DOMSelectorTreeComponentRenderer; + + constructor(componentRenderer: DOMSelectorTreeComponentRenderer, testOptions: TestOptions) { + super(); + + this._testOptions = testOptions; + this._tree = null; + + this._componentRenderer = componentRenderer; + + if (this._testOptions?.useShadowRoot === 'true') { + this._root = this.attachShadow({ mode: 'open' }); + } else { + this._root = this; + } + } + + public set tree(value: SelectorTreeNode | null) { + this._tree = value; + + if (value === null) { + while (this._root.hasChildNodes()) { + this._root.removeChild(this._root.lastChild!); + } + } else { + const wcTree = new WCTree(this._itemRenderer, undefined, undefined, this._testOptions?.useShadowRoot === 'true'); + wcTree.tree = value; + this._root.appendChild(wcTree); + } + } + + public get tree() { + return this._tree; + } + + private _itemRenderer = (node: SelectorTreeNode, depth: number, index: number): HTMLElement => { + const { value } = node; + + const div = document.createElement('div'); + div.classList.add(...value.classNames.map(cn => cn.substring(1))); + value.attributes.forEach(attr => { + div.setAttribute(attr.key, attr.value ?? ''); + }); + + div.style.marginLeft = `${depth * 10}px`; + div.appendChild(this._componentRenderer(node, depth, index)); + + return div; + }; +} + +window.customElements.define('wc-selector-tree', WCSelectorTree); diff --git a/apps/stress-test/src/shared/wc/WCTree.ts b/apps/stress-test/src/shared/wc/WCTree.ts new file mode 100644 index 00000000000000..7218232a7d8d22 --- /dev/null +++ b/apps/stress-test/src/shared/wc/WCTree.ts @@ -0,0 +1,64 @@ +import { TreeNode } from '../tree/RandomTree'; + +export type WCTreeItemRenderer = (node: T, depth: number, index: number) => HTMLElement; + +const template = document.createElement('template'); +template.innerHTML = ` +
+`; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export class WCTree> extends HTMLElement { + private _root: HTMLElement | ShadowRoot; + private _tree: T | null; + private _itemRenderer: WCTreeItemRenderer; + private _depth: number; + private _index: number; + private _useShadowRoot: boolean; + + constructor(itemRenderer: WCTreeItemRenderer, depth: number = 0, index = 0, useShadowRoot: boolean = false) { + super(); + + this._useShadowRoot = useShadowRoot; + + if (useShadowRoot) { + this._root = this.attachShadow({ mode: 'open' }); + } else { + this._root = this; + } + this._tree = null; + this._itemRenderer = itemRenderer; + this._depth = depth; + this._index = index; + } + + public set tree(value: T | null) { + this._tree = value; + this._render(); + } + + public get tree(): T | null { + return this._tree; + } + + private _render() { + if (this._tree === null) { + while (this._root.hasChildNodes()) { + this._root.removeChild(this._root.lastChild!); + } + return; + } + + this._root.appendChild(this._itemRenderer(this._tree, this._depth, this._index)); + + this._tree.children.forEach((child, i) => { + const node = new WCTree(this._itemRenderer, this._depth + 1, i + 1, this._useShadowRoot); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + node.tree = child; + this._root.appendChild(node); + }); + } +} + +window.customElements.define('wc-tree', WCTree); From d2431ef256926392b216555e343a8974c9caefbe Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Wed, 21 Sep 2022 15:22:41 -0700 Subject: [PATCH 3/3] clean up typescript types --- apps/stress-test/src/shared/react/ReactTree.tsx | 16 +++++----------- apps/stress-test/src/shared/tree/RandomTree.ts | 2 +- apps/stress-test/src/shared/utils/testOptions.ts | 13 +++++++++---- apps/stress-test/src/shared/utils/testUtils.ts | 6 ++---- .../src/shared/vanilla/VanillaTree.ts | 7 ++----- apps/stress-test/src/shared/wc/WCTree.ts | 7 ++----- 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/apps/stress-test/src/shared/react/ReactTree.tsx b/apps/stress-test/src/shared/react/ReactTree.tsx index a27deb12802577..4324d5ac94b149 100644 --- a/apps/stress-test/src/shared/react/ReactTree.tsx +++ b/apps/stress-test/src/shared/react/ReactTree.tsx @@ -3,37 +3,31 @@ import { TreeNode } from '../tree/RandomTree'; export type ReactTreeItemRenderer = (node: T, depth: number, index: number) => JSX.Element; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ReactTreeProps> = { +export type ReactTreeProps> = { tree: T; itemRenderer: ReactTreeItemRenderer; }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ReactTreeNodeProps> = { +export type ReactTreeNodeProps> = { root: T; renderer: ReactTreeItemRenderer; depth?: number; index?: number; }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const ReactTreeNode = >(props: ReactTreeNodeProps): JSX.Element => { +export const ReactTreeNode = >(props: ReactTreeNodeProps): JSX.Element => { const { root, renderer, depth = 0, index = 0, ...others } = props; return (
{renderer(root, depth, index)} {root.children.map((child, i) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return ; + return ; })}
); }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const ReactTree = >({ tree, itemRenderer }: ReactTreeProps): JSX.Element => { +export const ReactTree = >({ tree, itemRenderer }: ReactTreeProps): JSX.Element => { return ; }; diff --git a/apps/stress-test/src/shared/tree/RandomTree.ts b/apps/stress-test/src/shared/tree/RandomTree.ts index acd53db3600b75..f41d024c1fb8e9 100644 --- a/apps/stress-test/src/shared/tree/RandomTree.ts +++ b/apps/stress-test/src/shared/tree/RandomTree.ts @@ -57,7 +57,7 @@ export class RandomTree { public fromFixture = (fixture: TestFixture['tree'], parent: TreeNode | null = null): TreeNode => { const root: TreeNode = { - value: fixture.value, + value: fixture.value as T, children: [], parent, }; diff --git a/apps/stress-test/src/shared/utils/testOptions.ts b/apps/stress-test/src/shared/utils/testOptions.ts index 99d2a168475638..7e54a77013ef3b 100644 --- a/apps/stress-test/src/shared/utils/testOptions.ts +++ b/apps/stress-test/src/shared/utils/testOptions.ts @@ -7,6 +7,12 @@ export type TestOptions = { export type GetTestOptionsFn = () => TestOptions; +declare global { + interface URLSearchParams { + keys: () => string[]; + } +} + let params: TestOptions; export const getTestOptions = () => { if (params) { @@ -46,10 +52,9 @@ export const getTestOptions = () => { params.numRemoveNodes = numRemoveNodes; const ignore = ['numStartNodes', 'numAddNodes', 'numRemoveNodes']; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - for (const [key, value] of searchParams.entries()) { - if (!ignore.includes(key)) { + for (const key of searchParams.keys()) { + const value = searchParams.get(key); + if (value && !ignore.includes(key)) { params[key] = value; } } diff --git a/apps/stress-test/src/shared/utils/testUtils.ts b/apps/stress-test/src/shared/utils/testUtils.ts index 95299cd229054d..83b5a13d36b97c 100644 --- a/apps/stress-test/src/shared/utils/testUtils.ts +++ b/apps/stress-test/src/shared/utils/testUtils.ts @@ -1,11 +1,9 @@ export type TestFixture = { tree: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: any; + value: unknown; children: TestFixture['tree'][]; }; selectors: string[]; }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type TestRenderer = (node: any, depth: number, index: number) => any; +export type TestRenderer = (node: unknown, depth: number, index: number) => unknown; diff --git a/apps/stress-test/src/shared/vanilla/VanillaTree.ts b/apps/stress-test/src/shared/vanilla/VanillaTree.ts index 8939b00b10c9bb..eeb250045b43fd 100644 --- a/apps/stress-test/src/shared/vanilla/VanillaTree.ts +++ b/apps/stress-test/src/shared/vanilla/VanillaTree.ts @@ -2,8 +2,7 @@ import { TreeNode } from '../tree/RandomTree'; export type VanillaTreeItemRenderer = (node: T, depth: number, index: number) => HTMLElement; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const renderVanillaTree = >( +export const renderVanillaTree = >( tree: T, itemRenderer: VanillaTreeItemRenderer, depth: number = 0, @@ -15,9 +14,7 @@ export const renderVanillaTree = >( root.appendChild(itemRenderer(tree, depth, index)); tree.children.forEach((child, i) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const node = renderVanillaTree(child, itemRenderer, depth + 1, i + 1); + const node = renderVanillaTree(child as T, itemRenderer, depth + 1, i + 1); root.appendChild(node); }); diff --git a/apps/stress-test/src/shared/wc/WCTree.ts b/apps/stress-test/src/shared/wc/WCTree.ts index 7218232a7d8d22..cd803d593e1209 100644 --- a/apps/stress-test/src/shared/wc/WCTree.ts +++ b/apps/stress-test/src/shared/wc/WCTree.ts @@ -7,8 +7,7 @@ template.innerHTML = `
`; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export class WCTree> extends HTMLElement { +export class WCTree> extends HTMLElement { private _root: HTMLElement | ShadowRoot; private _tree: T | null; private _itemRenderer: WCTreeItemRenderer; @@ -53,9 +52,7 @@ export class WCTree> extends HTMLElement { this._tree.children.forEach((child, i) => { const node = new WCTree(this._itemRenderer, this._depth + 1, i + 1, this._useShadowRoot); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - node.tree = child; + node.tree = child as T; this._root.appendChild(node); }); }