Skip to content

Commit 7afecdb

Browse files
committed
Add login functionality
1 parent 8aebe07 commit 7afecdb

File tree

3 files changed

+100
-7
lines changed

3 files changed

+100
-7
lines changed

src/Experiment.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { act, render, screen } from '@testing-library/react';
2+
3+
import { Experiment } from './Experiment.js';
4+
5+
test('Login component is rendered', async () => {
6+
await act(async () => {
7+
render(
8+
<Experiment loginOptions={{
9+
loginType: 'login',
10+
loginComponent: <p>Hello</p>,
11+
}}
12+
>
13+
<p>World</p>
14+
</Experiment>,
15+
);
16+
});
17+
expect(screen.queryByText('Hello')).not.toBeNull();
18+
expect(screen.queryByText('World')).toBeNull();
19+
});
20+
21+
test('Children are immediately displayed if login set to "skip"', async () => {
22+
await act(async () => {
23+
render(
24+
<Experiment loginOptions={{
25+
loginType: 'login',
26+
loginComponent: <p>Hello</p>,
27+
}}
28+
>
29+
<p>World</p>
30+
</Experiment>,
31+
);
32+
});
33+
expect(screen.queryByText('Hello')).not.toBeNull();
34+
});

src/Experiment.tsx

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createContext, useCallback, useEffect, useMemo, useReducer, useRef, use
22
import { genUserIdDefault } from './utils.js';
33
import { Result, db } from './database.js';
44
import { DefaultEndScreen, renderDefaultErrorScreen } from './defaults.js';
5+
import { LoginOptions } from './login.js';
56
import { createRoot } from 'react-dom/client';
67

78
let errorHandlerEffectRun = false;
@@ -14,6 +15,10 @@ interface ExperimentInternals {
1415
addResult: (taskId: string, screenId: string, key: string, val: string) => void;
1516
}
1617

18+
interface ExperimentControls {
19+
login: (userId: string) => void;
20+
}
21+
1722
const ExperimentInternalsDefault: ExperimentInternals = {
1823
currentTask: '',
1924
registerTask: () => { throw new Error('Experiment ancestor component not found.'); },
@@ -22,10 +27,15 @@ const ExperimentInternalsDefault: ExperimentInternals = {
2227
addResult: () => { throw new Error('Experiment ancestor component not found.'); },
2328
};
2429

30+
const ExperimentControlsDefault: ExperimentControls = {
31+
login: () => { throw new Error('Experiment ancestor component not found.'); },
32+
};
33+
2534
const ExperimentInternalsContext = createContext(ExperimentInternalsDefault);
35+
const ExperimentControlsContext = createContext(ExperimentControlsDefault);
2636

2737
type ExperimentProps = {
28-
genUserId?: () => Promise<string>;
38+
loginOptions?: LoginOptions;
2939
onResultAdded?: (result: Result) => void;
3040
endScreen?: React.ReactNode;
3141
useErrorHandling?: boolean;
@@ -34,10 +44,10 @@ type ExperimentProps = {
3444
};
3545

3646
function Experiment({
47+
loginOptions = { loginType: 'skip', loginComponent: <> </> },
3748
endScreen = <DefaultEndScreen />,
3849
useErrorHandling = false,
3950
renderErrorScreen = renderDefaultErrorScreen,
40-
genUserId = genUserIdDefault,
4151
...otherProps
4252
}: ExperimentProps) {
4353
// valid user ID must not be empty
@@ -48,10 +58,10 @@ function Experiment({
4858
const [, forceUpdate] = useReducer(x => x + 1, 0);
4959

5060
useEffect(() => {
51-
if (userId == '') {
52-
genUserId()
61+
if (loginOptions.loginType == 'skip' && userId == '') {
62+
genUserIdDefault()
5363
.then((id) => {
54-
setUserId(id);
64+
login(id);
5565
});
5666
}
5767
if (useErrorHandling && !errorHandlerEffectRun) {
@@ -70,6 +80,7 @@ function Experiment({
7080
);
7181
document.body.appendChild(newDiv);
7282
}
83+
7384
const registerTask = useCallback((id: string) => {
7485
// no duplicate IDs allowed
7586
if (allTasksRef.current.includes(id)) {
@@ -129,11 +140,32 @@ function Experiment({
129140
addResult,
130141
}), [taskRef.current, registerTask, unregisterTask, advance, addResult]);
131142

143+
const login = useCallback((userId: string) => {
144+
setUserId(userId);
145+
}, [setUserId]);
146+
147+
const experimentControls = useMemo(() => ({
148+
login,
149+
}), [login]);
150+
151+
let toDisplay;
152+
if (ended) {
153+
toDisplay = endScreen;
154+
}
155+
else if (userId == '') {
156+
toDisplay = loginOptions.loginComponent;
157+
}
158+
else {
159+
toDisplay = otherProps.children;
160+
}
161+
132162
return (
133163
<ExperimentInternalsContext.Provider value={experimentInternals}>
134-
{ended ? endScreen : otherProps.children}
164+
<ExperimentControlsContext.Provider value={experimentControls}>
165+
{toDisplay}
166+
</ExperimentControlsContext.Provider>
135167
</ExperimentInternalsContext.Provider>
136168
);
137169
};
138170

139-
export { ExperimentInternalsContext, ExperimentProps, Experiment};
171+
export { ExperimentInternalsContext, ExperimentControlsContext, ExperimentProps, Experiment };

src/login.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
type LoginOptions = {
2+
/**
3+
* The type of login to use. If 'skip', a random user ID will be generated and used.
4+
*/
5+
loginType: 'skip' | 'login';
6+
/**
7+
* The component to display for login. If loginType is 'skip', this is ignored.
8+
*
9+
* In the following example, the login component displays a button that always logs
10+
* in the user with the ID 'myUserId' when clicked:
11+
*
12+
* ```tsx
13+
* function MyLoginComponent() {
14+
* const { login } = useContext(ExperimentControlsContext);
15+
*
16+
* return (
17+
* <button onClick={() => login('myUserId')}>Click me to log in!</button>
18+
* );
19+
* }
20+
* ```
21+
*
22+
* In a real-world application, you would likely want each user to get a unique ID.
23+
*/
24+
loginComponent: React.ReactNode;
25+
};
26+
27+
export { LoginOptions };

0 commit comments

Comments
 (0)