From d3fba4b36a15b92b37e44d7b373b5e4c07f1cd1f Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Fri, 23 Sep 2022 14:15:09 -0700 Subject: [PATCH 1/8] stress test: adds test cases for v8, v9 and web components Adds tests cases for - mounting - injecting styles - adding lots of nodes - removing all nodes - re-rendering the entire page for Fluent V8, V9 and Web Components. Introduces the concept of "renderers" that easily allow different components to be slotting into tests with just a query parameter. Moves RandomTree code to scripts to handle a ts-node compilation issue. This isn't ideal but it doesn't seem worth spending lots of time figuring the proper issue out right now. Similarly, some of the randomization code has been moved around in scripts because ts-node was having difficulty importing it. --- apps/stress-test/.gitignore | 1 + apps/stress-test/scripts/utils/fixtures.ts | 5 +- .../utils}/tree/RandomSelectorTreeNode.ts | 30 +-- .../utils}/tree/RandomTree.ts | 28 +-- .../src/components/v8/StressApp.tsx | 12 +- .../src/components/v8/StressContainer.tsx | 8 +- .../src/components/v9/StressApp.tsx | 12 +- .../src/components/v9/StressContainer.tsx | 8 +- .../src/components/wc/stressApp.wc.ts | 10 +- .../src/components/wc/stressContainer.wc.ts | 8 +- .../src/pages/v8/stress-tree/index.html | 11 + .../src/pages/v8/stress-tree/index.tsx | 16 ++ .../src/pages/v9/stress-tree/index.html | 11 + .../src/pages/v9/stress-tree/index.tsx | 14 ++ .../src/pages/wc/simple-stress/index.wc.ts | 6 +- .../src/pages/wc/stress-tree/index.html | 11 + .../src/pages/wc/stress-tree/index.wc.ts | 5 + apps/stress-test/src/renderers/v8/button.tsx | 9 + .../src/renderers/v8/spinButton.tsx | 9 + apps/stress-test/src/renderers/v8/stress.tsx | 49 ++++ .../renderers/v9/btnOverride/idiomatic.tsx | 44 ++++ .../src/renderers/v9/btnOverride/override.tsx | 42 ++++ .../src/renderers/v9/btnOverride/styles.ts | 90 ++++++++ apps/stress-test/src/renderers/v9/button.tsx | 9 + .../src/renderers/v9/spinButton.tsx | 9 + apps/stress-test/src/renderers/v9/stress.tsx | 54 +++++ apps/stress-test/src/renderers/wc/button.ts | 13 ++ .../src/renderers/wc/numberField.ts | 13 ++ apps/stress-test/src/renderers/wc/stress.ts | 11 + apps/stress-test/src/shared/Element.tsx | 35 --- .../stress-test/src/shared/ElementContext.tsx | 14 -- .../src/shared/css/injectStyles.ts | 211 ++++++++++++++++++ apps/stress-test/src/shared/injectStyles.ts | 38 ---- .../src/shared/performanceMeasure.ts | 17 -- .../src/shared/react/ReactSelectorTree.tsx | 4 +- .../src/shared/react/ReactTest.tsx | 90 ++++++++ .../src/shared/react/ReactTree.tsx | 2 +- apps/stress-test/src/shared/react/TestAdd.tsx | 29 +++ .../src/shared/react/TestInjectStyles.tsx | 16 ++ .../src/shared/react/TestMount.tsx | 17 ++ .../src/shared/react/TestReRenderAll.tsx | 32 +++ .../src/shared/react/TestRemoveAll.tsx | 28 +++ apps/stress-test/src/shared/react/types.ts | 3 +- apps/stress-test/src/shared/testParams.ts | 51 ----- .../src/shared/tree/iterators/dfs.ts | 28 +++ apps/stress-test/src/shared/tree/types.ts | 24 ++ .../src/shared/utils/performanceMeasure.ts | 11 + .../shared/utils/requestPostAnimationFrame.ts | 6 + .../stress-test/src/shared/utils/testUtils.ts | 28 ++- .../stress-test/src/shared/vanilla/TestAdd.ts | 27 +++ .../src/shared/vanilla/TestInjectStyles.ts | 22 ++ .../src/shared/vanilla/TestMount.ts | 17 ++ .../src/shared/vanilla/TestReRenderAll.ts | 33 +++ .../src/shared/vanilla/TestRemoveAll.ts | 29 +++ .../src/shared/vanilla/VanillaSelectorTree.ts | 2 +- .../src/shared/vanilla/VanillaTree.ts | 2 +- apps/stress-test/src/shared/vanilla/types.ts | 2 +- apps/stress-test/src/shared/wc/TestAdd.ts | 35 +++ .../src/shared/wc/TestInjectStyles.ts | 29 +++ apps/stress-test/src/shared/wc/TestMount.ts | 25 +++ .../src/shared/wc/TestReRenderAll.ts | 37 +++ .../src/shared/wc/TestRemoveAll.ts | 33 +++ .../src/shared/wc/WCSelectorTree.ts | 2 +- apps/stress-test/src/shared/wc/WCTest.ts | 61 +++++ apps/stress-test/src/shared/wc/WCTree.ts | 2 +- apps/stress-test/src/shared/wc/types.ts | 4 + apps/stress-test/webpack/webpack.config.ts | 2 +- 67 files changed, 1355 insertions(+), 241 deletions(-) rename apps/stress-test/{src/shared => scripts/utils}/tree/RandomSelectorTreeNode.ts (82%) rename apps/stress-test/{src/shared => scripts/utils}/tree/RandomTree.ts (74%) create mode 100644 apps/stress-test/src/pages/v8/stress-tree/index.html create mode 100644 apps/stress-test/src/pages/v8/stress-tree/index.tsx create mode 100644 apps/stress-test/src/pages/v9/stress-tree/index.html create mode 100644 apps/stress-test/src/pages/v9/stress-tree/index.tsx create mode 100644 apps/stress-test/src/pages/wc/stress-tree/index.html create mode 100644 apps/stress-test/src/pages/wc/stress-tree/index.wc.ts create mode 100644 apps/stress-test/src/renderers/v8/button.tsx create mode 100644 apps/stress-test/src/renderers/v8/spinButton.tsx create mode 100644 apps/stress-test/src/renderers/v8/stress.tsx create mode 100644 apps/stress-test/src/renderers/v9/btnOverride/idiomatic.tsx create mode 100644 apps/stress-test/src/renderers/v9/btnOverride/override.tsx create mode 100644 apps/stress-test/src/renderers/v9/btnOverride/styles.ts create mode 100644 apps/stress-test/src/renderers/v9/button.tsx create mode 100644 apps/stress-test/src/renderers/v9/spinButton.tsx create mode 100644 apps/stress-test/src/renderers/v9/stress.tsx create mode 100644 apps/stress-test/src/renderers/wc/button.ts create mode 100644 apps/stress-test/src/renderers/wc/numberField.ts create mode 100644 apps/stress-test/src/renderers/wc/stress.ts delete mode 100644 apps/stress-test/src/shared/Element.tsx delete mode 100644 apps/stress-test/src/shared/ElementContext.tsx create mode 100644 apps/stress-test/src/shared/css/injectStyles.ts delete mode 100644 apps/stress-test/src/shared/injectStyles.ts delete mode 100644 apps/stress-test/src/shared/performanceMeasure.ts create mode 100644 apps/stress-test/src/shared/react/ReactTest.tsx create mode 100644 apps/stress-test/src/shared/react/TestAdd.tsx create mode 100644 apps/stress-test/src/shared/react/TestInjectStyles.tsx create mode 100644 apps/stress-test/src/shared/react/TestMount.tsx create mode 100644 apps/stress-test/src/shared/react/TestReRenderAll.tsx create mode 100644 apps/stress-test/src/shared/react/TestRemoveAll.tsx delete mode 100644 apps/stress-test/src/shared/testParams.ts create mode 100644 apps/stress-test/src/shared/tree/iterators/dfs.ts create mode 100644 apps/stress-test/src/shared/tree/types.ts create mode 100644 apps/stress-test/src/shared/utils/performanceMeasure.ts create mode 100644 apps/stress-test/src/shared/utils/requestPostAnimationFrame.ts create mode 100644 apps/stress-test/src/shared/vanilla/TestAdd.ts create mode 100644 apps/stress-test/src/shared/vanilla/TestInjectStyles.ts create mode 100644 apps/stress-test/src/shared/vanilla/TestMount.ts create mode 100644 apps/stress-test/src/shared/vanilla/TestReRenderAll.ts create mode 100644 apps/stress-test/src/shared/vanilla/TestRemoveAll.ts create mode 100644 apps/stress-test/src/shared/wc/TestAdd.ts create mode 100644 apps/stress-test/src/shared/wc/TestInjectStyles.ts create mode 100644 apps/stress-test/src/shared/wc/TestMount.ts create mode 100644 apps/stress-test/src/shared/wc/TestReRenderAll.ts create mode 100644 apps/stress-test/src/shared/wc/TestRemoveAll.ts create mode 100644 apps/stress-test/src/shared/wc/WCTest.ts create mode 100644 apps/stress-test/src/shared/wc/types.ts diff --git a/apps/stress-test/.gitignore b/apps/stress-test/.gitignore index 2b77931853383a..8756e1fe4b6642 100644 --- a/apps/stress-test/.gitignore +++ b/apps/stress-test/.gitignore @@ -1,4 +1,5 @@ node_modules/ +dev-build/ dist/ benchmarks/**/*.json diff --git a/apps/stress-test/scripts/utils/fixtures.ts b/apps/stress-test/scripts/utils/fixtures.ts index c8feeb1f0a12a0..897250b9f100a4 100644 --- a/apps/stress-test/scripts/utils/fixtures.ts +++ b/apps/stress-test/scripts/utils/fixtures.ts @@ -1,8 +1,9 @@ import fs from 'fs-extra'; import { join } from 'path'; import { getFixturesDir, remove } from './paths.js'; -import { RandomTree } from '../../src/shared/tree/RandomTree.js'; -import { RandomSelectorTreeNode, selectorTreeCreator } from '../../src/shared/tree/RandomSelectorTreeNode.js'; +import { RandomTree } from './tree/RandomTree.js'; +import { RandomSelectorTreeNode } from '../../src/shared/tree/types.js'; +import { selectorTreeCreator } from './tree/RandomSelectorTreeNode.js'; import glob from 'glob'; type BuildTreeFixture = (name: string, options: { [key: string]: string }) => void; diff --git a/apps/stress-test/src/shared/tree/RandomSelectorTreeNode.ts b/apps/stress-test/scripts/utils/tree/RandomSelectorTreeNode.ts similarity index 82% rename from apps/stress-test/src/shared/tree/RandomSelectorTreeNode.ts rename to apps/stress-test/scripts/utils/tree/RandomSelectorTreeNode.ts index 89affde79593a0..ceb7184b05ba1d 100644 --- a/apps/stress-test/src/shared/tree/RandomSelectorTreeNode.ts +++ b/apps/stress-test/scripts/utils/tree/RandomSelectorTreeNode.ts @@ -1,24 +1,12 @@ -import { RandomSelector } from '../css/RandomSelector.js'; -import { TreeNode, TreeNodeCreateCallback } from './RandomTree.js'; -import { random } from '../utils/random.js'; - -export type Attribute = { - key: string; - value: string | undefined; - selector: string; -}; +import { LCG } from 'random-seedable'; +import { RandomSelector } from '../../../src/shared/css/RandomSelector.js'; +import { Attribute, RandomSelectorTreeNode, TreeNode, TreeNodeCreateCallback } from '../../../src/shared/tree/types'; -export type RandomSelectorTreeNode = { - name: string; - classNames: string[]; - attributes: Attribute[]; - siblings: string[]; - pseudos: string[]; -}; +const rando: LCG = new LCG(4212021); -export type SelectorTreeNode = TreeNode; +const coin = (pTrue: number = 0.5): boolean => rando.coin(pTrue); +const choice = (choices: T[]): T => rando.choice(choices); -const { coin, choice } = random(); const randomSelector = new RandomSelector(); const chances: { [key: string]: number } = { @@ -70,7 +58,7 @@ const maybeNot = (selector: string): string => { }; const getAttributes = () => { - const attributes = []; + const attributes = [] as Attribute[]; if (coin(chances.addAttribute)) { const selector = randomSelector.randomSelector(['attribute-name', 'attribute-value']); const [key, value] = selector.replace(/(\[|\])/g, '').split('='); @@ -81,7 +69,7 @@ const getAttributes = () => { }; const getSiblingSelectors = () => { - const siblings = []; + const siblings = [] as string[]; if (coin(chances.addSibling)) { siblings.push(randomSelector.randomSelector(['nth-child'])); @@ -91,7 +79,7 @@ const getSiblingSelectors = () => { }; const getPseudoSelectors = () => { - const pseudo = []; + const pseudo = [] as string[]; if (coin(chances.addPsuedo)) { pseudo.push(randomSelector.randomSelector(['pseudo-element'])); diff --git a/apps/stress-test/src/shared/tree/RandomTree.ts b/apps/stress-test/scripts/utils/tree/RandomTree.ts similarity index 74% rename from apps/stress-test/src/shared/tree/RandomTree.ts rename to apps/stress-test/scripts/utils/tree/RandomTree.ts index 06d8705343f9b8..fbe74f66ae5801 100644 --- a/apps/stress-test/src/shared/tree/RandomTree.ts +++ b/apps/stress-test/scripts/utils/tree/RandomTree.ts @@ -1,5 +1,8 @@ -import { random, Random } from '../utils/random.js'; -import { TestFixture } from '../utils/testUtils.js'; +import { TreeNode, TreeNodeCreateCallback } from '../../../src/shared/tree/types'; +import { TestTreeFixture } from '../../../src/shared/utils/testUtils.js'; +import { LCG } from 'random-seedable'; + +const defaultSeed = 4212021; type TreeParams = { minDepth?: number; @@ -9,15 +12,6 @@ type TreeParams = { 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/ @@ -37,15 +31,15 @@ export class RandomTree { private minBreadth: number; private maxBreadth: number; - private rando: Random; + private rando: LCG; - constructor({ minDepth = 1, maxDepth = 15, minBreadth = 1, maxBreadth = 15, seed }: TreeParams = {}) { + constructor({ minDepth = 1, maxDepth = 15, minBreadth = 1, maxBreadth = 15, seed = defaultSeed }: TreeParams = {}) { this.minDepth = minDepth; this.maxDepth = maxDepth; this.minBreadth = minBreadth; this.maxBreadth = maxBreadth; - this.rando = random(seed); + this.rando = new LCG(seed); this.numNodes = 0; } @@ -55,7 +49,7 @@ export class RandomTree { return this._doBuild(createNode, root, 1); }; - public fromFixture = (fixture: TestFixture['tree'], parent: TreeNode | null = null): TreeNode => { + public fromFixture = (fixture: TestTreeFixture['tree'], parent: TreeNode | null = null): TreeNode => { const root: TreeNode = { value: fixture.value as T, children: [], @@ -70,11 +64,11 @@ export class RandomTree { }; private _randomDepth = (max?: number): number => { - return this.rando.range(this.minDepth, max ?? this.maxDepth); + return this.rando.randRange(this.minDepth, max ?? this.maxDepth); }; private _randomBreadth = (max?: number): number => { - return this.rando.range(this.minBreadth, max ?? this.maxBreadth); + return this.rando.randRange(this.minBreadth, max ?? this.maxBreadth); }; private _doBuild = ( diff --git a/apps/stress-test/src/components/v8/StressApp.tsx b/apps/stress-test/src/components/v8/StressApp.tsx index df6eadaf54f5a0..6e3da22b363d7b 100644 --- a/apps/stress-test/src/components/v8/StressApp.tsx +++ b/apps/stress-test/src/components/v8/StressApp.tsx @@ -1,19 +1,19 @@ import * as React from 'react'; -import { getTestParams } from '../../shared/testParams'; -import { performanceMeasure } from '../../shared/performanceMeasure'; +import { getTestOptions } from '../../shared/utils/testOptions'; +import { performanceMeasure } from '../../shared/utils/performanceMeasure'; import { StressContainer } from './StressContainer'; export const StressApp = () => { - const [numChildren, setNumChildren] = React.useState(getTestParams().numStartNodes); + const [numChildren, setNumChildren] = React.useState(Number(getTestOptions().numStartNodes)); React.useEffect(() => { - const { test, numStartNodes, numAddNodes, numRemoveNodes } = getTestParams(); + const { test, numStartNodes, numAddNodes, numRemoveNodes } = getTestOptions(); if (test === 'add-node') { performanceMeasure('stress', 'start'); - setNumChildren(numStartNodes + numAddNodes); + setNumChildren(Number(numStartNodes) + Number(numAddNodes)); } else if (test === 'removeNode') { performanceMeasure('stress', 'start'); - setNumChildren(numStartNodes - numRemoveNodes); + setNumChildren(Number(numStartNodes) - Number(numRemoveNodes)); } }, []); diff --git a/apps/stress-test/src/components/v8/StressContainer.tsx b/apps/stress-test/src/components/v8/StressContainer.tsx index 26ef7d3ae0c169..03d06fda61889f 100644 --- a/apps/stress-test/src/components/v8/StressContainer.tsx +++ b/apps/stress-test/src/components/v8/StressContainer.tsx @@ -1,8 +1,8 @@ import { mergeStyleSets } from '@fluentui/react'; import * as React from 'react'; -import { injectGlobalCss } from '../../shared/injectStyles'; -import { getTestParams } from '../../shared/testParams'; -import { performanceMeasure } from '../../shared/performanceMeasure'; +import { injectGlobalCss } from '../../shared/css/injectStyles'; +import { getTestOptions } from '../../shared/utils/testOptions'; +import { performanceMeasure } from '../../shared/utils/performanceMeasure'; import { StressComponent } from './StressComponent'; const styles = mergeStyleSets({ @@ -21,7 +21,7 @@ export const StressContainer: React.FC = ({ numChildren = const [checked, setChecked] = React.useState(false); React.useEffect(() => { - const { test } = getTestParams(); + const { test } = getTestOptions(); if (test === 'mount') { performanceMeasure('stress', 'start'); } else if (test === 'inject-styles') { diff --git a/apps/stress-test/src/components/v9/StressApp.tsx b/apps/stress-test/src/components/v9/StressApp.tsx index 89116b36a7c470..32e02e406e22f3 100644 --- a/apps/stress-test/src/components/v9/StressApp.tsx +++ b/apps/stress-test/src/components/v9/StressApp.tsx @@ -1,20 +1,20 @@ import * as React from 'react'; import { FluentProvider, webLightTheme } from '@fluentui/react-components'; -import { getTestParams } from '../../shared/testParams'; -import { performanceMeasure } from '../../shared/performanceMeasure'; +import { getTestOptions } from '../../shared/utils/testOptions'; +import { performanceMeasure } from '../../shared/utils/performanceMeasure'; import { StressContainer } from './StressContainer'; export const StressApp = () => { - const [numChildren, setNumChildren] = React.useState(getTestParams().numStartNodes); + const [numChildren, setNumChildren] = React.useState(Number(getTestOptions().numStartNodes)); React.useEffect(() => { - const { test, numStartNodes, numAddNodes, numRemoveNodes } = getTestParams(); + const { test, numStartNodes, numAddNodes, numRemoveNodes } = getTestOptions(); if (test === 'add-node') { performanceMeasure('stress', 'start'); - setNumChildren(numStartNodes + numAddNodes); + setNumChildren(Number(numStartNodes) + Number(numAddNodes)); } else if (test === 'removeNode') { performanceMeasure('stress', 'start'); - setNumChildren(numStartNodes - numRemoveNodes); + setNumChildren(Number(numStartNodes) - Number(numRemoveNodes)); } }, []); diff --git a/apps/stress-test/src/components/v9/StressContainer.tsx b/apps/stress-test/src/components/v9/StressContainer.tsx index 5d5ed9b956fa64..caeb16cd5b7444 100644 --- a/apps/stress-test/src/components/v9/StressContainer.tsx +++ b/apps/stress-test/src/components/v9/StressContainer.tsx @@ -1,8 +1,8 @@ import { makeStyles, shorthands } from '@fluentui/react-components'; import * as React from 'react'; -import { injectGlobalCss } from '../../shared/injectStyles'; -import { getTestParams } from '../../shared/testParams'; -import { performanceMeasure } from '../../shared/performanceMeasure'; +import { injectGlobalCss } from '../../shared/css/injectStyles'; +import { getTestOptions } from '../../shared/utils/testOptions'; +import { performanceMeasure } from '../../shared/utils/performanceMeasure'; import { StressComponent } from './StressComponent'; const useStyles = makeStyles({ @@ -21,7 +21,7 @@ export const StressContainer: React.FC = ({ numChildren = const [checked, setChecked] = React.useState(false); React.useEffect(() => { - const { test } = getTestParams(); + const { test } = getTestOptions(); if (test === 'mount') { performanceMeasure('stress', 'start'); } else if (test === 'inject-styles') { diff --git a/apps/stress-test/src/components/wc/stressApp.wc.ts b/apps/stress-test/src/components/wc/stressApp.wc.ts index 30132397f0de0e..c4c993d473ae72 100644 --- a/apps/stress-test/src/components/wc/stressApp.wc.ts +++ b/apps/stress-test/src/components/wc/stressApp.wc.ts @@ -1,6 +1,6 @@ import { FASTElement, customElement, attr, html, css, repeat, ValueConverter } from '@microsoft/fast-element'; -import { getTestParams } from '../../shared/testParams'; -import { performanceMeasure } from '../../shared/performanceMeasure'; +import { getTestOptions } from '../../shared/utils/testOptions'; +import { performanceMeasure } from '../../shared/utils/performanceMeasure'; import { StressComponent } from './stressComponent.wc'; const styles = css` @@ -43,7 +43,7 @@ export class StressApp extends FASTElement { public connectedCallback(): void { super.connectedCallback(); - const { test, numStartNodes, numAddNodes, numRemoveNodes } = getTestParams(); + const { test, numStartNodes, numAddNodes, numRemoveNodes } = getTestOptions(); if (test === 'prop-update') { setTimeout(() => { @@ -53,12 +53,12 @@ export class StressApp extends FASTElement { } else if (test === 'add-node') { setTimeout(() => { performanceMeasure('stress', 'start'); - this.numchildren = numStartNodes + numAddNodes; + this.numchildren = Number(numStartNodes) + Number(numAddNodes); }, 2000); } else if (test === 'remove-node') { setTimeout(() => { performanceMeasure('stress', 'start'); - this.numchildren = numStartNodes - numRemoveNodes; + this.numchildren = Number(numStartNodes) - Number(numRemoveNodes); }, 2000); } } diff --git a/apps/stress-test/src/components/wc/stressContainer.wc.ts b/apps/stress-test/src/components/wc/stressContainer.wc.ts index 26a3df9100a5d2..49507d8294c140 100644 --- a/apps/stress-test/src/components/wc/stressContainer.wc.ts +++ b/apps/stress-test/src/components/wc/stressContainer.wc.ts @@ -1,7 +1,7 @@ import { FASTElement, customElement, html, css } from '@microsoft/fast-element'; -import { injectGlobalCss } from '../../shared/injectStyles'; -import { getTestParams } from '../../shared/testParams'; -import { performanceMeasure } from '../../shared/performanceMeasure'; +import { injectGlobalCss } from '../../shared/css/injectStyles'; +import { getTestOptions } from '../../shared/utils/testOptions'; +import { performanceMeasure } from '../../shared/utils/performanceMeasure'; const styles = css` :host { @@ -26,7 +26,7 @@ export class StressContainer extends FASTElement { public connectedCallback(): void { super.connectedCallback(); - const { test } = getTestParams(); + const { test } = getTestOptions(); if (test === 'mount') { performance.mark('start'); diff --git a/apps/stress-test/src/pages/v8/stress-tree/index.html b/apps/stress-test/src/pages/v8/stress-tree/index.html new file mode 100644 index 00000000000000..9ff4031b962193 --- /dev/null +++ b/apps/stress-test/src/pages/v8/stress-tree/index.html @@ -0,0 +1,11 @@ + + + + + + v8: Stress Tree + + +
+ + diff --git a/apps/stress-test/src/pages/v8/stress-tree/index.tsx b/apps/stress-test/src/pages/v8/stress-tree/index.tsx new file mode 100644 index 00000000000000..af8fb1d5059743 --- /dev/null +++ b/apps/stress-test/src/pages/v8/stress-tree/index.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { initializeIcons, ThemeProvider } from '@fluentui/react'; +import { getTestOptions } from '../../../shared/utils/testOptions'; +import { ReactTest } from '../../../shared/react/ReactTest'; + +initializeIcons(); + +const { fixtureName, rendererName, r } = getTestOptions(); + +ReactDOM.render( + + + , + document.getElementById('root'), +); diff --git a/apps/stress-test/src/pages/v9/stress-tree/index.html b/apps/stress-test/src/pages/v9/stress-tree/index.html new file mode 100644 index 00000000000000..5d99e8898db678 --- /dev/null +++ b/apps/stress-test/src/pages/v9/stress-tree/index.html @@ -0,0 +1,11 @@ + + + + + + v9: Stress Tree + + +
+ + diff --git a/apps/stress-test/src/pages/v9/stress-tree/index.tsx b/apps/stress-test/src/pages/v9/stress-tree/index.tsx new file mode 100644 index 00000000000000..c8b69fdc3733d0 --- /dev/null +++ b/apps/stress-test/src/pages/v9/stress-tree/index.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { FluentProvider, webLightTheme } from '@fluentui/react-components'; +import { ReactTest } from '../../../shared/react/ReactTest'; +import { getTestOptions } from '../../../shared/utils/testOptions'; + +const { fixtureName, rendererName, r } = getTestOptions(); +ReactDOM.render( + + + , + document.getElementById('root'), +); diff --git a/apps/stress-test/src/pages/wc/simple-stress/index.wc.ts b/apps/stress-test/src/pages/wc/simple-stress/index.wc.ts index 951bb24195e7ae..3f28e7f78fb9cf 100644 --- a/apps/stress-test/src/pages/wc/simple-stress/index.wc.ts +++ b/apps/stress-test/src/pages/wc/simple-stress/index.wc.ts @@ -9,11 +9,11 @@ import { import { StressApp } from '../../../components/wc/stressApp.wc'; import { StressComponent } from '../../../components/wc/stressComponent.wc'; import { StressContainer } from '../../../components/wc/stressContainer.wc'; -import { getTestParams } from '../../../shared/testParams'; +import { getTestOptions } from '../../../shared/utils/testOptions'; -const testParams = getTestParams(); +const testOptions = getTestOptions(); -document.querySelector('stress-app')?.setAttribute('numchildren', testParams.numStartNodes.toString()); +document.querySelector('stress-app')?.setAttribute('numchildren', testOptions.numStartNodes.toString()); provideFluentDesignSystem().register( fluentButton(), diff --git a/apps/stress-test/src/pages/wc/stress-tree/index.html b/apps/stress-test/src/pages/wc/stress-tree/index.html new file mode 100644 index 00000000000000..c20b268b0cb78d --- /dev/null +++ b/apps/stress-test/src/pages/wc/stress-tree/index.html @@ -0,0 +1,11 @@ + + + + + + WC: Stress Tree + + +
+ + diff --git a/apps/stress-test/src/pages/wc/stress-tree/index.wc.ts b/apps/stress-test/src/pages/wc/stress-tree/index.wc.ts new file mode 100644 index 00000000000000..c5a4e6778605be --- /dev/null +++ b/apps/stress-test/src/pages/wc/stress-tree/index.wc.ts @@ -0,0 +1,5 @@ +import { wcTest } from '../../../shared/wc/WCTest'; + +wcTest().then(testNode => { + document.querySelector('#app')?.appendChild(testNode!); +}); diff --git a/apps/stress-test/src/renderers/v8/button.tsx b/apps/stress-test/src/renderers/v8/button.tsx new file mode 100644 index 00000000000000..74ca75be0686dd --- /dev/null +++ b/apps/stress-test/src/renderers/v8/button.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { DefaultButton } from '@fluentui/react'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v8/spinButton.tsx b/apps/stress-test/src/renderers/v8/spinButton.tsx new file mode 100644 index 00000000000000..1f0db14c83fb58 --- /dev/null +++ b/apps/stress-test/src/renderers/v8/spinButton.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { SpinButton } from '@fluentui/react'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v8/stress.tsx b/apps/stress-test/src/renderers/v8/stress.tsx new file mode 100644 index 00000000000000..4bbd00d65c589d --- /dev/null +++ b/apps/stress-test/src/renderers/v8/stress.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { + mergeStyleSets, + DefaultButton, + Separator, + Checkbox, + SpinButton, + Spinner, + DefaultPalette, +} from '@fluentui/react'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const styles = mergeStyleSets({ + stressComponent: { + display: 'flex', + flexDirection: 'column', + rowGap: '10px', + maxWidth: '300px', + border: `1px solid ${DefaultPalette.neutralLight}`, + borderRadius: '4px', + padding: '10px', + }, +}); + +export type StressComponentProps = { + id?: string; + checked: boolean; +}; + +const StressComponent: React.FC = ({ id = '', checked }) => { + return ( +
+ A button + + + + + + + +
+ ); +}; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/btnOverride/idiomatic.tsx b/apps/stress-test/src/renderers/v9/btnOverride/idiomatic.tsx new file mode 100644 index 00000000000000..8d5e66f36fc800 --- /dev/null +++ b/apps/stress-test/src/renderers/v9/btnOverride/idiomatic.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { + useButton_unstable, + renderButton_unstable, + ForwardRefComponent, + ButtonProps, + ButtonState, + mergeClasses, + buttonClassNames, +} from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../../shared/react/types'; +import { useRootStyles, useRootDisabledStyles } from './styles'; + +const useButtonIdiomaticOverrideStyles = (state: ButtonState): ButtonState => { + const rootStyles = useRootStyles(); + const rootDisabledStyles = useRootDisabledStyles(); + + const { disabled, disabledFocusable } = state; + + state.root.className = mergeClasses( + buttonClassNames.root, + rootStyles.base, + rootStyles.highContrast, + (disabled || disabledFocusable) && rootDisabledStyles.base, + (disabled || disabledFocusable) && rootDisabledStyles.highContrast, + state.root.className, + ); + + return state; +}; + +const ButtonIdiomaticOverride: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useButton_unstable(props, ref); + + useButtonIdiomaticOverrideStyles(state); + + return renderButton_unstable(state); +}) as ForwardRefComponent; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return {`${node.value.name}, ${index}`}; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/btnOverride/override.tsx b/apps/stress-test/src/renderers/v9/btnOverride/override.tsx new file mode 100644 index 00000000000000..1e4daeeed1d3ee --- /dev/null +++ b/apps/stress-test/src/renderers/v9/btnOverride/override.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { Button, ButtonProps, makeStyles, mergeClasses } from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../../shared/react/types'; +import { useRootStyles, useRootDisabledStyles } from './styles'; + +const useButtonOverridesStyles = makeStyles({ + base: { + fontWeight: 'initial', + minWidth: 'initial', + fontSize: 'unset', + lineHeight: 'normal', + }, +}); + +const ButtonOverride: React.FC = props => { + const rootStyles = useRootStyles(); + const rootDisabledStyles = useRootDisabledStyles(); + const buttonOverrideStyles = useButtonOverridesStyles(); + + const { className, disabled, disabledFocusable } = props; + const applyDisabledStyles = disabled || disabledFocusable; + + return ( + ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/spinButton.tsx b/apps/stress-test/src/renderers/v9/spinButton.tsx new file mode 100644 index 00000000000000..c32fa1ac1785b7 --- /dev/null +++ b/apps/stress-test/src/renderers/v9/spinButton.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { SpinButton } from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/stress.tsx b/apps/stress-test/src/renderers/v9/stress.tsx new file mode 100644 index 00000000000000..3915d1cee00489 --- /dev/null +++ b/apps/stress-test/src/renderers/v9/stress.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { + makeStyles, + tokens, + shorthands, + Button, + Divider, + Checkbox, + Label, + SpinButton, + Spinner, +} from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const useStyles = makeStyles({ + stressComponent: { + display: 'flex', + flexDirection: 'column', + rowGap: '10px', + maxWidth: '300px', + ...shorthands.border('1px', 'solid', tokens.colorNeutralStroke1), + ...shorthands.borderRadius(tokens.borderRadiusMedium), + ...shorthands.padding('10px'), + }, +}); + +export type StressComponentProps = { + id?: string; + checked: boolean; +}; + +const StressComponent: React.FC = ({ id = '', checked }) => { + const styles = useStyles(); + + return ( +
+ + + + + + + + + +
+ ); +}; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/wc/button.ts b/apps/stress-test/src/renderers/wc/button.ts new file mode 100644 index 00000000000000..c9bbc64f44cb1a --- /dev/null +++ b/apps/stress-test/src/renderers/wc/button.ts @@ -0,0 +1,13 @@ +import { fluentButton, provideFluentDesignSystem } from '@fluentui/web-components'; +import { DOMSelectorTreeComponentRenderer } from '../../shared/vanilla/types'; + +provideFluentDesignSystem().register(fluentButton()); + +const componentRenderer: DOMSelectorTreeComponentRenderer = (node, depth, index) => { + const btn = document.createElement('fluent-button'); + btn.innerHTML = node.value.name + ' ' + index; + + return btn; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/wc/numberField.ts b/apps/stress-test/src/renderers/wc/numberField.ts new file mode 100644 index 00000000000000..cf42f26495813b --- /dev/null +++ b/apps/stress-test/src/renderers/wc/numberField.ts @@ -0,0 +1,13 @@ +import { fluentNumberField, NumberField, provideFluentDesignSystem } from '@fluentui/web-components'; +import { DOMSelectorTreeComponentRenderer } from '../../shared/vanilla/types'; + +provideFluentDesignSystem().register(fluentNumberField()); + +const componentRenderer: DOMSelectorTreeComponentRenderer = (node, depth, index) => { + const numberField = document.createElement('fluent-number-field') as NumberField; + numberField.value = node.value.name + ' ' + index; + + return numberField; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/wc/stress.ts b/apps/stress-test/src/renderers/wc/stress.ts new file mode 100644 index 00000000000000..55ded6c6c696a2 --- /dev/null +++ b/apps/stress-test/src/renderers/wc/stress.ts @@ -0,0 +1,11 @@ +import { StressComponent } from '../../components/wc/stressComponent.wc'; +import { DOMSelectorTreeComponentRenderer } from '../../shared/vanilla/types'; + +typeof StressComponent; + +const componentRenderer: DOMSelectorTreeComponentRenderer = (node, depth, index) => { + const stressComp = document.createElement('stress-component'); + return stressComp; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/shared/Element.tsx b/apps/stress-test/src/shared/Element.tsx deleted file mode 100644 index 3e308d066dbc11..00000000000000 --- a/apps/stress-test/src/shared/Element.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from 'react'; -import { useElementContext } from './ElementContext'; - -const alphabet = 'abcdefghijklmnopqrstuvwxyz'.split(''); -const alphabetLength = alphabet.length; - -export type ElementProps = { - as?: React.ReactNode; - depth?: number; - start?: number; - classPrefix?: string; - className?: string; - children?: React.ReactNode; -}; - -export const Element: React.FC = props => { - const { depth: contextDepth } = useElementContext(); - const { as = 'div', children, depth = contextDepth, start = 0, classPrefix = '', className, ...rest } = props; - - const El = as as React.ElementType; - let depthClassName = alphabet[start % alphabetLength]; - depthClassName = classPrefix ? `${classPrefix}-${depthClassName}` : depthClassName; - - const cn = className ? `${className} ${depthClassName}` : depthClassName; - - return depth > 1 ? ( - - - - ) : ( - - {children} - - ); -}; diff --git a/apps/stress-test/src/shared/ElementContext.tsx b/apps/stress-test/src/shared/ElementContext.tsx deleted file mode 100644 index 6f3caa4d6b5a53..00000000000000 --- a/apps/stress-test/src/shared/ElementContext.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; - -export type ElementContextObject = { - depth: number; -}; - -const ElementContext = React.createContext({ - depth: 26, -}); - -export const useElementContext = () => React.useContext(ElementContext); - -const ElementProvider = ElementContext.Provider; -export { ElementProvider }; diff --git a/apps/stress-test/src/shared/css/injectStyles.ts b/apps/stress-test/src/shared/css/injectStyles.ts new file mode 100644 index 00000000000000..211e4aa61b80cc --- /dev/null +++ b/apps/stress-test/src/shared/css/injectStyles.ts @@ -0,0 +1,211 @@ +// Inspired by: https://github.com/nolanlawson/shadow-selector-benchmark +import { random } from '../utils/random'; + +const colors = [ + 'aliceblue', + 'antiquewhite', + 'aqua', + 'aquamarine', + 'azure', + 'beige', + 'bisque', + 'black', + 'blanchedalmond', + 'blue', + 'blueviolet', + 'brown', + 'burlywood', + 'cadetblue', + 'chartreuse', + 'chocolate', + 'coral', + 'cornflowerblue', + 'cornsilk', + 'crimson', + 'cyan', + 'darkblue', + 'darkcyan', + 'darkgoldenrod', + 'darkgray', + 'darkgreen', + 'darkgrey', + 'darkkhaki', + 'darkmagenta', + 'darkolivegreen', + 'darkorange', + 'darkorchid', + 'darkred', + 'darksalmon', + 'darkseagreen', + 'darkslateblue', + 'darkslategray', + 'darkslategrey', + 'darkturquoise', + 'darkviolet', + 'deeppink', + 'deepskyblue', + 'dimgray', + 'dimgrey', + 'dodgerblue', + 'firebrick', + 'floralwhite', + 'forestgreen', + 'fuchsia', + 'gainsboro', + 'ghostwhite', + 'goldenrod', + 'gold', + 'gray', + 'green', + 'greenyellow', + 'grey', + 'honeydew', + 'hotpink', + 'indianred', + 'indigo', + 'ivory', + 'khaki', + 'lavenderblush', + 'lavender', + 'lawngreen', + 'lemonchiffon', + 'lightblue', + 'lightcoral', + 'lightcyan', + 'lightgoldenrodyellow', + 'lightgray', + 'lightgreen', + 'lightgrey', + 'lightpink', + 'lightsalmon', + 'lightseagreen', + 'lightskyblue', + 'lightslategray', + 'lightslategrey', + 'lightsteelblue', + 'lightyellow', + 'lime', + 'limegreen', + 'linen', + 'magenta', + 'maroon', + 'mediumaquamarine', + 'mediumblue', + 'mediumorchid', + 'mediumpurple', + 'mediumseagreen', + 'mediumslateblue', + 'mediumspringgreen', + 'mediumturquoise', + 'mediumvioletred', + 'midnightblue', + 'mintcream', + 'mistyrose', + 'moccasin', + 'navajowhite', + 'navy', + 'oldlace', + 'olive', + 'olivedrab', + 'orange', + 'orangered', + 'orchid', + 'palegoldenrod', + 'palegreen', + 'paleturquoise', + 'palevioletred', + 'papayawhip', + 'peachpuff', + 'peru', + 'pink', + 'plum', + 'powderblue', + 'purple', + 'rebeccapurple', + 'red', + 'rosybrown', + 'royalblue', + 'saddlebrown', + 'salmon', + 'sandybrown', + 'seagreen', + 'seashell', + 'sienna', + 'silver', + 'skyblue', + 'slateblue', + 'slategray', + 'slategrey', + 'snow', + 'springgreen', + 'steelblue', + 'tan', + 'teal', + 'thistle', + 'tomato', + 'turquoise', + 'violet', + 'wheat', + 'white', + 'whitesmoke', + 'yellow', + 'yellowgreen', +]; + +export const randomCssFromSelectors = (selectors: string[]): string => { + const { choice } = random(); + + let css = ''; + + selectors.forEach(selector => { + css += `${selector} { background-color: ${choice(colors)}; }\n`; + }); + + return css; +}; + +function createStyleTag(css: string) { + const style = document.createElement('style'); + style.textContent = css; + return style; +} + +export function injectGlobalCss(css?: string) { + performance.mark('fluent-inject-global-css-start'); + css = + css ?? + ` + div { + background-color: yellow; + } + + button { + background-color: hotpink; + } + + hr { + background-color: red; + } + + stress-app { + background-color: yellow; + } + + stress-container { + background-color: green; + } + + stress-component { + background-color: aliceblue; + } + `; + + const style = createStyleTag(css); + document.head.appendChild(style); + performance.measure('fluent-inject-global-css', 'fluent-inject-global-css-start'); + return style; +} + +export const styleInjector = (selectors: string[]): HTMLStyleElement => { + return injectGlobalCss(randomCssFromSelectors(selectors)); +}; diff --git a/apps/stress-test/src/shared/injectStyles.ts b/apps/stress-test/src/shared/injectStyles.ts deleted file mode 100644 index ab918be96ba2b3..00000000000000 --- a/apps/stress-test/src/shared/injectStyles.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Inspired by: https://github.com/nolanlawson/shadow-selector-benchmark -function createStyleTag(css: string) { - const style = document.createElement('style'); - style.textContent = css; - return style; -} - -export function injectGlobalCss(css?: string) { - performance.mark('fluent-inject-global-css-start'); - css = ` - div { - background-color: yellow; - } - - button { - background-color: hotpink; - } - - hr { - background-color: red; - } - - stress-app { - background-color: yellow; - } - - stress-container { - background-color: green; - } - - stress-component { - background-color: aliceblue; - } - `; - - document.head.appendChild(createStyleTag(css)); - performance.measure('fluent-inject-global-css', 'fluent-inject-global-css-start'); -} diff --git a/apps/stress-test/src/shared/performanceMeasure.ts b/apps/stress-test/src/shared/performanceMeasure.ts deleted file mode 100644 index 5c7ce7021d31e5..00000000000000 --- a/apps/stress-test/src/shared/performanceMeasure.ts +++ /dev/null @@ -1,17 +0,0 @@ -export type PerformanceMeasureFn = (measureName: string, startMark: string) => void; - -export const performanceMeasure: PerformanceMeasureFn = (measureName, startMark) => { - performance.mark(startMark); - - // requestPostAnimationFrame polyfill - requestAnimationFrame(() => { - addEventListener( - 'message', - () => { - performance.measure(measureName, startMark); - }, - { once: true }, - ); - postMessage('', '*'); - }); -}; diff --git a/apps/stress-test/src/shared/react/ReactSelectorTree.tsx b/apps/stress-test/src/shared/react/ReactSelectorTree.tsx index 7dd0df4d8bb2ca..b162180740c424 100644 --- a/apps/stress-test/src/shared/react/ReactSelectorTree.tsx +++ b/apps/stress-test/src/shared/react/ReactSelectorTree.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import { TreeNode } from '../tree/RandomTree'; +import { TreeNode } from '../tree/types'; import { ReactTree } from './ReactTree'; -import { RandomSelectorTreeNode, SelectorTreeNode } from '../tree/RandomSelectorTreeNode'; +import { RandomSelectorTreeNode, SelectorTreeNode } from '../tree/types'; import { ReactSelectorTreeComponentRenderer } from './types'; type ReactSelectorTreeProps = { diff --git a/apps/stress-test/src/shared/react/ReactTest.tsx b/apps/stress-test/src/shared/react/ReactTest.tsx new file mode 100644 index 00000000000000..e06b0e3f5c48aa --- /dev/null +++ b/apps/stress-test/src/shared/react/ReactTest.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import { getTestOptions, TestOptions } from '../utils/testOptions'; +import { loadTestData, TestReactRenderer, TestTreeFixture } from '../utils/testUtils'; +import { RandomSelectorTreeNode, TreeNode } from '../tree/types'; +import { RandomTree } from '../../../scripts/utils/tree/RandomTree'; +import { TestInjectStyles } from './TestInjectStyles'; +import { TestMount } from './TestMount'; +import { TestRemoveAll } from './TestRemoveAll'; +import { TestReRenderAll } from './TestReRenderAll'; +import { TestAdd } from './TestAdd'; + +type TestData = { + tree?: TreeNode; + selectors?: string[]; + testOptions: TestOptions; + renderer?: TestReactRenderer; +}; + +type ReactTestProps = { + target: string; + fixtureName: string; + rendererName: string; +}; + +export const ReactTest: React.FC = ({ target, fixtureName, rendererName }) => { + const [testData, setTestData] = React.useState({ + testOptions: getTestOptions(), + }); + + React.useEffect(() => { + loadTestData(target, fixtureName, rendererName).then( + ({ fixture, renderer }) => { + const selectors = fixture.selectors; + const fixtureTree = fixture.tree; + + const treeBuilder = new RandomTree(); + const tree = treeBuilder.fromFixture(fixtureTree); + + setTestData({ + ...testData, + tree, + selectors, + renderer, + }); + }, + ); + }, []); + + const { + testOptions: { test }, + tree, + selectors, + renderer, + } = testData; + + let Test; + switch (test) { + case 'inject-styles': + Test = TestInjectStyles; + break; + + case 'mount': + Test = TestMount; + break; + + case 're-render-all': + Test = TestReRenderAll; + break; + + case 'remove-all': + Test = TestRemoveAll; + break; + + case 'add': + Test = TestAdd; + break; + + default: + Test = TestMount; + break; + } + + return ( + <> + {selectors && tree && ( + + )} + + ); +}; diff --git a/apps/stress-test/src/shared/react/ReactTree.tsx b/apps/stress-test/src/shared/react/ReactTree.tsx index 4324d5ac94b149..dfdc1a762688a7 100644 --- a/apps/stress-test/src/shared/react/ReactTree.tsx +++ b/apps/stress-test/src/shared/react/ReactTree.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { TreeNode } from '../tree/RandomTree'; +import { TreeNode } from '../tree/types'; export type ReactTreeItemRenderer = (node: T, depth: number, index: number) => JSX.Element; diff --git a/apps/stress-test/src/shared/react/TestAdd.tsx b/apps/stress-test/src/shared/react/TestAdd.tsx new file mode 100644 index 00000000000000..1996cc5ee11935 --- /dev/null +++ b/apps/stress-test/src/shared/react/TestAdd.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { styleInjector } from '../css/injectStyles'; +import { performanceMeasure } from '../utils/performanceMeasure'; +import { ReactSelectorTree } from './ReactSelectorTree'; +import type { TestProps } from './types'; + +export const TestAdd: React.FC = ({ tree, selectors, componentRenderer, testOptions }) => { + const [added, setAdded] = React.useState(false); + + React.useEffect(() => { + if (testOptions.withStyles === 'true') { + styleInjector(selectors); + } + }, []); + + React.useEffect(() => { + setTimeout(() => { + setAdded(true); + performanceMeasure(); + }, 2000); + }, []); + + return ( + <> + + {added && } + + ); +}; diff --git a/apps/stress-test/src/shared/react/TestInjectStyles.tsx b/apps/stress-test/src/shared/react/TestInjectStyles.tsx new file mode 100644 index 00000000000000..c57af6dbe38f78 --- /dev/null +++ b/apps/stress-test/src/shared/react/TestInjectStyles.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { styleInjector } from '../css/injectStyles'; +import { performanceMeasure } from '../utils/performanceMeasure'; +import { ReactSelectorTree } from './ReactSelectorTree'; +import type { TestProps } from './types'; + +export const TestInjectStyles: React.FC = ({ tree, selectors, componentRenderer }) => { + React.useEffect(() => { + setTimeout(() => { + styleInjector(selectors); + performanceMeasure(); + }, 2000); + }, []); + + return ; +}; diff --git a/apps/stress-test/src/shared/react/TestMount.tsx b/apps/stress-test/src/shared/react/TestMount.tsx new file mode 100644 index 00000000000000..86f1f976d0cdce --- /dev/null +++ b/apps/stress-test/src/shared/react/TestMount.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { styleInjector } from '../css/injectStyles'; +import { performanceMeasure } from '../utils/performanceMeasure'; +import { ReactSelectorTree } from './ReactSelectorTree'; +import type { TestProps } from './types'; + +export const TestMount: React.FC = ({ tree, selectors, componentRenderer, testOptions }) => { + // eslint-disable-next-line no-restricted-properties + React.useLayoutEffect(() => { + if (testOptions.withStyles === 'true') { + styleInjector(selectors); + } + performanceMeasure(); + }, []); + + return ; +}; diff --git a/apps/stress-test/src/shared/react/TestReRenderAll.tsx b/apps/stress-test/src/shared/react/TestReRenderAll.tsx new file mode 100644 index 00000000000000..2c2ab45af6d411 --- /dev/null +++ b/apps/stress-test/src/shared/react/TestReRenderAll.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { styleInjector } from '../css/injectStyles'; +import { performanceMeasure } from '../utils/performanceMeasure'; +import { ReactSelectorTree } from './ReactSelectorTree'; +import type { TestProps } from './types'; + +export const TestReRenderAll: React.FC = ({ tree, selectors, componentRenderer, testOptions }) => { + const [theTree, setTheTree] = React.useState(); + + React.useEffect(() => { + if (testOptions.withStyles === 'true') { + styleInjector(selectors); + } + }, []); + + React.useEffect(() => { + if (theTree === undefined && tree) { + setTheTree(tree); + + setTimeout(() => { + setTheTree(undefined); + + setTimeout(() => { + setTheTree(tree); + performanceMeasure(); + }, 2000); + }, 2000); + } + }, [tree]); + + return ; +}; diff --git a/apps/stress-test/src/shared/react/TestRemoveAll.tsx b/apps/stress-test/src/shared/react/TestRemoveAll.tsx new file mode 100644 index 00000000000000..cdd4771ca58d2d --- /dev/null +++ b/apps/stress-test/src/shared/react/TestRemoveAll.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { styleInjector } from '../css/injectStyles'; +import { performanceMeasure } from '../utils/performanceMeasure'; +import { ReactSelectorTree } from './ReactSelectorTree'; +import type { TestProps } from './types'; + +export const TestRemoveAll: React.FC = ({ tree, selectors, componentRenderer, testOptions }) => { + const [theTree, setTheTree] = React.useState(); + + React.useEffect(() => { + if (testOptions.withStyles === 'true') { + styleInjector(selectors); + } + }, []); + + React.useEffect(() => { + if (theTree === undefined && tree) { + setTheTree(tree); + + setTimeout(() => { + setTheTree(undefined); + performanceMeasure(); + }, 2000); + } + }, [tree]); + + return ; +}; diff --git a/apps/stress-test/src/shared/react/types.ts b/apps/stress-test/src/shared/react/types.ts index 96eab1e699dc30..2725dda0548cc0 100644 --- a/apps/stress-test/src/shared/react/types.ts +++ b/apps/stress-test/src/shared/react/types.ts @@ -1,6 +1,5 @@ +import { RandomSelectorTreeNode, SelectorTreeNode, TreeNode } from '../tree/types'; 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; diff --git a/apps/stress-test/src/shared/testParams.ts b/apps/stress-test/src/shared/testParams.ts deleted file mode 100644 index 5effe504fe6f27..00000000000000 --- a/apps/stress-test/src/shared/testParams.ts +++ /dev/null @@ -1,51 +0,0 @@ -const testTypes = ['manual', 'mount', 'inject-styles', 'prop-update', 'remove-node', 'add-node']; - -export type TestParams = { - test: typeof testTypes[number]; - numStartNodes: number; - numAddNodes: number; - numRemoveNodes: number; -}; - -export type GetTestParamsFn = () => TestParams; - -let params: TestParams; -export const getTestParams = () => { - if (params) { - return params; - } - params = {} as TestParams; - - if (typeof window === 'undefined') { - return params; - } - - const searchParams = new URLSearchParams(window.location.search); - - let test = searchParams.get('test'); - if (!test || !testTypes.includes(test as TestParams['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 TestParams['test']; - params.numStartNodes = numStartNodes; - params.numAddNodes = numAddNodes; - params.numRemoveNodes = numRemoveNodes; - - return params; -}; diff --git a/apps/stress-test/src/shared/tree/iterators/dfs.ts b/apps/stress-test/src/shared/tree/iterators/dfs.ts new file mode 100644 index 00000000000000..b6f61f88a93e44 --- /dev/null +++ b/apps/stress-test/src/shared/tree/iterators/dfs.ts @@ -0,0 +1,28 @@ +import { TreeNode } from '../types'; + +type TreeResult = { + node: T; + index: number; +}; + +export const dfsIterator = (tree: TreeNode): Iterable> => { + let index = 0; + return { + *[Symbol.iterator]() { + const stack = [tree]; + while (stack.length) { + const item = stack.pop()!; + if (item.children.length) { + let i = item.children.length - 1; + while (i > -1) { + stack.push(item.children[i]); + i--; + } + } + + yield { node: item.value, index }; + index++; + } + }, + }; +}; diff --git a/apps/stress-test/src/shared/tree/types.ts b/apps/stress-test/src/shared/tree/types.ts new file mode 100644 index 00000000000000..10cc6114437bf7 --- /dev/null +++ b/apps/stress-test/src/shared/tree/types.ts @@ -0,0 +1,24 @@ +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; + +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; diff --git a/apps/stress-test/src/shared/utils/performanceMeasure.ts b/apps/stress-test/src/shared/utils/performanceMeasure.ts new file mode 100644 index 00000000000000..43d7af07a3fff8 --- /dev/null +++ b/apps/stress-test/src/shared/utils/performanceMeasure.ts @@ -0,0 +1,11 @@ +import { requestPostAnimationFrame } from './requestPostAnimationFrame'; + +export type PerformanceMeasureFn = (measureName?: string, startMark?: string) => void; + +export const performanceMeasure: PerformanceMeasureFn = (measureName = 'stress', startMark = 'start') => { + performance.mark(startMark); + + requestPostAnimationFrame(() => { + performance.measure(measureName, startMark); + }); +}; diff --git a/apps/stress-test/src/shared/utils/requestPostAnimationFrame.ts b/apps/stress-test/src/shared/utils/requestPostAnimationFrame.ts new file mode 100644 index 00000000000000..1380db348d6a4f --- /dev/null +++ b/apps/stress-test/src/shared/utils/requestPostAnimationFrame.ts @@ -0,0 +1,6 @@ +export const requestPostAnimationFrame = (callback: Function): void => { + requestAnimationFrame(() => { + addEventListener('message', _ => callback(), { once: true }); + postMessage('', '*'); + }); +}; diff --git a/apps/stress-test/src/shared/utils/testUtils.ts b/apps/stress-test/src/shared/utils/testUtils.ts index 83b5a13d36b97c..e1de92452ff3ad 100644 --- a/apps/stress-test/src/shared/utils/testUtils.ts +++ b/apps/stress-test/src/shared/utils/testUtils.ts @@ -1,9 +1,31 @@ -export type TestFixture = { +export type TestTreeFixture = { tree: { value: unknown; - children: TestFixture['tree'][]; + children: TestTreeFixture['tree'][]; }; selectors: string[]; }; -export type TestRenderer = (node: unknown, depth: number, index: number) => unknown; +export type TestReactRenderer = (node: unknown, depth: number, index: number) => JSX.Element; +export type TestDOMRenderer = (node: unknown, depth: number, index: number) => HTMLElement; + +export type LoaderResult = { + fixture: TFixture; + renderer: TRenderer; +}; + +export const loadTestData = ( + target: string, + fixtureName: string, + rendererName: string, +): Promise> => { + return Promise.all([ + import(`../../fixtures/${fixtureName}`), + import(`../../renderers/${target}/${rendererName}`), + ]).then(([fixtureResult, rendererResult]) => { + const { default: fixture } = fixtureResult; + const { default: renderer } = rendererResult; + + return { fixture, renderer }; + }); +}; diff --git a/apps/stress-test/src/shared/vanilla/TestAdd.ts b/apps/stress-test/src/shared/vanilla/TestAdd.ts new file mode 100644 index 00000000000000..59506a6348961c --- /dev/null +++ b/apps/stress-test/src/shared/vanilla/TestAdd.ts @@ -0,0 +1,27 @@ +import { styleInjector } from '../css/injectStyles'; +import { performanceMeasure } from '../utils/performanceMeasure'; +import { TestOptions } from '../utils/testOptions'; +import { SelectorTreeNode } from '../tree/types'; +import { DOMSelectorTreeComponentRenderer } from './types'; +import { renderVanillaSelectorTree } from './VanillaSelectorTree'; + +export const testAdd = ( + tree: SelectorTreeNode, + selectors: string[], + componentRenderer: DOMSelectorTreeComponentRenderer, + testOptions: TestOptions, +): HTMLElement => { + if (testOptions.withStyles === 'true') { + styleInjector(selectors); + } + + const wrapper = document.createElement('div'); + wrapper.appendChild(renderVanillaSelectorTree(tree, selectors, componentRenderer, testOptions)); + + setTimeout(() => { + wrapper.appendChild(renderVanillaSelectorTree(tree, selectors, componentRenderer, testOptions)); + performanceMeasure(); + }, 2000); + + return wrapper; +}; diff --git a/apps/stress-test/src/shared/vanilla/TestInjectStyles.ts b/apps/stress-test/src/shared/vanilla/TestInjectStyles.ts new file mode 100644 index 00000000000000..537eb2885ab8b9 --- /dev/null +++ b/apps/stress-test/src/shared/vanilla/TestInjectStyles.ts @@ -0,0 +1,22 @@ +import { styleInjector } from '../css/injectStyles'; +import { performanceMeasure } from '../utils/performanceMeasure'; +import { TestOptions } from '../utils/testOptions'; +import { SelectorTreeNode } from '../tree/types'; +import { DOMSelectorTreeComponentRenderer } from './types'; +import { renderVanillaSelectorTree } from './VanillaSelectorTree'; + +export const testInjectStyles = ( + tree: SelectorTreeNode, + selectors: string[], + componentRenderer: DOMSelectorTreeComponentRenderer, + testOptions: TestOptions, +): HTMLElement => { + const vanillaTree = renderVanillaSelectorTree(tree, selectors, componentRenderer, testOptions); + + setTimeout(() => { + styleInjector(selectors); + performanceMeasure(); + }, 2000); + + return vanillaTree; +}; diff --git a/apps/stress-test/src/shared/vanilla/TestMount.ts b/apps/stress-test/src/shared/vanilla/TestMount.ts new file mode 100644 index 00000000000000..1927473b7466fc --- /dev/null +++ b/apps/stress-test/src/shared/vanilla/TestMount.ts @@ -0,0 +1,17 @@ +import { performanceMeasure } from '../utils/performanceMeasure'; +import { TestOptions } from '../utils/testOptions'; +import { SelectorTreeNode } from '../tree/types'; +import { DOMSelectorTreeComponentRenderer } from './types'; +import { renderVanillaSelectorTree } from './VanillaSelectorTree'; + +export const testMount = ( + tree: SelectorTreeNode, + selectors: string[], + componentRenderer: DOMSelectorTreeComponentRenderer, + testOptions: TestOptions, +): HTMLElement => { + const vanillaTree = renderVanillaSelectorTree(tree, selectors, componentRenderer, testOptions); + + requestAnimationFrame(() => performanceMeasure()); + return vanillaTree; +}; diff --git a/apps/stress-test/src/shared/vanilla/TestReRenderAll.ts b/apps/stress-test/src/shared/vanilla/TestReRenderAll.ts new file mode 100644 index 00000000000000..eba98a30ff3feb --- /dev/null +++ b/apps/stress-test/src/shared/vanilla/TestReRenderAll.ts @@ -0,0 +1,33 @@ +import { styleInjector } from '../css/injectStyles'; +import { performanceMeasure } from '../utils/performanceMeasure'; +import { TestOptions } from '../utils/testOptions'; +import { SelectorTreeNode } from '../tree/types'; +import { DOMSelectorTreeComponentRenderer } from './types'; +import { renderVanillaSelectorTree } from './VanillaSelectorTree'; + +export const testReRenderAll = ( + tree: SelectorTreeNode, + selectors: string[], + componentRenderer: DOMSelectorTreeComponentRenderer, + testOptions: TestOptions, +): HTMLElement => { + if (testOptions.withStyles === 'true') { + styleInjector(selectors); + } + + const wrapper = document.createElement('div'); + wrapper.appendChild(renderVanillaSelectorTree(tree, selectors, componentRenderer, testOptions)); + + setTimeout(() => { + while (wrapper.hasChildNodes()) { + wrapper.removeChild(wrapper.lastChild!); + } + + setTimeout(() => { + wrapper.appendChild(renderVanillaSelectorTree(tree, selectors, componentRenderer, testOptions)); + performanceMeasure(); + }, 2000); + }, 2000); + + return wrapper; +}; diff --git a/apps/stress-test/src/shared/vanilla/TestRemoveAll.ts b/apps/stress-test/src/shared/vanilla/TestRemoveAll.ts new file mode 100644 index 00000000000000..44c8fc4005e003 --- /dev/null +++ b/apps/stress-test/src/shared/vanilla/TestRemoveAll.ts @@ -0,0 +1,29 @@ +import { styleInjector } from '../css/injectStyles'; +import { performanceMeasure } from '../utils/performanceMeasure'; +import { TestOptions } from '../utils/testOptions'; +import { SelectorTreeNode } from '../tree/types'; +import { DOMSelectorTreeComponentRenderer } from './types'; +import { renderVanillaSelectorTree } from './VanillaSelectorTree'; + +export const testRemoveAll = ( + tree: SelectorTreeNode, + selectors: string[], + componentRenderer: DOMSelectorTreeComponentRenderer, + testOptions: TestOptions, +): HTMLElement => { + if (testOptions.withStyles === 'true') { + styleInjector(selectors); + } + + const wrapper = document.createElement('div'); + wrapper.appendChild(renderVanillaSelectorTree(tree, selectors, componentRenderer, testOptions)); + + setTimeout(() => { + while (wrapper.hasChildNodes()) { + wrapper.removeChild(wrapper.lastChild!); + } + performanceMeasure(); + }, 2000); + + return wrapper; +}; diff --git a/apps/stress-test/src/shared/vanilla/VanillaSelectorTree.ts b/apps/stress-test/src/shared/vanilla/VanillaSelectorTree.ts index 308fbd70abe436..34122b11e6b2af 100644 --- a/apps/stress-test/src/shared/vanilla/VanillaSelectorTree.ts +++ b/apps/stress-test/src/shared/vanilla/VanillaSelectorTree.ts @@ -1,5 +1,5 @@ import { TestOptions } from '../utils/testOptions'; -import { SelectorTreeNode } from '../tree/RandomSelectorTreeNode'; +import { SelectorTreeNode } from '../tree/types'; import { DOMSelectorTreeComponentRenderer } from './types'; import { renderVanillaTree } from './VanillaTree'; diff --git a/apps/stress-test/src/shared/vanilla/VanillaTree.ts b/apps/stress-test/src/shared/vanilla/VanillaTree.ts index eeb250045b43fd..fa0084492cc98b 100644 --- a/apps/stress-test/src/shared/vanilla/VanillaTree.ts +++ b/apps/stress-test/src/shared/vanilla/VanillaTree.ts @@ -1,4 +1,4 @@ -import { TreeNode } from '../tree/RandomTree'; +import { TreeNode } from '../tree/types'; export type VanillaTreeItemRenderer = (node: T, depth: number, index: number) => HTMLElement; diff --git a/apps/stress-test/src/shared/vanilla/types.ts b/apps/stress-test/src/shared/vanilla/types.ts index 3700ac33581877..fe46bf4ab53434 100644 --- a/apps/stress-test/src/shared/vanilla/types.ts +++ b/apps/stress-test/src/shared/vanilla/types.ts @@ -1,3 +1,3 @@ -import { SelectorTreeNode } from '../tree/RandomSelectorTreeNode'; +import { SelectorTreeNode } from '../tree/types'; export type DOMSelectorTreeComponentRenderer = (node: SelectorTreeNode, depth: number, index: number) => HTMLElement; diff --git a/apps/stress-test/src/shared/wc/TestAdd.ts b/apps/stress-test/src/shared/wc/TestAdd.ts new file mode 100644 index 00000000000000..66af63039550bd --- /dev/null +++ b/apps/stress-test/src/shared/wc/TestAdd.ts @@ -0,0 +1,35 @@ +import { TestOptions } from '../utils/testOptions'; +import { WCSelectorTree } from './WCSelectorTree'; +import { WCTestTree } from './types'; +import { SelectorTreeNode } from '../tree/types'; +import { DOMSelectorTreeComponentRenderer } from '../vanilla/types'; +import { styleInjector } from '../css/injectStyles'; +import { performanceMeasure } from '../utils/performanceMeasure'; + +export class TestAdd extends HTMLElement implements WCTestTree { + constructor( + tree: SelectorTreeNode, + selectors: string[], + componentRenderer: DOMSelectorTreeComponentRenderer, + testOptions: TestOptions, + ) { + super(); + + if (testOptions.withStyles === 'true') { + styleInjector(selectors); + } + + const domTree = new WCSelectorTree(componentRenderer, testOptions); + domTree.tree = tree; + this.appendChild(domTree); + + setTimeout(() => { + const addDomTree = new WCSelectorTree(componentRenderer, testOptions); + addDomTree.tree = tree; + this.appendChild(addDomTree); + requestAnimationFrame(() => performanceMeasure()); + }, 2000); + } +} + +window.customElements.define('wc-test-add', TestAdd); diff --git a/apps/stress-test/src/shared/wc/TestInjectStyles.ts b/apps/stress-test/src/shared/wc/TestInjectStyles.ts new file mode 100644 index 00000000000000..65851107fb7cf7 --- /dev/null +++ b/apps/stress-test/src/shared/wc/TestInjectStyles.ts @@ -0,0 +1,29 @@ +import { TestOptions } from '../utils/testOptions'; +import { WCSelectorTree } from './WCSelectorTree'; +import { WCTestTree } from './types'; +import { SelectorTreeNode } from '../tree/types'; +import { DOMSelectorTreeComponentRenderer } from '../vanilla/types'; +import { styleInjector } from '../css/injectStyles'; +import { performanceMeasure } from '../utils/performanceMeasure'; + +export class TestInjectStyles extends HTMLElement implements WCTestTree { + constructor( + tree: SelectorTreeNode, + selectors: string[], + componentRenderer: DOMSelectorTreeComponentRenderer, + testOptions: TestOptions, + ) { + super(); + + const domTree = new WCSelectorTree(componentRenderer, testOptions); + domTree.tree = tree; + this.appendChild(domTree); + + setTimeout(() => { + styleInjector(selectors); + performanceMeasure(); + }, 2000); + } +} + +window.customElements.define('wc-test-inject-styles', TestInjectStyles); diff --git a/apps/stress-test/src/shared/wc/TestMount.ts b/apps/stress-test/src/shared/wc/TestMount.ts new file mode 100644 index 00000000000000..cf4201725f6a05 --- /dev/null +++ b/apps/stress-test/src/shared/wc/TestMount.ts @@ -0,0 +1,25 @@ +import { performanceMeasure } from '../utils/performanceMeasure'; +import { TestOptions } from '../utils/testOptions'; +import { SelectorTreeNode } from '../tree/types'; +import { DOMSelectorTreeComponentRenderer } from '../vanilla/types'; +import { WCTestTree } from './types'; +import { WCSelectorTree } from './WCSelectorTree'; + +export class TestMount extends HTMLElement implements WCTestTree { + constructor( + tree: SelectorTreeNode, + _selectors: string[], + componentRenderer: DOMSelectorTreeComponentRenderer, + testOptions: TestOptions, + ) { + super(); + + const domTree = new WCSelectorTree(componentRenderer, testOptions); + domTree.tree = tree; + + this.appendChild(domTree); + requestAnimationFrame(() => performanceMeasure()); + } +} + +window.customElements.define('wc-test-mount', TestMount); diff --git a/apps/stress-test/src/shared/wc/TestReRenderAll.ts b/apps/stress-test/src/shared/wc/TestReRenderAll.ts new file mode 100644 index 00000000000000..388e807b1c7b35 --- /dev/null +++ b/apps/stress-test/src/shared/wc/TestReRenderAll.ts @@ -0,0 +1,37 @@ +import { TestOptions } from '../utils/testOptions'; +import { WCSelectorTree } from './WCSelectorTree'; +import { WCTestTree } from './types'; +import { SelectorTreeNode } from '../tree/types'; +import { DOMSelectorTreeComponentRenderer } from '../vanilla/types'; +import { styleInjector } from '../css/injectStyles'; +import { performanceMeasure } from '../utils/performanceMeasure'; + +export class TestReRenderAll extends HTMLElement implements WCTestTree { + constructor( + tree: SelectorTreeNode, + selectors: string[], + componentRenderer: DOMSelectorTreeComponentRenderer, + testOptions: TestOptions, + ) { + super(); + + if (testOptions.withStyles === 'true') { + styleInjector(selectors); + } + + const domTree = new WCSelectorTree(componentRenderer, testOptions); + domTree.tree = tree; + this.appendChild(domTree); + + setTimeout(() => { + domTree.tree = null; + + setTimeout(() => { + domTree.tree = tree; + requestAnimationFrame(() => performanceMeasure()); + }, 2000); + }, 2000); + } +} + +window.customElements.define('wc-test-re-render-all', TestReRenderAll); diff --git a/apps/stress-test/src/shared/wc/TestRemoveAll.ts b/apps/stress-test/src/shared/wc/TestRemoveAll.ts new file mode 100644 index 00000000000000..74b7c7a7de8580 --- /dev/null +++ b/apps/stress-test/src/shared/wc/TestRemoveAll.ts @@ -0,0 +1,33 @@ +import { TestOptions } from '../utils/testOptions'; +import { WCSelectorTree } from './WCSelectorTree'; +import { WCTestTree } from './types'; +import { SelectorTreeNode } from '../tree/types'; +import { DOMSelectorTreeComponentRenderer } from '../vanilla/types'; +import { styleInjector } from '../css/injectStyles'; +import { performanceMeasure } from '../utils/performanceMeasure'; + +export class TestRemoveAll extends HTMLElement implements WCTestTree { + constructor( + tree: SelectorTreeNode, + selectors: string[], + componentRenderer: DOMSelectorTreeComponentRenderer, + testOptions: TestOptions, + ) { + super(); + + if (testOptions.withStyles === 'true') { + styleInjector(selectors); + } + + const domTree = new WCSelectorTree(componentRenderer, testOptions); + domTree.tree = tree; + this.appendChild(domTree); + + setTimeout(() => { + domTree.tree = null; + requestAnimationFrame(() => performanceMeasure()); + }, 2000); + } +} + +window.customElements.define('wc-test-remove-all', TestRemoveAll); diff --git a/apps/stress-test/src/shared/wc/WCSelectorTree.ts b/apps/stress-test/src/shared/wc/WCSelectorTree.ts index 957d862e5d6214..333d523a6296cf 100644 --- a/apps/stress-test/src/shared/wc/WCSelectorTree.ts +++ b/apps/stress-test/src/shared/wc/WCSelectorTree.ts @@ -1,5 +1,5 @@ import { TestOptions } from '../utils/testOptions'; -import { SelectorTreeNode } from '../tree/RandomSelectorTreeNode'; +import { SelectorTreeNode } from '../tree/types'; import { DOMSelectorTreeComponentRenderer } from '../vanilla/types'; import { WCTree } from './WCTree'; diff --git a/apps/stress-test/src/shared/wc/WCTest.ts b/apps/stress-test/src/shared/wc/WCTest.ts new file mode 100644 index 00000000000000..a14d8b6120b1c4 --- /dev/null +++ b/apps/stress-test/src/shared/wc/WCTest.ts @@ -0,0 +1,61 @@ +import { getTestOptions } from '../utils/testOptions'; +import { loadTestData, TestDOMRenderer, TestTreeFixture } from '../utils/testUtils'; +import { RandomSelectorTreeNode } from '../tree/types'; +import { RandomTree } from '../../../scripts/utils/tree/RandomTree'; +import { testInjectStyles } from '../vanilla/TestInjectStyles'; +import { testMount } from '../vanilla/TestMount'; +import { testRemoveAll } from '../vanilla/TestRemoveAll'; +import { testReRenderAll } from '../vanilla/TestReRenderAll'; +import { TestInjectStyles } from './TestInjectStyles'; +import { TestMount } from './TestMount'; +import { TestRemoveAll } from './TestRemoveAll'; +import { TestReRenderAll } from './TestReRenderAll'; +import { TestAdd } from './TestAdd'; +import { testAdd } from '../vanilla/TestAdd'; + +export const wcTest = (): Promise => { + const testOptions = getTestOptions(); + const { test, fixtureName, rendererName, r } = testOptions; + + return loadTestData('wc', fixtureName, rendererName ?? r).then( + ({ fixture, renderer }) => { + const selectors: string[] = fixture.selectors; + const fixtureTree = fixture.tree; + + const treeBuilder = new RandomTree(); + const tree = treeBuilder.fromFixture(fixtureTree); + const { wcRenderer } = testOptions; + + let testFn; + + const isVanilla = wcRenderer === 'vanilla'; + + if (test === 'inject-styles') { + testFn = isVanilla ? testInjectStyles : TestInjectStyles; + } else if (test === 'mount') { + testFn = isVanilla ? testMount : TestMount; + } else if (test === 're-render-all') { + testFn = isVanilla ? testReRenderAll : TestReRenderAll; + } else if (test === 'remove-all') { + testFn = isVanilla ? testRemoveAll : TestRemoveAll; + } else if (test === 'add') { + testFn = isVanilla ? testAdd : TestAdd; + } + + if (testFn) { + if (isVanilla) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return testFn(tree, selectors, renderer, testOptions); + } else { + /* eslint-disable */ + // @ts-ignore + return new testFn(tree, selectors, renderer, testOptions); + /* eslint-enable */ + } + } + + return null; + }, + ); +}; diff --git a/apps/stress-test/src/shared/wc/WCTree.ts b/apps/stress-test/src/shared/wc/WCTree.ts index cd803d593e1209..49c52fc3202e70 100644 --- a/apps/stress-test/src/shared/wc/WCTree.ts +++ b/apps/stress-test/src/shared/wc/WCTree.ts @@ -1,4 +1,4 @@ -import { TreeNode } from '../tree/RandomTree'; +import { TreeNode } from '../tree/types'; export type WCTreeItemRenderer = (node: T, depth: number, index: number) => HTMLElement; diff --git a/apps/stress-test/src/shared/wc/types.ts b/apps/stress-test/src/shared/wc/types.ts new file mode 100644 index 00000000000000..48fbc32bde2cdf --- /dev/null +++ b/apps/stress-test/src/shared/wc/types.ts @@ -0,0 +1,4 @@ +export interface WCTestTree {} +export interface TestOptions { + [key: string]: string | number; +} diff --git a/apps/stress-test/webpack/webpack.config.ts b/apps/stress-test/webpack/webpack.config.ts index 6c57826f433b2d..14f1605cba8a83 100644 --- a/apps/stress-test/webpack/webpack.config.ts +++ b/apps/stress-test/webpack/webpack.config.ts @@ -27,7 +27,7 @@ const createConfig: WebpackConfigurationCreator = (_env, argv) => { output: { filename: '[name].[contenthash].bundle.js', sourceMapFilename: '[name].[contenthash].map', - path: path.resolve(path.dirname(__dirname), 'dist'), + path: path.resolve(path.dirname(__dirname), !isProd ? 'dev-build' : 'dist'), }, devtool: 'source-map', resolve: { From 62b0c6fff9feb1b6b354880a0fa0cc6562bb1459 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Fri, 23 Sep 2022 15:26:14 -0700 Subject: [PATCH 2/8] stress test: configure fixtures, tree generation and sample sizes Adds a "target size" option to RandomTree that, when set, builds a tree to a specific size. Tweaks the default fixture parameters based on manual testing. Fixtures essentially double in size for each t-shirt size. Reduces the default number of Tachometer samples from 25 to 10. In some testing this still gives statistically significant results for all default sizes while making tests take significantly less time to run. --- .../scripts/utils/configureYargs.ts | 2 +- apps/stress-test/scripts/utils/fixtures.ts | 84 +++++++++++-------- .../scripts/utils/tree/RandomTree.ts | 23 ++++- 3 files changed, 67 insertions(+), 42 deletions(-) diff --git a/apps/stress-test/scripts/utils/configureYargs.ts b/apps/stress-test/scripts/utils/configureYargs.ts index f7947958443850..885368b1a30f18 100644 --- a/apps/stress-test/scripts/utils/configureYargs.ts +++ b/apps/stress-test/scripts/utils/configureYargs.ts @@ -23,7 +23,7 @@ const cliOptions = { }, 'sample-size': { describe: 'Number of samples to run for each test case.', - default: 25, + default: 10, }, targets: { describe: 'Tests to target.', diff --git a/apps/stress-test/scripts/utils/fixtures.ts b/apps/stress-test/scripts/utils/fixtures.ts index 897250b9f100a4..b0eb6834b3249f 100644 --- a/apps/stress-test/scripts/utils/fixtures.ts +++ b/apps/stress-test/scripts/utils/fixtures.ts @@ -21,77 +21,87 @@ const defaultFixtureOptions: DefaultFixtureOptions = { // eslint-disable-next-line @typescript-eslint/naming-convention xs_1: { minBreadth: '1', - maxBreadth: '3', + maxBreadth: '5', minDepth: '1', - maxDepth: '3', + maxDepth: '5', + targetSize: '250', }, // eslint-disable-next-line @typescript-eslint/naming-convention xs_2: { minBreadth: '1', - maxBreadth: '3', + maxBreadth: '5', minDepth: '1', - maxDepth: '3', - seed: '4212021', + maxDepth: '5', + targetSize: '250', + seed: '7032017', }, // eslint-disable-next-line @typescript-eslint/naming-convention s_1: { minBreadth: '2', - maxBreadth: '5', + maxBreadth: '10', minDepth: '2', - maxDepth: '5', + maxDepth: '10', + targetSize: '500', }, // eslint-disable-next-line @typescript-eslint/naming-convention s_2: { minBreadth: '2', - maxBreadth: '5', + maxBreadth: '10', minDepth: '2', - maxDepth: '5', - seed: '4212021', + maxDepth: '10', + targetSize: '500', + seed: '7032017', }, // eslint-disable-next-line @typescript-eslint/naming-convention m_1: { - minBreadth: '3', - maxBreadth: '8', - minDepth: '3', - maxDepth: '8', + minBreadth: '4', + maxBreadth: '20', + minDepth: '4', + maxDepth: '20', + targetSize: '1000', }, // eslint-disable-next-line @typescript-eslint/naming-convention m_2: { - minBreadth: '3', - maxBreadth: '8', - minDepth: '3', - maxDepth: '8', - seed: '4212021', + minBreadth: '4', + maxBreadth: '20', + minDepth: '4', + maxDepth: '20', + targetSize: '1000', + seed: '7032017', }, // eslint-disable-next-line @typescript-eslint/naming-convention l_1: { - minBreadth: '5', - maxBreadth: '15', - minDepth: '4', - maxDepth: '15', + minBreadth: '8', + maxBreadth: '40', + minDepth: '8', + maxDepth: '40', + targetSize: '2000', }, // eslint-disable-next-line @typescript-eslint/naming-convention l_2: { - minBreadth: '5', - maxBreadth: '15', - minDepth: '4', - maxDepth: '15', - seed: '4212021', + minBreadth: '8', + maxBreadth: '40', + minDepth: '8', + maxDepth: '40', + targetSize: '2000', + seed: '7032017', }, // eslint-disable-next-line @typescript-eslint/naming-convention xl_1: { - minBreadth: '10', - maxBreadth: '20', - minDepth: '10', - maxDepth: '20', + minBreadth: '16', + maxBreadth: '80', + minDepth: '16', + maxDepth: '80', + targetSize: '4000', }, // eslint-disable-next-line @typescript-eslint/naming-convention xl_2: { - minBreadth: '10', - maxBreadth: '20', - minDepth: '10', - maxDepth: '20', - seed: '4212021', + minBreadth: '16', + maxBreadth: '80', + minDepth: '16', + maxDepth: '80', + targetSize: '4000', + seed: '7032017', }, }; diff --git a/apps/stress-test/scripts/utils/tree/RandomTree.ts b/apps/stress-test/scripts/utils/tree/RandomTree.ts index fbe74f66ae5801..0465070d12bca1 100644 --- a/apps/stress-test/scripts/utils/tree/RandomTree.ts +++ b/apps/stress-test/scripts/utils/tree/RandomTree.ts @@ -10,6 +10,7 @@ type TreeParams = { minBreadth?: number; maxBreadth?: number; seed?: number; + targetSize?: number; }; /** @@ -30,14 +31,23 @@ export class RandomTree { private maxDepth: number; private minBreadth: number; private maxBreadth: number; + private targetSize: number; private rando: LCG; - constructor({ minDepth = 1, maxDepth = 15, minBreadth = 1, maxBreadth = 15, seed = defaultSeed }: TreeParams = {}) { + constructor({ + minDepth = 1, + maxDepth = 15, + minBreadth = 1, + maxBreadth = 15, + seed = defaultSeed, + targetSize, + }: TreeParams = {}) { this.minDepth = minDepth; this.maxDepth = maxDepth; this.minBreadth = minBreadth; this.maxBreadth = maxBreadth; + this.targetSize = targetSize ?? Infinity; this.rando = new LCG(seed); this.numNodes = 0; @@ -46,7 +56,12 @@ export class RandomTree { public build = (createNode: TreeNodeCreateCallback): TreeNode => { this.numNodes = 1; const root = createNode(null, 0, 0); - return this._doBuild(createNode, root, 1); + let tree = this._doBuild(createNode, root, 1); + while (this.numNodes < this.targetSize) { + tree = this._doBuild(createNode, root, 1); + } + + return tree; }; public fromFixture = (fixture: TestTreeFixture['tree'], parent: TreeNode | null = null): TreeNode => { @@ -80,13 +95,13 @@ export class RandomTree { const breadth = this._randomBreadth(currentBreadth); const depth = this._randomDepth(Math.max(this.maxDepth - currentDepth, this.minDepth)); - for (let i = 0; i < breadth; i++) { + for (let i = 0; i < breadth && this.numNodes < this.targetSize; i++) { this.numNodes++; const node = createNode(parent, currentDepth, breadth); parent.children.push(node); - if (currentDepth < depth) { + if (currentDepth < depth && this.numNodes < this.targetSize) { this._doBuild( createNode, node, From 11394e5b6ecf967aab4f47055e6fa1dc07ced3a0 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Fri, 23 Sep 2022 15:52:24 -0700 Subject: [PATCH 3/8] stress test: remove 'renderers' cli option This feature was removed in favor of query params. --- apps/stress-test/scripts/commands/buildTestConfig.ts | 1 - apps/stress-test/scripts/utils/configureYargs.ts | 7 ------- 2 files changed, 8 deletions(-) diff --git a/apps/stress-test/scripts/commands/buildTestConfig.ts b/apps/stress-test/scripts/commands/buildTestConfig.ts index 71dd499b51e414..fa5213f226a6a3 100644 --- a/apps/stress-test/scripts/commands/buildTestConfig.ts +++ b/apps/stress-test/scripts/commands/buildTestConfig.ts @@ -72,7 +72,6 @@ const makeConfigJson: MakeConfigJson = (_scenario, browser, testCase, sampleSize ], expand: targets.map(target => { - // const params = querystring.stringify({ test: testCase, ...testOptions }); const targetParams = target.includes('?') ? querystring.parse(target.substring(target.indexOf('?') + 1)) : {}; const params = querystring.stringify({ ...targetParams, diff --git a/apps/stress-test/scripts/utils/configureYargs.ts b/apps/stress-test/scripts/utils/configureYargs.ts index 885368b1a30f18..648b2ea290e526 100644 --- a/apps/stress-test/scripts/utils/configureYargs.ts +++ b/apps/stress-test/scripts/utils/configureYargs.ts @@ -45,9 +45,6 @@ const cliOptions = { describe: 'Optimization mode for Griffel.', default: 'buildtime', }, - renderers: { - describe: 'Renderers to use for testing. This determines what is actually tested.', - }, 'test-options': { describe: 'Options to apply to each test. E.g., option1=value1 option2=value2', coerce: (arg: string[]) => { @@ -164,7 +161,6 @@ const configureYargs: CongfigureYargs = (command, y) => { targets, 'test-options': testOptions, port, - renderers, } = cliOptions; configure(y, { scenario, @@ -175,7 +171,6 @@ const configureYargs: CongfigureYargs = (command, y) => { targets, 'test-options': testOptions, port, - renderers, }); break; } @@ -192,7 +187,6 @@ const configureYargs: CongfigureYargs = (command, y) => { 'process-results': processResults, port, root, - renderers, } = cliOptions; configure(y, { scenario, @@ -205,7 +199,6 @@ const configureYargs: CongfigureYargs = (command, y) => { 'process-results': processResults, port, root, - renderers, }); break; } From f0b0d09b3d58e386a93a4a35adb6bbaff8d6a028 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Fri, 23 Sep 2022 15:52:56 -0700 Subject: [PATCH 4/8] stress test: update readme Fix inaccuracies, document new features. --- apps/stress-test/README.md | 56 ++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/apps/stress-test/README.md b/apps/stress-test/README.md index 5726399518aa6b..7f4184db4d448a 100644 --- a/apps/stress-test/README.md +++ b/apps/stress-test/README.md @@ -26,16 +26,16 @@ $ yarn workspace @fluentui/stress-test stress-test build --build-deps $ yarn workspace @fluentui/stress-test stress-test build ``` -> NOTE: `build:local` is much slower, but is required the first time you're building the application or if you've pulled in lots of changes. Use `build:app` if you don't need to build dependencies like `@fluentui/react` as it's much faster. +> NOTE: `build --build-deps` is much slower, but is required the first time you're building the application or if you've pulled in lots of changes. Use `build` if you don't need to build dependencies like `@fluentui/react` as it's much faster. ### Examples ```shell -# Run the "simple-stress" scenario with the "mount" and "prop-update" test cases against Firefox with small sizes and low sample size -$ yarn workspace @fluentui/stress-test stress-test run --scenario simple-stress --sample-size 2 --test-cases mount prop-update --browsers firefox --sizes xs s +# Run your "my-scenario" with the "mount" and "inject-styles" test cases against Firefox with small sizes and low sample size. Target the "stress-tree" page with "button" renderers. +$ yarn workspace @fluentui/stress-test stress-test run --scenario my-scenario --test-cases mount inject-styles --browsers chrome firefox --sizes xs s --targets v8/stress-test?r=button v9/stress-test?r=button wc/stress-test?r=button -# Run the "simple-stress" scenario with the "mount" and "prop-update" test cases against the default browsers at the default sizes and sample size -$ yarn workspace @fluentui/stress-test stress-test run --scenario simple-stress --test-cases mount prop-update +# Run your "my-scenario" scenario with the "mount" and "inject-styles" test cases against the default browsers at the default sizes and sample size. Target the "stress-tree" page with "button" renderers. +$ yarn workspace @fluentui/stress-test stress-test run --scenario my-scenario --test-cases mount inject-styles --targets v8/stress-test?r=button v9/stress-test?r=button wc/stress-test?r=button ``` > NOTE: Tests should be run against production builds. While tests can be run against development builds, and this is useful for gathering quick results and debugging, the performance characteristics of development and production builds can differ quite a bit. @@ -43,8 +43,9 @@ $ yarn workspace @fluentui/stress-test stress-test run --scenario simple-stress ## Glossary - **scenario**: A testing scenario for a specific line of investigation. For example, if you wanted to compare the performance of different `Button` implementations you might create a "button-test" scenario for various targets. -- **targets**: Different implementation targets. For example: "v8" for Fluent UI v8, "v9" for Fluent UI v9, "wc" for Fluent UI Web Components. +- **targets**: Different implementation targets. These are URLs that can accept query parameters. For example "v8/stress-tree?r=button", "wc/stress-tree?r=button". - **test cases**: Different test cases to run against a given scenario. For example, you might want to test mounting performance for a scenario. +- **renderers**: Simple components that can be plugged into targets allowing easy customization of the component under test. ## Development @@ -65,6 +66,45 @@ The `benchmarks` folder houses Tachometer configurations and test results; and h The `scripts` folder house the Stress Test CLI app that is used to run tests. -### Adding test cases +### Creating your own tests -Add tests cases to the appropriate `src/pages` sub-folder. For example to create a new test, "my test" for Fluent v9 add it to `src/pages/v9/my-test`. Use an existing page as a guide for the files you need to add. Pages are automatically picked up by Webpack when the dev server is started. +There are a few way you can create your own tests depending on what you want to do. + +#### Adding a new test with a renderer + +The simplest thing to do is use an existing test page like "stress-tree" but with your own renderer. A renderer is just a function that renders a component to be rendered. Renderers are specific to the UI library being tested so a Web Component renderer is different from a React one but conceptually they are the same. + +An example React renderer: + +```tsx +// myButton.tsx +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; +``` + +> Note: renderers need to be saved in the appropriate `src/renderers` subfolder. `v8` for Fluent V8, `v9` for Fluent V9, `wc` for Fluent Web Components + +In your test specify the renderer with the `r` query parameter: + +```shell +yarn workspace @fluentui/stress-test stress-test run my-button-scenarion --test-cases mount --targets v9/stress-tree?r=myButton +``` + +#### Adding new test fixtures + +Test fixtures are used to drive tests. In the "stress-tree` page fixtures are used to control different sizes of DOM tree so tests can be run against "simple" to "complex" DOM trees. + +Fixtures can be generated with the `build-fixture` command: + +```shell +yarn workspace @fluentui/stress-test stress-test build-fixture --name my-fixture --option option1=value1 option2=value2 +``` + +Fixtures are saved in the `src/fixtures` folder. + +> Note: fixtures can become quite large so they are excluded from git. If you need others to be able to generate your fixture be sure to keep track of the options you use to create the fixture. + +#### Adding a new test page + +A page is a way to control the entire structure of your test. Currently, we ship a "stress-tree" page which loads a broad and deep DOM tree to simulate a complex real-world web application. If you need something different create a page to represent the situation you need to test. See `src/pages` for examples of how to create a page. From e0cbefb5bc0b43406f7b78f7e4989eec09127c61 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Mon, 26 Sep 2022 08:13:06 -0700 Subject: [PATCH 5/8] stress-test: improve tests Improve test consistency across browsers. Firefox and Chrome/Edge can measure different things in certain scenarios so this commit updates the measurements for mount to account for this. The React Profiler has been enabled so we can get timing information from React. In particular this is useful for knowing when React started doing work. --- apps/stress-test/scripts/utils/fixtures.ts | 2 +- .../utils/tree/RandomSelectorTreeNode.ts | 99 +++- .../scripts/utils/tree/RandomTree.ts | 29 +- .../src/pages/v8/stress-tree/index.tsx | 2 +- .../src/pages/v9/stress-tree/index.tsx | 1 + .../src/pages/wc/stress-tree/index.wc.ts | 4 + .../stress-test/src/renderers/v8/checkbox.tsx | 9 + .../src/renderers/v8/textField.tsx | 9 + .../stress-test/src/renderers/v8/textarea.tsx | 9 + .../src/renderers/v9/btn/basic.tsx | 10 + .../v9/{btnOverride => btn}/idiomatic.tsx | 0 .../v9/{btnOverride => btn}/override.tsx | 0 .../v9/{btnOverride => btn}/styles.ts | 0 .../stress-test/src/renderers/v9/checkbox.tsx | 9 + apps/stress-test/src/renderers/v9/input.tsx | 9 + .../src/renderers/v9/spinButton/noPseudos.tsx | 483 ++++++++++++++++++ .../src/renderers/v9/spinButton/vanilla.tsx | 8 + .../stress-test/src/renderers/v9/textarea.tsx | 9 + .../src/renderers/v9/textarea/noPseudo.tsx | 282 ++++++++++ .../src/renderers/v9/vanillaButton.tsx | 8 + .../src/renderers/wc/btn/vanillaButton.ts | 11 + .../src/renderers/wc/btn/wcBasicButton.ts | 101 ++++ apps/stress-test/src/renderers/wc/button.ts | 4 +- apps/stress-test/src/renderers/wc/checkbox.ts | 13 + .../src/renderers/wc/numberField/vanilla.ts | 11 + .../stress-test/src/renderers/wc/textField.ts | 13 + .../src/shared/react/ReactTree.tsx | 13 +- .../src/shared/react/TestInjectStyles.tsx | 5 +- .../src/shared/react/TestMount.tsx | 43 +- apps/stress-test/src/shared/react/onRender.ts | 12 + .../src/shared/vanilla/TestMount.ts | 13 +- .../src/shared/vanilla/VanillaTree.ts | 10 + apps/stress-test/src/shared/wc/TestMount.ts | 7 +- apps/stress-test/src/shared/wc/WCTest.ts | 24 +- apps/stress-test/tsconfig.json | 2 +- apps/stress-test/webpack/webpack.config.ts | 14 + package.json | 1 - yarn.lock | 71 +-- 38 files changed, 1212 insertions(+), 138 deletions(-) create mode 100644 apps/stress-test/src/renderers/v8/checkbox.tsx create mode 100644 apps/stress-test/src/renderers/v8/textField.tsx create mode 100644 apps/stress-test/src/renderers/v8/textarea.tsx create mode 100644 apps/stress-test/src/renderers/v9/btn/basic.tsx rename apps/stress-test/src/renderers/v9/{btnOverride => btn}/idiomatic.tsx (100%) rename apps/stress-test/src/renderers/v9/{btnOverride => btn}/override.tsx (100%) rename apps/stress-test/src/renderers/v9/{btnOverride => btn}/styles.ts (100%) create mode 100644 apps/stress-test/src/renderers/v9/checkbox.tsx create mode 100644 apps/stress-test/src/renderers/v9/input.tsx create mode 100644 apps/stress-test/src/renderers/v9/spinButton/noPseudos.tsx create mode 100644 apps/stress-test/src/renderers/v9/spinButton/vanilla.tsx create mode 100644 apps/stress-test/src/renderers/v9/textarea.tsx create mode 100644 apps/stress-test/src/renderers/v9/textarea/noPseudo.tsx create mode 100644 apps/stress-test/src/renderers/v9/vanillaButton.tsx create mode 100644 apps/stress-test/src/renderers/wc/btn/vanillaButton.ts create mode 100644 apps/stress-test/src/renderers/wc/btn/wcBasicButton.ts create mode 100644 apps/stress-test/src/renderers/wc/checkbox.ts create mode 100644 apps/stress-test/src/renderers/wc/numberField/vanilla.ts create mode 100644 apps/stress-test/src/renderers/wc/textField.ts create mode 100644 apps/stress-test/src/shared/react/onRender.ts diff --git a/apps/stress-test/scripts/utils/fixtures.ts b/apps/stress-test/scripts/utils/fixtures.ts index b0eb6834b3249f..745a66f3f7bd37 100644 --- a/apps/stress-test/scripts/utils/fixtures.ts +++ b/apps/stress-test/scripts/utils/fixtures.ts @@ -134,7 +134,7 @@ export const buildTreeFixture: BuildTreeFixture = (name, options) => { const data = { tree: JSON.parse(jsonTree), - selectors, + selectors: Array.from(new Set(selectors)), }; const js = `export default ${JSON.stringify(data, null, 2)};`; diff --git a/apps/stress-test/scripts/utils/tree/RandomSelectorTreeNode.ts b/apps/stress-test/scripts/utils/tree/RandomSelectorTreeNode.ts index ceb7184b05ba1d..3481dc455e8fdf 100644 --- a/apps/stress-test/scripts/utils/tree/RandomSelectorTreeNode.ts +++ b/apps/stress-test/scripts/utils/tree/RandomSelectorTreeNode.ts @@ -12,23 +12,41 @@ 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, + addAttribute: 0.2, + buildDescendentSelector: 0.5, + addSibling: 0.1, + addPseudo: 0.1, useDescendantCombinator: 0.2, + useNonMatchingSelector: 0.5, }; +const nonMatchingSelector = '.non-matching-selector'; + const buildDescendentSelector = ( node: TreeNode | null, - selector: string = '', + selector?: string, ): string => { if (!node) { + return selector ?? ''; + } + + if (!selector) { + selector = choice(getSelectorsFromNode(node, { pseudos: false })); + } + + const parent = node.parent; + + if (!parent) { return selector; } + let selectorChoices = getSelectorsFromNode(parent, { pseudos: false }); + if (coin(chances.useNonMatchingSelector)) { + selectorChoices = [...selectorChoices, nonMatchingSelector]; + } + selector = ( - maybeNot(choice(getSelectorsFromNode(node))) + + maybeNot(choice(selectorChoices)) + (coin(chances.useDescendantCombinator) ? ' > ' : ' ') + selector ).trim(); @@ -68,11 +86,38 @@ const getAttributes = () => { return attributes; }; -const getSiblingSelectors = () => { +const getSiblingSelectors = ( + parent: TreeNode | null, + node: TreeNode, +) => { const siblings = [] as string[]; - if (coin(chances.addSibling)) { - siblings.push(randomSelector.randomSelector(['nth-child'])); + if (parent && coin(chances.addSibling)) { + const combinator = choice(['nth-child', '~', '+']); + if (combinator === 'nth-child') { + siblings.push(randomSelector.randomSelector(['nth-child'])); + } else { + const sibling = choice(parent.children); + if (!sibling) { + return siblings; + } + const siblingSelectorType = choice(['classNames', 'attribute']); + let siblingSelector; + if (siblingSelectorType === 'classNames') { + siblingSelector = choice(sibling.value.classNames) ?? '*'; + } else { + siblingSelector = choice(sibling.value.attributes)?.selector ?? '*'; + } + + const nodeSelectorType = choice(['classNames', 'attribute']); + let nodeSelector; + if (nodeSelectorType === 'classNames') { + nodeSelector = choice(node.value.classNames) ?? '*'; + } else { + nodeSelector = choice(node.value.attributes)?.selector ?? '*'; + } + siblings.push(`${siblingSelector} ${combinator} ${nodeSelector}`); + } } return siblings; @@ -88,13 +133,31 @@ const getPseudoSelectors = () => { return pseudo; }; -const getSelectorsFromNode = (node: TreeNode): string[] => { - return [ - ...node.value.classNames, - ...node.value.attributes.map(attr => attr.selector), - ...node.value.siblings, - ...node.value.pseudos, - ]; +type IncludeOptions = { + [k in keyof RandomSelectorTreeNode]?: boolean; +}; +const getSelectorsFromNode = (node: TreeNode, include?: IncludeOptions): string[] => { + const { classNames = true, attributes = true, siblings = true, pseudos = true } = include ?? {}; + + let selectors = [] as string[]; + + if (classNames) { + selectors = selectors.concat(...node.value.classNames); + } + + if (attributes) { + selectors = selectors.concat(...node.value.attributes.map(attr => attr.selector)); + } + + if (siblings) { + selectors = selectors.concat(...node.value.siblings); + } + + if (pseudos) { + selectors = selectors.concat(...node.value.pseudos); + } + + return selectors; }; export type RandomSelectorTreeCreator = (selectors: string[]) => TreeNodeCreateCallback; @@ -106,13 +169,15 @@ export const selectorTreeCreator: RandomSelectorTreeCreator = selectors => { name: `${depth}-${breadth}`, classNames: getNodeClassNames(), attributes: getAttributes(), - siblings: getSiblingSelectors(), + siblings: [] as string[], pseudos: getPseudoSelectors(), }, children: [], parent, }; + node.value.siblings = getSiblingSelectors(parent, node); + if (coin(chances.buildDescendentSelector)) { const descendentSelector = buildDescendentSelector(node); selectors.push(descendentSelector); diff --git a/apps/stress-test/scripts/utils/tree/RandomTree.ts b/apps/stress-test/scripts/utils/tree/RandomTree.ts index 0465070d12bca1..6c83d6922b9e2e 100644 --- a/apps/stress-test/scripts/utils/tree/RandomTree.ts +++ b/apps/stress-test/scripts/utils/tree/RandomTree.ts @@ -13,18 +13,6 @@ type TreeParams = { targetSize?: number; }; -/** - * 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; @@ -43,11 +31,11 @@ export class RandomTree { seed = defaultSeed, targetSize, }: TreeParams = {}) { - this.minDepth = minDepth; - this.maxDepth = maxDepth; - this.minBreadth = minBreadth; - this.maxBreadth = maxBreadth; - this.targetSize = targetSize ?? Infinity; + this.minDepth = Number(minDepth); + this.maxDepth = Number(maxDepth); + this.minBreadth = Number(minBreadth); + this.maxBreadth = Number(maxBreadth); + this.targetSize = targetSize ? Number(targetSize) : Infinity; this.rando = new LCG(seed); this.numNodes = 0; @@ -102,12 +90,7 @@ export class RandomTree { parent.children.push(node); if (currentDepth < depth && this.numNodes < this.targetSize) { - this._doBuild( - createNode, - node, - currentDepth + 1, - Math.max(Math.ceil(decay(this.maxBreadth, 0.005, this.numNodes)), this.minBreadth), - ); + this._doBuild(createNode, node, currentDepth + 1); } } diff --git a/apps/stress-test/src/pages/v8/stress-tree/index.tsx b/apps/stress-test/src/pages/v8/stress-tree/index.tsx index af8fb1d5059743..055cce6a621342 100644 --- a/apps/stress-test/src/pages/v8/stress-tree/index.tsx +++ b/apps/stress-test/src/pages/v8/stress-tree/index.tsx @@ -7,7 +7,7 @@ import { ReactTest } from '../../../shared/react/ReactTest'; initializeIcons(); const { fixtureName, rendererName, r } = getTestOptions(); - +document.title += ' | ' + r ?? rendererName; ReactDOM.render( diff --git a/apps/stress-test/src/pages/v9/stress-tree/index.tsx b/apps/stress-test/src/pages/v9/stress-tree/index.tsx index c8b69fdc3733d0..c8b0001571dc11 100644 --- a/apps/stress-test/src/pages/v9/stress-tree/index.tsx +++ b/apps/stress-test/src/pages/v9/stress-tree/index.tsx @@ -6,6 +6,7 @@ import { ReactTest } from '../../../shared/react/ReactTest'; import { getTestOptions } from '../../../shared/utils/testOptions'; const { fixtureName, rendererName, r } = getTestOptions(); +document.title += ' | ' + r ?? rendererName; ReactDOM.render( diff --git a/apps/stress-test/src/pages/wc/stress-tree/index.wc.ts b/apps/stress-test/src/pages/wc/stress-tree/index.wc.ts index c5a4e6778605be..09d14b03269fe6 100644 --- a/apps/stress-test/src/pages/wc/stress-tree/index.wc.ts +++ b/apps/stress-test/src/pages/wc/stress-tree/index.wc.ts @@ -1,5 +1,9 @@ +import { getTestOptions } from '../../../shared/utils/testOptions'; import { wcTest } from '../../../shared/wc/WCTest'; wcTest().then(testNode => { + const { r, rendererName } = getTestOptions(); + document.title += ' | ' + r ?? rendererName; + document.querySelector('#app')?.appendChild(testNode!); }); diff --git a/apps/stress-test/src/renderers/v8/checkbox.tsx b/apps/stress-test/src/renderers/v8/checkbox.tsx new file mode 100644 index 00000000000000..d52032c41e0917 --- /dev/null +++ b/apps/stress-test/src/renderers/v8/checkbox.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { Checkbox } from '@fluentui/react'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v8/textField.tsx b/apps/stress-test/src/renderers/v8/textField.tsx new file mode 100644 index 00000000000000..1d0d855cfae7af --- /dev/null +++ b/apps/stress-test/src/renderers/v8/textField.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { TextField } from '@fluentui/react'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v8/textarea.tsx b/apps/stress-test/src/renderers/v8/textarea.tsx new file mode 100644 index 00000000000000..2dbd15db5085ea --- /dev/null +++ b/apps/stress-test/src/renderers/v8/textarea.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { TextField } from '@fluentui/react'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/btn/basic.tsx b/apps/stress-test/src/renderers/v9/btn/basic.tsx new file mode 100644 index 00000000000000..0c30dbc0592ac8 --- /dev/null +++ b/apps/stress-test/src/renderers/v9/btn/basic.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import { useRootStyles } from './styles'; +import { ReactSelectorTreeComponentRenderer } from '../../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + const rootStyles = useRootStyles(); + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/btnOverride/idiomatic.tsx b/apps/stress-test/src/renderers/v9/btn/idiomatic.tsx similarity index 100% rename from apps/stress-test/src/renderers/v9/btnOverride/idiomatic.tsx rename to apps/stress-test/src/renderers/v9/btn/idiomatic.tsx diff --git a/apps/stress-test/src/renderers/v9/btnOverride/override.tsx b/apps/stress-test/src/renderers/v9/btn/override.tsx similarity index 100% rename from apps/stress-test/src/renderers/v9/btnOverride/override.tsx rename to apps/stress-test/src/renderers/v9/btn/override.tsx diff --git a/apps/stress-test/src/renderers/v9/btnOverride/styles.ts b/apps/stress-test/src/renderers/v9/btn/styles.ts similarity index 100% rename from apps/stress-test/src/renderers/v9/btnOverride/styles.ts rename to apps/stress-test/src/renderers/v9/btn/styles.ts diff --git a/apps/stress-test/src/renderers/v9/checkbox.tsx b/apps/stress-test/src/renderers/v9/checkbox.tsx new file mode 100644 index 00000000000000..976203251a208a --- /dev/null +++ b/apps/stress-test/src/renderers/v9/checkbox.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { Checkbox } from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/input.tsx b/apps/stress-test/src/renderers/v9/input.tsx new file mode 100644 index 00000000000000..3a36f5c647e3c5 --- /dev/null +++ b/apps/stress-test/src/renderers/v9/input.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { Input } from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/spinButton/noPseudos.tsx b/apps/stress-test/src/renderers/v9/spinButton/noPseudos.tsx new file mode 100644 index 00000000000000..c4ffd670a77531 --- /dev/null +++ b/apps/stress-test/src/renderers/v9/spinButton/noPseudos.tsx @@ -0,0 +1,483 @@ +import * as React from 'react'; +import { + useSpinButton_unstable, + renderSpinButton_unstable, + ForwardRefComponent, + SpinButtonProps, + SpinButtonState, + makeStyles, + mergeClasses, + shorthands, + spinButtonClassNames, + tokens, + useInputStyles_unstable, +} from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../../shared/react/types'; + +const spinButtonExtraClassNames = { + buttonActive: 'fui-SpinButton__button_active', +}; + +const useRootStyles = makeStyles({ + base: { + display: 'inline-grid', + gridTemplateColumns: `1fr 24px`, + gridTemplateRows: '1fr 1fr', + columnGap: tokens.spacingHorizontalXS, + rowGap: 0, + paddingRight: 0, + position: 'relative', + // Remove the border styles from react-input + ...shorthands.border('0'), + isolation: 'isolate', + + // Apply border styles on the ::before pseudo element. + // We cannot use ::after since react-input uses that + // for the selector styles. + // Using the pseudo element allows us to place the border + // above content in the component which ensures the buttons + // line up visually with the border as expected. Without this + // there is a bit of a gap which can become very noticeable + // at high zoom or when OS zoom levels are not divisible by 2 + // (e.g., 150% on Windows in Firefox) + // This is most noticeable on the "outline" appearance which is + // also the default so it feels worth the extra ceremony to get right. + '::before': { + content: '""', + boxSizing: 'border-box', + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + ...shorthands.borderRadius(tokens.borderRadiusMedium), + pointerEvents: 'none', + zIndex: 10, + }, + + '::after': { + right: 0, + bottom: 0, + left: 0, + zIndex: 20, + }, + }, + + outline: { + '::before': { + ...shorthands.border('1px', 'solid', tokens.colorNeutralStroke1), + borderBottomColor: tokens.colorNeutralStrokeAccessible, + }, + }, + + outlineInteractive: { + ':hover': { + '::before': { + ...shorthands.borderColor(tokens.colorNeutralStroke1Hover), + borderBottomColor: tokens.colorNeutralStrokeAccessibleHover, + }, + }, + // DO NOT add a space between the selectors! It changes the behavior of make-styles. + ':active,:focus-within': { + '::before': { + ...shorthands.borderColor(tokens.colorNeutralStroke1Pressed), + borderBottomColor: tokens.colorNeutralStrokeAccessiblePressed, + }, + }, + }, + + underline: { + '::before': { + ...shorthands.borderRadius(0), // corners look strange if rounded + ...shorthands.borderBottom('1px', 'solid', tokens.colorNeutralStrokeAccessible), + }, + }, + + underlineInteractive: { + ':hover': { + '::before': { + borderBottomColor: tokens.colorNeutralStrokeAccessibleHover, + }, + }, + // DO NOT add a space between the selectors! It changes the behavior of make-styles. + ':active,:focus-within': { + '::before': { + borderBottomColor: tokens.colorNeutralStrokeAccessiblePressed, + }, + }, + }, + + filled: { + '::before': { + ...shorthands.border('1px', 'solid', tokens.colorTransparentStroke), + }, + }, + + filledInteractive: { + // DO NOT add a space between the selectors! It changes the behavior of make-styles. + ':hover,:focus-within': { + '::before': { + // also handles pressed border color (:active) + ...shorthands.borderColor(tokens.colorTransparentStrokeInteractive), + }, + }, + }, + + disabled: { + '@media (forced-colors: active)': { + ...shorthands.borderColor('GrayText'), + }, + }, + + outlineDisabled: { + '::before': { + ...shorthands.border('1px', 'solid', tokens.colorNeutralStrokeDisabled), + ...shorthands.borderRadius(tokens.borderRadiusMedium), // because underline doesn't usually have a radius + }, + }, + + underlineDisabled: { + '::before': { + ...shorthands.borderBottom('1px', 'solid', tokens.colorTransparentStrokeDisabled), + }, + }, + + filledDisabled: { + '::before': { + ...shorthands.border('1px', 'solid', tokens.colorTransparentStrokeDisabled), + }, + }, +}); + +const useInputStyles = makeStyles({ + base: { + gridColumnStart: '1', + gridColumnEnd: '2', + gridRowStart: '1', + gridRowEnd: '3', + outlineStyle: 'none', + ...shorthands.padding(0), + }, +}); + +const useButtonStyles = makeStyles({ + base: { + display: 'inline-flex', + width: '24px', + alignItems: 'center', + justifyContent: 'center', + ...shorthands.border(0), + position: 'absolute', + + outlineStyle: 'none', + height: '100%', + + ':enabled:hover': { + cursor: 'pointer', + }, + + ':active': { + outlineStyle: 'none', + }, + + ':disabled': { + cursor: 'not-allowed', + }, + }, + + incrementButton: { + gridColumnStart: '2', + gridColumnEnd: '3', + gridRowStart: '1', + gridRowEnd: '2', + ...shorthands.borderRadius(0, tokens.borderRadiusMedium, 0, 0), + }, + + // TODO: revisit these padding numbers for aligning the icon. + // Padding values aren't perfect. + // The icon doesn't align perfectly with the Figma designs. + // It's set in a 16x16px square but the artwork is inset from that + // so I've had to compute the numbers by handle. + // Additionally the design uses fractional values so these are + // rounded to the nearest integer. + incrementButtonSmall: { + ...shorthands.padding('3px', '5px', '0px', '5px'), + }, + + incrementButtonMedium: { + ...shorthands.padding('4px', '5px', '1px', '5px'), + }, + + decrementButton: { + gridColumnStart: '2', + gridColumnEnd: '3', + gridRowStart: '2', + gridRowEnd: '3', + ...shorthands.borderRadius(0, 0, tokens.borderRadiusMedium, 0), + }, + + decrementButtonSmall: { + ...shorthands.padding('0px', '5px', '3px', '5px'), + }, + + decrementButtonMedium: { + ...shorthands.padding('1px', '5px', '4px', '5px'), + }, + + outline: { + backgroundColor: 'transparent', + color: tokens.colorNeutralForeground3, + ':enabled': { + ':hover': { + color: tokens.colorNeutralForeground3Hover, + backgroundColor: tokens.colorSubtleBackgroundHover, + }, + ':active': { + color: tokens.colorNeutralForeground3Pressed, + backgroundColor: tokens.colorSubtleBackgroundPressed, + }, + [`&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForeground3Pressed, + backgroundColor: tokens.colorSubtleBackgroundPressed, + }, + }, + ':disabled': { + color: tokens.colorNeutralForegroundDisabled, + }, + }, + + underline: { + backgroundColor: 'transparent', + color: tokens.colorNeutralForeground3, + ':enabled': { + ':hover': { + color: tokens.colorNeutralForeground3Hover, + backgroundColor: tokens.colorSubtleBackgroundHover, + }, + ':active': { + color: tokens.colorNeutralForeground3Pressed, + backgroundColor: tokens.colorSubtleBackgroundPressed, + }, + [`&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForeground3Pressed, + backgroundColor: tokens.colorSubtleBackgroundPressed, + }, + }, + ':disabled': { + color: tokens.colorNeutralForegroundDisabled, + }, + }, + 'filled-darker': { + backgroundColor: 'transparent', + color: tokens.colorNeutralForeground3, + + ':enabled': { + ':hover': { + color: tokens.colorNeutralForeground3Hover, + backgroundColor: tokens.colorNeutralBackground3Hover, + }, + ':active': { + color: tokens.colorNeutralForeground3Pressed, + backgroundColor: tokens.colorNeutralBackground3Pressed, + }, + [`&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForeground3Pressed, + backgroundColor: tokens.colorNeutralBackground3Pressed, + }, + }, + ':disabled': { + color: tokens.colorNeutralForegroundDisabled, + }, + }, + 'filled-lighter': { + backgroundColor: 'transparent', + color: tokens.colorNeutralForeground3, + + ':enabled': { + ':hover': { + color: tokens.colorNeutralForeground3Hover, + backgroundColor: tokens.colorNeutralBackground1Hover, + }, + [`:active,&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForeground3Pressed, + backgroundColor: tokens.colorNeutralBackground1Pressed, + }, + }, + ':disabled': { + color: tokens.colorNeutralForegroundDisabled, + }, + }, +}); + +// Cannot just disable button as they need to remain +// exposed to ATs like screen readers. +const useButtonDisabledStyles = makeStyles({ + base: { + cursor: 'not-allowed', + + ':hover': { + cursor: 'not-allowed', + }, + }, + + outline: { + color: tokens.colorNeutralForegroundDisabled, + ':enabled': { + ':hover': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + ':active': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + [`&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + }, + }, + + underline: { + color: tokens.colorNeutralForegroundDisabled, + ':enabled': { + ':hover': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + ':active': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + [`&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + }, + }, + + 'filled-darker': { + color: tokens.colorNeutralForegroundDisabled, + ':enabled': { + ':hover': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + ':active': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + [`&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + }, + }, + + 'filled-lighter': { + color: tokens.colorNeutralForegroundDisabled, + ':enabled': { + ':hover': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + ':active': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + [`&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + }, + }, +}); + +/** + * Apply styling to the SpinButton slots based on the state + */ +export const useSpinButtonStyles_unstable = (state: SpinButtonState): SpinButtonState => { + const { appearance, atBound, spinState, size } = state; + const disabled = state.input.disabled; + const filled = appearance.startsWith('filled'); + + const rootStyles = useRootStyles(); + const buttonStyles = useButtonStyles(); + const buttonDisabledStyles = useButtonDisabledStyles(); + const inputStyles = useInputStyles(); + + // Grab the root className here so we can be sure to merge is last + const rootClassName = state.root.className; + state.root.className = undefined; + // Reuse react-input's styles without re-using the Input component. + useInputStyles_unstable({ + size, + appearance, + input: state.input, + root: state.root, + components: { + root: 'span', + input: 'input', + contentBefore: 'span', + contentAfter: 'span', + }, + }); + + state.root.className = mergeClasses( + state.root.className, // Get the classes from useInputStyles_unstable + spinButtonClassNames.root, + rootStyles.base, + appearance === 'outline' && rootStyles.outline, + appearance === 'underline' && rootStyles.underline, + filled && rootStyles.filled, + !disabled && appearance === 'outline' && rootStyles.outlineInteractive, + !disabled && appearance === 'underline' && rootStyles.underlineInteractive, + !disabled && filled && rootStyles.filledInteractive, + disabled && rootStyles.disabled, + disabled && appearance === 'outline' && rootStyles.outlineDisabled, + disabled && appearance === 'underline' && rootStyles.underlineDisabled, + disabled && filled && rootStyles.filledDisabled, + rootClassName, // Make sure any original class name is applied last + ); + + state.incrementButton.className = mergeClasses( + spinButtonClassNames.incrementButton, + spinState === 'up' && `${spinButtonExtraClassNames.buttonActive}`, + buttonStyles.base, + buttonStyles.incrementButton, + buttonStyles[appearance], + size === 'small' ? buttonStyles.incrementButtonSmall : buttonStyles.incrementButtonMedium, + (atBound === 'max' || atBound === 'both') && buttonDisabledStyles.base, + (atBound === 'max' || atBound === 'both') && buttonDisabledStyles[appearance], + state.incrementButton.className, + ); + state.decrementButton.className = mergeClasses( + spinButtonClassNames.decrementButton, + spinState === 'down' && `${spinButtonExtraClassNames.buttonActive}`, + buttonStyles.base, + buttonStyles.decrementButton, + buttonStyles[appearance], + size === 'small' ? buttonStyles.decrementButtonSmall : buttonStyles.decrementButtonMedium, + (atBound === 'min' || atBound === 'both') && buttonDisabledStyles.base, + (atBound === 'min' || atBound === 'both') && buttonDisabledStyles[appearance], + state.decrementButton.className, + ); + + state.input.className = mergeClasses(spinButtonClassNames.input, state.input.className, inputStyles.base); + + return state; +}; + +const SpinButtonNoPseudos: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useSpinButton_unstable(props, ref); + + useSpinButtonStyles_unstable(state); + + return renderSpinButton_unstable(state); +}) as ForwardRefComponent; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/spinButton/vanilla.tsx b/apps/stress-test/src/renderers/v9/spinButton/vanilla.tsx new file mode 100644 index 00000000000000..02451c886d9f03 --- /dev/null +++ b/apps/stress-test/src/renderers/v9/spinButton/vanilla.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { ReactSelectorTreeComponentRenderer } from '../../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/textarea.tsx b/apps/stress-test/src/renderers/v9/textarea.tsx new file mode 100644 index 00000000000000..74e67548d06c9f --- /dev/null +++ b/apps/stress-test/src/renderers/v9/textarea.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { Textarea } from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return