Laurent Nguyen bcd8b7229f
Upgrade typescript to 4.9.5 and jest to 29.7.0 (and related packages) (#1884)
* Upgrade typescript to 4.9.5

* Fix compile issue and put back files in tsconfig.strict.json

* Update test snapshots

* Fix jest tests by upgrading jest and other related packages.

* Attempt to fix playwright test

* Revert "Attempt to fix playwright test"

This reverts commit 8293f34c9ce3901eb160a56526c59e3d3e1e343e.

* 2nd attempt to fix example test

* fix waitFor in playwright

* Remove unused describe section

* Attempt to fix e2e test

* Revert "Attempt to fix e2e test"

This reverts commit 9745bcd2efccf92e33e62e4563349cb76e6236de.

* Upgrade playwright to latest

* Revert "Upgrade playwright to latest"

This reverts commit e2ea1d01895f9db157a92a5167a7865b7cc43266.

* Error test on e2e

* Revert "Error test on e2e"

This reverts commit 124e3764f72fff70df9cc1a2e93d12faa7b507e9.

* Try to select dropdown item by xpath selector

* Revert "Try to select dropdown item by xpath selector"

This reverts commit 8eb42a64e23cee589a7c0a809c039ecc7867b7ba.

* Attempt to wait until page is fully loaded

* Revert "Attempt to wait until page is fully loaded"

This reverts commit bb43fcea6e71e593f7d1a3e374e9f1d57052ab70.

* Use playwright selectOption to select dropdown option

* Revert "Use playwright selectOption to select dropdown option"

This reverts commit daa8cd09304f68093df2d94f4bd1139cb367465a.

* Select dropdown option with playwright api instead of manual click

* c7ab4c7ecf7b05f32a85568bce1a667ad8c62703Revert "Select dropdown option with playwright api instead of manual click"

This reverts commit c7ab4c7ecf7b05f32a85568bce1a667ad8c62703.

* Wait for 5s after dropdown click

* Revert "Wait for 5s after dropdown click"

This reverts commit 847e9ad33fbfac99a947fc088e1c8c1c14c57853.

* Try forcing click

* Revert "Try forcing click"

This reverts commit 29b9fa1bda18dd3aba7853b20c6215d971bc826a.

* Force click on the dropdown and set viewport size bigger.

* Force click on the dropdown and set viewport size bigger.

* try force clicking option

* Skip container test on webkit

* Add branded browsers to e2e tests

---------

Co-authored-by: Ashley Stanton-Nurse <ashleyst@microsoft.com>
2024-07-30 15:41:41 -07:00

413 lines
13 KiB
TypeScript

import {
Dropdown,
IDropdownOption,
IStackTokens,
Label,
Link,
MessageBar,
MessageBarType,
Position,
Slider,
SpinButton,
Stack,
Text,
TextField,
Toggle,
} from "@fluentui/react";
import * as React from "react";
import {
ChoiceItem,
Description,
DescriptionType,
Info,
InputType,
InputTypeValue,
NumberUiType,
SmartUiInput,
} from "../../../SelfServe/SelfServeTypes";
import { ToolTipLabelComponent } from "../Settings/SettingsSubComponents/ToolTipLabelComponent";
import * as InputUtils from "./InputUtils";
import "./SmartUiComponent.less";
/**
* Generic UX renderer
* It takes:
* - a JSON object as data
* - a Map of callbacks
* - a descriptor of the UX.
*/
interface BaseDisplay {
labelTKey: string;
dataFieldName: string;
errorMessage?: string;
type: InputTypeValue;
}
interface BaseInput extends BaseDisplay {
placeholderTKey?: string;
}
/**
* For now, this only supports integers
*/
interface NumberInput extends BaseInput {
min: number;
max: number;
step: number;
defaultValue?: number;
uiType: NumberUiType;
}
interface BooleanInput extends BaseInput {
trueLabelTKey: string;
falseLabelTKey: string;
defaultValue?: boolean;
}
interface StringInput extends BaseInput {
defaultValue?: string;
}
interface ChoiceInput extends BaseInput {
choices: ChoiceItem[];
defaultKey?: string;
}
interface DescriptionDisplay extends BaseDisplay {
description?: Description;
isDynamicDescription?: boolean;
}
type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
interface Node {
id: string;
info?: Info;
input?: AnyDisplay;
children?: Node[];
}
export interface SmartUiDescriptor {
root: Node;
}
/************************** Component implementation starts here ************************************* */
export interface SmartUiComponentProps {
descriptor: SmartUiDescriptor;
currentValues: Map<string, SmartUiInput>;
onInputChange: (input: AnyDisplay, newValue: InputType) => void;
onError: (hasError: boolean) => void;
disabled: boolean;
getTranslation: (messageKey: string, namespace?: string) => string;
}
interface SmartUiComponentState {
errors: Map<string, string>;
}
export class SmartUiComponent extends React.Component<SmartUiComponentProps, SmartUiComponentState> {
private shouldCheckErrors = true;
private static readonly labelStyle = {
color: "#393939",
fontFamily: "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
fontSize: 12,
};
componentDidUpdate(): void {
if (!this.shouldCheckErrors) {
this.shouldCheckErrors = true;
return;
}
this.props.onError(this.state.errors.size > 0);
this.shouldCheckErrors = false;
}
constructor(props: SmartUiComponentProps) {
super(props);
this.state = {
errors: new Map(),
};
}
private renderInfo(info: Info): JSX.Element {
return (
info && (
<Text>
{this.props.getTranslation(info.messageTKey)}{" "}
{info.link && (
<Link href={info.link.href} target="_blank">
{this.props.getTranslation(info.link.textTKey)}
</Link>
)}
</Text>
)
);
}
private renderTextInput(input: StringInput, labelId: string, labelElement: JSX.Element): JSX.Element {
const value = this.props.currentValues.get(input.dataFieldName)?.value as string;
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
return (
<Stack>
{labelElement}
<TextField
id={`${input.dataFieldName}-textField-input`}
aria-labelledby={labelId}
type="text"
value={value || ""}
placeholder={this.props.getTranslation(input.placeholderTKey)}
disabled={disabled}
onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
styles={{
root: { width: 400 },
}}
/>
</Stack>
);
}
private renderDescription(input: DescriptionDisplay, labelId: string, labelElement: JSX.Element): JSX.Element {
const dataFieldName = input.dataFieldName;
const description = input.description || (this.props.currentValues.get(dataFieldName)?.value as Description);
if (!description) {
if (!input.isDynamicDescription) {
return this.renderError("Description is not provided.");
}
// If input is a dynamic description and description is not available, empty element is rendered
return <></>;
}
const descriptionElement = (
<Stack>
{labelElement}
<Text id={`${dataFieldName}-text-display`} aria-labelledby={labelId} style={{ whiteSpace: "pre-line" }}>
{this.props.getTranslation(description.textTKey)}{" "}
{description.link && (
<Link target="_blank" href={description.link.href}>
{this.props.getTranslation(description.link.textTKey)}
</Link>
)}
</Text>
</Stack>
);
if (description.type === DescriptionType.Text) {
return descriptionElement;
}
const messageBarType =
description.type === DescriptionType.InfoMessageBar ? MessageBarType.info : MessageBarType.warning;
return <MessageBar messageBarType={messageBarType}>{descriptionElement}</MessageBar>;
}
private clearError(dataFieldName: string): void {
const { errors } = this.state;
errors.delete(dataFieldName);
this.setState({ errors });
}
private onValidate = (input: NumberInput, value: string, min: number, max: number): string => {
const newValue = InputUtils.onValidateValueChange(value, min, max);
const dataFieldName = input.dataFieldName;
if (newValue) {
this.props.onInputChange(input, newValue);
this.clearError(dataFieldName);
return newValue.toString();
} else {
const { errors } = this.state;
errors.set(dataFieldName, `Invalid value '${value}'. It must be between ${min} and ${max}`);
this.setState({ errors });
}
return undefined;
};
private onIncrement = (input: NumberInput, value: string, step: number, max: number): string => {
const newValue = InputUtils.onIncrementValue(value, step, max);
const dataFieldName = input.dataFieldName;
if (newValue) {
this.props.onInputChange(input, newValue);
this.clearError(dataFieldName);
return newValue.toString();
}
return undefined;
};
private onDecrement = (input: NumberInput, value: string, step: number, min: number): string => {
const newValue = InputUtils.onDecrementValue(value, step, min);
const dataFieldName = input.dataFieldName;
if (newValue) {
this.props.onInputChange(input, newValue);
this.clearError(dataFieldName);
return newValue.toString();
}
return undefined;
};
private renderNumberInput(input: NumberInput, labelId: string, labelElement: JSX.Element): JSX.Element {
const { labelTKey, min, max, dataFieldName, step } = input;
const props = {
min: min,
max: max,
ariaLabel: this.props.getTranslation(labelTKey),
step: step,
};
const value = this.props.currentValues.get(dataFieldName)?.value as number;
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
if (input.uiType === NumberUiType.Spinner) {
return (
<Stack>
{labelElement}
<Stack styles={{ root: { width: 400 } }} tokens={{ childrenGap: 2 }}>
<SpinButton
{...props}
id={`${input.dataFieldName}-spinner-input`}
value={value?.toString()}
onValidate={(newValue) => this.onValidate(input, newValue, props.min, props.max)}
onIncrement={(newValue) => this.onIncrement(input, newValue, props.step, props.max)}
onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)}
labelPosition={Position.top}
aria-labelledby={labelId}
disabled={disabled}
/>
{this.state.errors.has(dataFieldName) && (
<MessageBar messageBarType={MessageBarType.error}>
Error: {this.state.errors.get(dataFieldName)}
</MessageBar>
)}
</Stack>
</Stack>
);
} else if (input.uiType === NumberUiType.Slider) {
return (
<Stack>
{labelElement}
<div id={`${input.dataFieldName}-slider-input`}>
<Slider
{...props}
value={value}
disabled={disabled}
onChange={(newValue) => this.props.onInputChange(input, newValue)}
styles={{
root: { width: 400 },
valueLabel: SmartUiComponent.labelStyle,
}}
/>
</div>
</Stack>
);
} else {
return <>Unsupported number UI type {input.uiType}</>;
}
}
private renderBooleanInput(input: BooleanInput, labelId: string, labelElement: JSX.Element): JSX.Element {
const value = this.props.currentValues.get(input.dataFieldName)?.value as boolean;
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
return (
<Stack>
{labelElement}
<Toggle
id={`${input.dataFieldName}-toggle-input`}
aria-labelledby={labelId}
checked={value || false}
onText={this.props.getTranslation(input.trueLabelTKey)}
offText={this.props.getTranslation(input.falseLabelTKey)}
disabled={disabled}
onChange={(event, checked: boolean) => this.props.onInputChange(input, checked)}
styles={{ root: { width: 400 } }}
/>
</Stack>
);
}
private renderChoiceInput(input: ChoiceInput, labelId: string, labelElement: JSX.Element): JSX.Element {
const { defaultKey, dataFieldName, choices, placeholderTKey } = input;
const value = this.props.currentValues.get(dataFieldName)?.value as string;
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
let selectedKey = value ? value : defaultKey;
if (!selectedKey) {
selectedKey = "";
}
return (
<Stack>
{labelElement}
<Dropdown
id={`${input.dataFieldName}-dropdown-input`}
aria-labelledby={labelId}
selectedKey={selectedKey}
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
placeholder={this.props.getTranslation(placeholderTKey)}
disabled={disabled}
// Removed dropdownWidth="auto" as dropdown accept only number
options={choices.map((c) => ({
key: c.key,
text: this.props.getTranslation(c.labelTKey),
}))}
styles={{
root: { width: 400 },
dropdown: SmartUiComponent.labelStyle,
}}
/>
</Stack>
);
}
private renderError(errorMessage: string): JSX.Element {
return <MessageBar messageBarType={MessageBarType.error}>Error: {errorMessage}</MessageBar>;
}
private renderElement(input: AnyDisplay, info: Info): JSX.Element {
if (input.errorMessage) {
return this.renderError(input.errorMessage);
}
const inputHidden = this.props.currentValues.get(input.dataFieldName)?.hidden;
if (inputHidden) {
return <></>;
}
const labelId = `${input.dataFieldName}-label`;
const labelElement: JSX.Element = input.labelTKey && (
<Label id={labelId}>
<ToolTipLabelComponent
label={this.props.getTranslation(input.labelTKey)}
toolTipElement={this.renderInfo(info)}
/>
</Label>
);
return <Stack>{this.renderControl(input, labelId, labelElement)}</Stack>;
}
private renderControl(input: AnyDisplay, labelId: string, labelElement: JSX.Element): JSX.Element {
switch (input.type) {
case "string":
if ("description" in input || "isDynamicDescription" in input) {
return this.renderDescription(input as DescriptionDisplay, labelId, labelElement);
}
return this.renderTextInput(input as StringInput, labelId, labelElement);
case "number":
return this.renderNumberInput(input as NumberInput, labelId, labelElement);
case "boolean":
return this.renderBooleanInput(input as BooleanInput, labelId, labelElement);
case "object":
return this.renderChoiceInput(input as ChoiceInput, labelId, labelElement);
default:
throw new Error(`Unknown input type: ${input.type}`);
}
}
private renderNode(node: Node): JSX.Element {
const containerStackTokens: IStackTokens = { childrenGap: 10 };
return (
<Stack tokens={containerStackTokens} className="widgetRendererContainer">
<Stack.Item>{node.input && this.renderElement(node.input, node.info as Info)}</Stack.Item>
{node.children && node.children.map((child) => <div key={child.id}>{this.renderNode(child)}</div>)}
</Stack>
);
}
render(): JSX.Element {
return this.renderNode(this.props.descriptor.root);
}
}