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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/components-react/shared/inputs/ImagePickerInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ export const ImagePickerInput = InputComponent((p: TListInputProps<string>) => {
<div className={styles.imagePicker}>
{p.options?.map(opt => (
<div
data-name={p.value === opt.value ? 'image-option-active' : `image-option-${opt.value}`}
key={opt.value}
className={cx(styles.imageOption, p.value === opt.value && styles.active)}
className={cx(styles.imageOption, { [styles.active]: p.value === opt.value })}
onClick={() => p.onChange && p.onChange(opt.value)}
>
{typeof opt.image === 'string' ? <img src={opt.image} /> : opt.image}
Expand Down
37 changes: 32 additions & 5 deletions app/components-react/widgets/common/useWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as remote from '@electron/remote';
import { WidgetDefinitions, WidgetType } from '../../../services/widgets';
import { WidgetDefinitions, WidgetType, getWidgetName } from '../../../services/widgets';
import { Services } from '../../service-provider';
import { throttle } from 'lodash-decorators';
import { assertIsDefined, getDefined } from '../../../util/properties-type-guards';
Expand Down Expand Up @@ -138,10 +138,37 @@ export class WidgetModule<TWidgetState extends IWidgetState = IWidgetState> {

// load settings from the server to the store
this.state.type = widget.type;
const data = await this.fetchData();
this.setData(data);
this.setPrevSettings(data);
this.state.setIsLoading(false);

// Used for debugging if fetching widget data failed to identify if the issue came from the api
const startTime = performance.now();
const widgetName = getWidgetName(widget.type);
try {
const data = await this.fetchData();
this.setData(data);
this.setPrevSettings(data);
} catch (e: unknown) {
console.error(
`${widgetName} Error: fetch widget data rejected after ${Math.round(
performance.now() - startTime,
)}ms}`,
e,
);

// Alert the user that the widget failed to load settings so they can determine the follow-up action instead of
// automatically closing the window, which may be confusing and frustrating for the user.
alertAsync({
title: $t(
'Something went wrong while loading settings for %{widgetName}. Please reopen the settings window or re-add the widget.',
{
widgetName,
},
),
afterCloseFn: this.close,
});
} finally {
// Without this, a failed fetch will be stuck loading with an infinite spinner
this.state.setIsLoading(false);
}
}

destroy() {
Expand Down
3 changes: 2 additions & 1 deletion app/i18n/en-US/widgets.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,6 @@
"Goals": "Goals",
"Flair": "Flair",
"Charity": "Charity",
"Saving custom code can have potential security risks, make sure you trust the code you are about to apply.": "Saving custom code can have potential security risks, make sure you trust the code you are about to apply."
"Saving custom code can have potential security risks, make sure you trust the code you are about to apply.": "Saving custom code can have potential security risks, make sure you trust the code you are about to apply.",
"Something went wrong while loading settings for %{widgetName}. Please reopen the settings window or re-add the widget.": "Something went wrong while loading settings for %{widgetName}. Please reopen the settings window or re-add the widget."
}
9 changes: 9 additions & 0 deletions app/services/widgets/widgets-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -767,3 +767,12 @@ export const WidgetDisplayData = (platform?: string): { [x: number]: IWidgetDisp
supportedOS: [OS.Windows],
},
});

export function getWidgetName(widgetType: WidgetType): string {
const widget = WidgetDefinitions[widgetType];
if (!widget) {
console.error(`Unknown widget type: ${widgetType}`);
}

return widget?.name || $t('Widget');
}
2 changes: 1 addition & 1 deletion test/helpers/modules/forms/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export function useForm(name?: string) {
*/
async function getInputControllers() {
// wait for form to be visible
await waitForDisplayed(formSelector);
await waitForDisplayed(formSelector, { timeout: 15000 });
const $inputs = await getInputElements();
const controllers: BaseInputController<any>[] = [];
for (const $input of $inputs) {
Expand Down
12 changes: 10 additions & 2 deletions test/helpers/modules/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { setInputValue } from './forms/form';
import { dialogDismiss } from '../webdriver/dialog';
import { contextMenuClick } from '../webdriver/context-menu';
import { sleep } from '../sleep';

async function clickSourceAction(selector: string) {
const $el = await (await select('[data-name=sourcesControls]')).$(selector);
Expand Down Expand Up @@ -53,8 +54,8 @@ export async function openSourceProperties(name: string) {
export async function addSource(
type: string,
name: string,
closeProps = true,
audioSource = false,
closeProps: boolean = true,
waitForResponse: boolean = false,
) {
await focusMain();
await clickAddSource();
Expand Down Expand Up @@ -82,6 +83,13 @@ export async function addSource(
if (closeProps) {
await closeWindow('child');
} else {
// For some sources that require data to be returned from an async call before loading, such as the tip-jar widget,
// the source settings window may not be fully loaded when the `focusChild` call is made. Allow a delay in the test
// to wait for the call to complete instead of failing the test due to a stale handle.
if (waitForResponse) {
await sleep(1000); // Adjust the delay as needed
}

await focusChild();
}
}
Expand Down
14 changes: 9 additions & 5 deletions test/regular/widgets/tip-jar.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { test, useWebdriver } from '../../helpers/webdriver';
import { addSource } from '../../helpers/modules/sources';
import { waitForDisplayed, clickWhenDisplayed, closeWindow } from '../../helpers/modules/core';
import { logIn } from '../../helpers/webdriver/user';

// not a react hook
// eslint-disable-next-line react-hooks/rules-of-hooks
useWebdriver();

test('Set tip-jar settings', async t => {
if (!(await logIn(t))) return;

await addSource('The Jar', 'The Jar', false, true);
await clickWhenDisplayed('li=Jar Image', { timeout: 15000 });

const client = t.context.app.client;
await addSource( 'The Jar', '__The Jar', false);
const martiniGlass = '[src="https://cdn.streamlabs.com/static/tip-jar/jars/glass-martini.png"]';
const activeMartiniGlass =
'.active img[src="https://cdn.streamlabs.com/static/tip-jar/jars/glass-martini.png"]';
await (await client.$(martiniGlass)).waitForDisplayed();
await (await client.$(martiniGlass)).waitForDisplayed({ timeout: 15000 });
await (await client.$(martiniGlass)).click();
await (await client.$(activeMartiniGlass)).waitForDisplayed();
await waitForDisplayed('[data-name="image-option-active"]', { timeout: 15000 });
Comment thread
michelinewu marked this conversation as resolved.
await closeWindow('child');
t.pass();
});
Loading