Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: Integrate Round management and enhance Histogram component #1397

Open
wants to merge 17 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4653a03
feat: add RoundResponse type and integrate round management in Block …
drikusroor Nov 25, 2024
dbc849c
refactor: update Histogram component to use shouldRandomize logic and…
drikusroor Nov 25, 2024
fbe6b81
fix: add type annotation for Story in Histogram stories
drikusroor Nov 25, 2024
0d88a63
refactor: streamline Action type definition and update Block componen…
drikusroor Nov 25, 2024
23700c2
fix: add mock functions for setRound & setCurrentAction
drikusroor Nov 25, 2024
ac1d0bd
fix(test): fix Histogram tests with mock store and update timer inter…
drikusroor Nov 25, 2024
158e7dc
fix(test): add mock currentAction function in MatchingPairs tests
drikusroor Nov 25, 2024
739ce83
fix: update actions state and type to use Round in Block component
drikusroor Nov 25, 2024
7744b10
feat(story): add dynamic playback arguments and common decorator for …
drikusroor Nov 25, 2024
9b45e16
refactor: simplify component props by extending Action types in Expla…
drikusroor Nov 27, 2024
705d9f5
refactor: rename Action imports in Explainer, Final, Info, Score, and…
drikusroor Nov 27, 2024
f51656a
refactor: remove setRound and update currentAction handling in Block …
drikusroor Nov 27, 2024
123a38c
fix(test): update mock implementation of currentAction in Histogram t…
drikusroor Nov 27, 2024
4f8b4bb
fix(Block): ensure currentAction is reset to null on error and spread…
drikusroor Nov 27, 2024
817e8e0
refactor(stores): remove unused get parameter from createActionSlice
drikusroor Nov 27, 2024
4aa8dee
fix(package): update lint:fix script to include all JavaScript file t…
drikusroor Nov 27, 2024
970396b
refactor: clean up unused variables and improve dependencies in compo…
drikusroor Nov 27, 2024
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
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"storybook": "storybook dev -p 6006 --no-open",
"storybook:build": "storybook build",
"lint": "eslint src/**/*.{js,jsx,ts,tsx}",
"lint:fix": "eslint --fix src/**/*.js",
"lint:fix": "eslint --fix src/**/*.{js,jsx,ts,tsx}",
"build-storybook": "storybook build"
},
"eslintConfig": {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import IExperiment from "@/types/Experiment";
import Participant, { ParticipantLink } from "./types/Participant";
import Session from "./types/Session";
import Experiment from "@/types/Experiment";
import { RoundResponse } from "./types/Round";

// API handles the calls to the Hooked-server api

Expand Down Expand Up @@ -173,7 +174,7 @@ interface GetNextRoundParams {


// Get next_round from server
export const getNextRound = async ({ session }: GetNextRoundParams) => {
export const getNextRound = async ({ session }: GetNextRoundParams): Promise<RoundResponse | null> => {

const sessionId = session.id.toString();

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/AppBar/AppBar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, fireEvent } from '@testing-library/react';
import { render } from '@testing-library/react';
import AppBar from './AppBar';
import { BrowserRouter as Router } from 'react-router-dom';
import { vi, describe, beforeEach, it, expect } from 'vitest';
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/Block/Block.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ vi.mock('../../API', () => ({

vi.mock('../../util/stores', () => ({
__esModule: true,
default: (fn) => {
default: (fn: any) => {
const state = {
session: mockSessionStore,
participant: mockParticipantStore,
Expand All @@ -51,6 +51,7 @@ vi.mock('../../util/stores', () => ({
setHeadData: vi.fn(),
resetHeadData: vi.fn(),
setBlock: vi.fn(),
setCurrentAction: vi.fn(),
};

return fn(state);
Expand Down
53 changes: 20 additions & 33 deletions frontend/src/components/Block/Block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,20 @@ import classNames from "classnames";
import useBoundStore from "@/util/stores";
import { getNextRound, useBlock } from "@/API";
import DefaultPage from "@/components/Page/DefaultPage";
import Explainer, { ExplainerProps } from "@/components/Explainer/Explainer";
import Final, { FinalProps } from "@/components/Final/Final";
import Loading, { LoadingProps } from "@/components/Loading/Loading";
import Playlist, { PlaylistProps } from "@/components/Playlist/Playlist";
import Score, { ScoreProps } from "@/components/Score/Score";
import Trial, { TrialProps } from "@/components/Trial/Trial";
import Info, { InfoProps } from "@/components/Info/Info";
import Explainer from "@/components/Explainer/Explainer";
import Final from "@/components/Final/Final";
import Loading from "@/components/Loading/Loading";
import Playlist from "@/components/Playlist/Playlist";
import Score from "@/components/Score/Score";
import Trial from "@/components/Trial/Trial";
import Info from "@/components/Info/Info";
import FloatingActionButton from "@/components/FloatingActionButton/FloatingActionButton";
import UserFeedback from "@/components/UserFeedback/UserFeedback";
import FontLoader from "@/components/FontLoader/FontLoader";
import useResultHandler from "@/hooks/useResultHandler";
import Session from "@/types/Session";
import { RedirectProps } from "../Redirect/Redirect";

interface SharedActionProps {
title?: string;
config?: object;
style?: object;
}

type ActionProps = SharedActionProps &
(
| { view: "EXPLAINER" } & ExplainerProps
| { view: "INFO" } & InfoProps
| { view: "TRIAL_VIEW" } & TrialProps
| { view: 'SCORE' } & ScoreProps
| { view: 'FINAL' } & FinalProps
| { view: 'PLAYLIST' } & PlaylistProps
| { view: 'REDIRECT' } & RedirectProps
| { view: "LOADING" } & LoadingProps
)
import { Action } from "@/types/Action";
import { Round } from "@/types/Round";

// Block handles the main (experiment) block flow:
// - Loads the block and participant
Expand All @@ -46,7 +29,7 @@ type ActionProps = SharedActionProps &
// Empty URL parameter "participant_id" is the same as no URL parameter at all
const Block = () => {
const { slug } = useParams();
const startState = { view: "LOADING" } as ActionProps;
const startState = { view: "LOADING" } as Action;
// Stores
const setError = useBoundStore(state => state.setError);
const participant = useBoundStore((state) => state.participant);
Expand All @@ -56,36 +39,39 @@ const Block = () => {
const setTheme = useBoundStore((state) => state.setTheme);
const resetTheme = useBoundStore((state) => state.resetTheme);
const setBlock = useBoundStore((state) => state.setBlock);
const setCurrentAction = useBoundStore((state) => state.setCurrentAction);

const setHeadData = useBoundStore((state) => state.setHeadData);
const resetHeadData = useBoundStore((state) => state.resetHeadData);

// Current block state
const [actions, setActions] = useState([]);
const [state, setState] = useState<ActionProps | null>(startState);
const [actions, setActions] = useState<Round>([]);
const [state, setState] = useState<Action | null>(startState);
const [key, setKey] = useState<number>(Math.random());
const playlist = useRef(null);

// API hooks
const [block, loadingBlock] = useBlock(slug);
const [block, loadingBlock] = useBlock(slug!);

const loadingText = block ? block.loading_text : "";
const className = block ? block.class_name : "";

/** Set new state as spread of current state to force re-render */
const updateState = useCallback((state: ActionProps) => {
const updateState = useCallback((state: Action) => {
if (!state) return;

setState({ ...state });
setKey(Math.random());
}, []);

const updateActions = useCallback((currentActions: []) => {
const updateActions = useCallback((currentActions: Round) => {
const newActions = currentActions;
setActions(newActions);
const newState = newActions.shift();
const currentAction = newState ? newState : null;
setCurrentAction({ ...currentAction });
updateState(newState);
}, [updateState]);
}, [updateState, setCurrentAction]);

const continueToNextRound = async (activeSession: Session) => {
// Try to get next_round data from server
Expand All @@ -98,6 +84,7 @@ const Block = () => {
setError(
"An error occured while loading the data, please try to reload the page. (Error: next_round data unavailable)"
);
setCurrentAction(null);
setState(null);
}
};
Expand Down
3 changes: 0 additions & 3 deletions frontend/src/components/Experiment/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ export const Header: React.FC<HeaderProps> = ({
socialMediaConfig
}) => {

// Get current URL minus the query string
const currentUrl = window.location.href.split('?')[0];

return (
<div className="hero">
<div className="intro">
Expand Down
12 changes: 2 additions & 10 deletions frontend/src/components/Explainer/Explainer.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import { useEffect } from "react";
import Button from "../Button/Button";
import { Explainer as ExplainerAction } from "@/types/Action";

interface ExplainerStep {
number: number;
description: string;
}

export interface ExplainerProps {
instruction: string;
button_label: string;
steps?: Array<ExplainerStep>;
timer: number | null;
export interface ExplainerProps extends ExplainerAction {
onNext: () => void;
}

Expand Down
33 changes: 2 additions & 31 deletions frontend/src/components/Final/Final.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,10 @@ import useBoundStore from "../../util/stores";
import ParticipantLink from "../ParticipantLink/ParticipantLink";
import UserFeedback from "../UserFeedback/UserFeedback";
import FinalButton from "./FinalButton";
import ISocial from "@/types/Social";
import Block, { FeedbackInfo } from "@/types/Block";
import Participant from "@/types/Participant";
import { Final as FinalAction } from "@/types/Action";

export interface FinalProps {
block: Block;
participant: Participant;
score: number;
final_text: string | TrustedHTML;
action_texts: {
all_experiments: string;
profile: string;
play_again: string;
}
button: {
text: string;
link: string;
};
export interface FinalProps extends FinalAction {
onNext: () => void;
show_participant_link: boolean;
participant_id_only: boolean;
show_profile_link: boolean;
social: ISocial;
feedback_info?: FeedbackInfo;
points: string;
rank: {
class: string;
text: string;
}
logo: {
image: string;
link: string;
};
}

/**
Expand Down
29 changes: 19 additions & 10 deletions frontend/src/components/Histogram/Histogram.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, act, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach, Mock, } from 'vitest';
import { render, act } from '@testing-library/react';
import Histogram from './Histogram';

// Mock requestAnimationFrame and cancelAnimationFrame
Expand All @@ -14,9 +14,21 @@ vi.stubGlobal('cancelAnimationFrame', (handle: number): void => {
// Mock setInterval and clearInterval
vi.useFakeTimers();

vi.mock('../../util/stores', () => ({
__esModule: true,
default: (fn: any) => {
const state = {
currentAction: { playback: { play_method: 'BUFFER' } },
};

return fn(state);
},
useBoundStore: vi.fn()
}));

describe('Histogram', () => {
let mockAnalyser: {
getByteFrequencyData: vi.Mock;
getByteFrequencyData: Mock
};

beforeEach(() => {
Expand Down Expand Up @@ -154,11 +166,8 @@ describe('Histogram', () => {
it('updates bar heights based on random data when random is true and running is true', async () => {
const bars = 5;

// Ensure the analyser does not provide data
mockAnalyser.getByteFrequencyData.mockImplementation(() => { });

const { container, rerender } = render(
<Histogram running={true} bars={bars} random={true} />
<Histogram running={true} bars={bars} random={true} interval={200} />
);

const getHeights = () =>
Expand All @@ -168,17 +177,16 @@ describe('Histogram', () => {

const initialHeights = getHeights();

// Advance timers and trigger animation frame
// Advance timers by at least one interval
await act(async () => {
vi.advanceTimersByTime(100);
vi.advanceTimersByTime(200);
});

rerender(<Histogram running={true} bars={bars} random={true} />);

const updatedHeights = getHeights();

expect(initialHeights).not.to.deep.equal(updatedHeights);
expect(mockAnalyser.getByteFrequencyData).not.toHaveBeenCalled();
});

it('does not call getByteFrequencyData when random is true', async () => {
Expand Down Expand Up @@ -225,6 +233,7 @@ describe('Histogram', () => {

it('updates bar heights based on frequency data using requestAnimationFrame', async () => {
const bars = 5;

mockAnalyser.getByteFrequencyData.mockImplementation((array) => {
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256);
Expand Down
20 changes: 13 additions & 7 deletions frontend/src/components/Histogram/Histogram.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import useBoundStore from '@/util/stores';

interface HistogramProps {
bars?: number;
Expand Down Expand Up @@ -27,10 +28,15 @@ const Histogram: React.FC<HistogramProps> = ({
backgroundColor = undefined,
borderRadius = '0.15rem',
random = false,
interval = 100,
interval = 200,
}) => {
const [frequencyData, setFrequencyData] = useState<Uint8Array>(new Uint8Array(bars));

const currentAction = useBoundStore((state) => state.currentAction);
const isBuffer = currentAction?.playback?.play_method === 'BUFFER';

const shouldRandomize = random || !isBuffer;

const animationFrameRef = useRef<number>();
const intervalRef = useRef<number>();

Expand All @@ -50,7 +56,7 @@ const Histogram: React.FC<HistogramProps> = ({
const updateFrequencyData = () => {
let dataWithoutExtremes: Uint8Array;

if (random) {
if (shouldRandomize) {
// Generate random frequency data
dataWithoutExtremes = new Uint8Array(bars);
for (let i = 0; i < bars; i++) {
Expand All @@ -71,14 +77,14 @@ const Histogram: React.FC<HistogramProps> = ({
}
};

if (random) {
// Use setInterval when random is true
if (shouldRandomize) {
// Use setInterval when shouldRandomize is true
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
intervalRef.current = window.setInterval(updateFrequencyData, interval);
} else {
// Use requestAnimationFrame when random is false
// Use requestAnimationFrame when shouldRandomize is false
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
Expand All @@ -93,7 +99,7 @@ const Histogram: React.FC<HistogramProps> = ({
clearInterval(intervalRef.current);
}
};
}, [running, bars, random, interval]);
}, [running, bars, shouldRandomize, interval]);

const barWidth = `calc((100% - ${(bars - 1) * spacing}px) / ${bars})`;

Expand All @@ -120,7 +126,7 @@ const Histogram: React.FC<HistogramProps> = ({
height: `${(frequencyData[index] / 255) * 100}%`,
backgroundColor: 'currentColor',
marginRight: index < bars - 1 ? spacing : 0,
transition: random
transition: shouldRandomize
? `height ${interval / 1000}s ease`
: 'height 0.05s ease',
}}
Expand Down
7 changes: 2 additions & 5 deletions frontend/src/components/Info/Info.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { useEffect, useState } from "react";

import Button from "../Button/Button";
import { Info as InfoAction } from "@/types/Action";

export interface InfoProps {
heading?: string;
body: string | TrustedHTML;
button_label?: string;
button_link?: string;
export interface InfoProps extends InfoAction {
onNext?: () => void;
}

Expand Down
Loading