Skip to content

Commit

Permalink
Added: Set head data of Experiment page based on name, description an…
Browse files Browse the repository at this point in the history
…d its theme's image (#1285)

* refactor: Regard Block page as having a "Block" type in its structured data

* feat: Add `useHeadDataFromExperiment` hook to set head data in Experiment component

* test: Add tests for `useHeadDataFromExperiment` hook

* type: Add `Experiment` type to `useExperiment hook`

* type: Refactor useHeadDataFromExperiment to accept null as experiment parameter
  • Loading branch information
drikusroor authored Oct 1, 2024
1 parent 4ab20d4 commit 4061f91
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 4 deletions.
3 changes: 2 additions & 1 deletion frontend/src/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import IBlock from "@/types/Block";
import IExperiment from "@/types/Experiment";
import Participant, { ParticipantLink } from "./types/Participant";
import Session from "./types/Session";
import Experiment from "@/types/Experiment";

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

Expand Down Expand Up @@ -48,7 +49,7 @@ export const useBlock = (slug: string): [IBlock | null, boolean] =>
useGet<IBlock>(API_BASE_URL + URLS.block.get(slug));

export const useExperiment = (slug: string) => {
const data = useGet(API_BASE_URL + URLS.experiment.get(slug));
const data = useGet<Experiment>(API_BASE_URL + URLS.experiment.get(slug));
return data;
}

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Block/Block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ const Block = () => {
setHeadData({
title: block.name,
description: block.description,
image: block.image?.file,
image: block.image?.file ?? "",
url: window.location.href,
structuredData: {
"@type": "Experiment",
"@type": "Block",
},
});

Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/Experiment/Experiment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ExperimentAbout from "./ExperimentAbout/ExperimentAbout";
import ExperimentDashboard from "./ExperimentDashboard/ExperimentDashboard";
import IExperiment from "@/types/Experiment";
import Redirect from "@/components/Redirect/Redirect";
import useHeadDataFromExperiment from "@/hooks/useHeadDataFromExperiment";

const Experiment = () => {
const { slug } = useParams();
Expand All @@ -23,12 +24,16 @@ const Experiment = () => {
const [hasShownConsent, setHasShownConsent] = useState(false);
const participant = useBoundStore((state) => state.participant);
const setTheme = useBoundStore((state) => state.setTheme);
const setHeadData = useBoundStore((state) => state.setHeadData);
const resetHeadData = useBoundStore((state) => state.resetHeadData);
const participantIdUrl = participant?.participant_id_url;
const nextBlock = experiment?.nextBlock;
const displayDashboard = experiment?.dashboard.length;
const showConsent = experiment?.consent;
const totalScore = experiment?.totalScore;

useHeadDataFromExperiment(experiment, setHeadData, resetHeadData);

if (experiment?.theme) {
setTheme(experiment.theme);
}
Expand Down
69 changes: 69 additions & 0 deletions frontend/src/hooks/useHeadDataFromExperiment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { renderHook } from '@testing-library/react';
import { vi, describe, beforeEach, test, expect } from 'vitest';
import useHeadDataFromExperiment from './useHeadDataFromExperiment';
import IExperiment from '@/types/Experiment';

describe('useHeadDataFromExperiment', () => {
const mockSetHeadData = vi.fn();
const mockResetHeadData = vi.fn();

beforeEach(() => {
vi.resetAllMocks();
// Mock window.location
Object.defineProperty(window, 'location', {
value: { href: 'https://example.com/experiment' },
writable: true
});
});

test('should set head data when experiment is provided', () => {
const mockExperiment: IExperiment = {
name: 'Test Experiment',
description: '<p>This is a <strong>test</strong> description</p>',
theme: {
logo: {
file: '/test-logo.jpg'
}
}
} as IExperiment;

renderHook(() => useHeadDataFromExperiment(mockExperiment, mockSetHeadData, mockResetHeadData));

expect(mockSetHeadData).toHaveBeenCalledWith({
title: 'Test Experiment',
description: 'This is a test description',
image: '/test-logo.jpg',
url: 'https://example.com/experiment',
structuredData: {
"@type": "Experiment",
},
});
});

test('should use default image when theme logo is not provided', () => {
const mockExperiment: IExperiment = {
name: 'Test Experiment',
description: 'Test description',
} as IExperiment;

renderHook(() => useHeadDataFromExperiment(mockExperiment, mockSetHeadData, mockResetHeadData));

expect(mockSetHeadData).toHaveBeenCalledWith(expect.objectContaining({
image: "/images/aml-logolarge-1200x630.jpg",
}));
});

test('should not set head data when experiment is null', () => {
renderHook(() => useHeadDataFromExperiment(null, mockSetHeadData, mockResetHeadData));

expect(mockSetHeadData).not.toHaveBeenCalled();
});

test('should reset head data on unmount', () => {
const { unmount } = renderHook(() => useHeadDataFromExperiment({} as IExperiment, mockSetHeadData, mockResetHeadData));

unmount();

expect(mockResetHeadData).toHaveBeenCalled();
});
});
30 changes: 30 additions & 0 deletions frontend/src/hooks/useHeadDataFromExperiment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useEffect } from 'react';
import IExperiment from "@/types/Experiment";
import { DocumentHeadSlice } from '@/util/stores';

const useHeadDataFromExperiment = (experiment: IExperiment | null, setHeadData: DocumentHeadSlice['setHeadData'], resetHeadData: DocumentHeadSlice['resetHeadData']) => {
useEffect(() => {
if (experiment) {
// De-htmlify the description's HTML string
const descriptionDiv = document.createElement("div");
descriptionDiv.innerHTML = experiment.description;
const description = descriptionDiv.textContent || '';

setHeadData({
title: experiment.name,
description: description,
image: experiment.theme?.logo?.file ?? "/images/aml-logolarge-1200x630.jpg",
url: window.location.href,
structuredData: {
"@type": "Experiment",
},
});
}

return () => {
resetHeadData();
};
}, [experiment, setHeadData, resetHeadData]);
};

export default useHeadDataFromExperiment;
2 changes: 1 addition & 1 deletion frontend/src/util/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ interface HeadData {
structuredData: Partial<StructuredData>;
}

interface DocumentHeadSlice {
export interface DocumentHeadSlice {
headData: HeadData;
setHeadData: (headData: HeadData) => void;
patchHeadData: (headData: Partial<HeadData>) => void;
Expand Down

0 comments on commit 4061f91

Please sign in to comment.