Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/stress-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
137 changes: 137 additions & 0 deletions apps/stress-test/src/shared/css/RandomSelector.ts
Original file line number Diff line number Diff line change
@@ -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)}`;
};
}
33 changes: 33 additions & 0 deletions apps/stress-test/src/shared/react/ReactSelectorTree.tsx
Original file line number Diff line number Diff line change
@@ -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<RandomSelectorTreeNode>;
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 (
<div className={className} {...attrs} style={{ marginLeft: `${depth * 10}px` }}>
{componentRenderer(node, depth, index)}
</div>
);
};

return renderer;
};
export const ReactSelectorTree: React.FC<ReactSelectorTreeProps> = ({ tree, componentRenderer }) => {
return <>{tree ? <ReactTree tree={tree} itemRenderer={buildRenderer(componentRenderer)} /> : null}</>;
};
33 changes: 33 additions & 0 deletions apps/stress-test/src/shared/react/ReactTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as React from 'react';
import { TreeNode } from '../tree/RandomTree';

export type ReactTreeItemRenderer<T> = (node: T, depth: number, index: number) => JSX.Element;

export type ReactTreeProps<T extends TreeNode<unknown>> = {
tree: T;
itemRenderer: ReactTreeItemRenderer<T>;
};

export type ReactTreeNodeProps<T extends TreeNode<unknown>> = {
root: T;
renderer: ReactTreeItemRenderer<T>;
depth?: number;
index?: number;
};

export const ReactTreeNode = <T extends TreeNode<unknown>>(props: ReactTreeNodeProps<T>): JSX.Element => {
const { root, renderer, depth = 0, index = 0, ...others } = props;

return (
<div className="react-tree-node" {...others}>
{renderer(root, depth, index)}
{root.children.map((child, i) => {
return <ReactTreeNode root={child as T} key={i} renderer={renderer} depth={depth + 1} index={i + 1} />;
})}
</div>
);
};

export const ReactTree = <T extends TreeNode<unknown>>({ tree, itemRenderer }: ReactTreeProps<T>): JSX.Element => {
return <ReactTreeNode root={tree} renderer={itemRenderer} />;
};
12 changes: 12 additions & 0 deletions apps/stress-test/src/shared/react/types.ts
Original file line number Diff line number Diff line change
@@ -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<RandomSelectorTreeNode>;
selectors: string[];
testOptions: TestOptions;
};
139 changes: 139 additions & 0 deletions apps/stress-test/src/shared/tree/RandomSelectorTreeNode.ts
Original file line number Diff line number Diff line change
@@ -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<RandomSelectorTreeNode>;

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 = <T extends RandomSelectorTreeNode>(
node: TreeNode<T> | 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<RandomSelectorTreeNode>): string[] => {
return [
...node.value.classNames,
...node.value.attributes.map(attr => attr.selector),
...node.value.siblings,
...node.value.pseudos,
];
};

export type RandomSelectorTreeCreator = (selectors: string[]) => TreeNodeCreateCallback<RandomSelectorTreeNode>;

export const selectorTreeCreator: RandomSelectorTreeCreator = selectors => {
const createSelectorTree: TreeNodeCreateCallback<RandomSelectorTreeNode> = (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;
};
Loading