Skip to content

Commit

Permalink
react-experiment: change useLogger typing (#192)
Browse files Browse the repository at this point in the history
also adds test for useLogger
  • Loading branch information
QuentinRoy committed Nov 27, 2023
1 parent 669e424 commit 7696b2f
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 31 deletions.
5 changes: 5 additions & 0 deletions .changeset/pink-sloths-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lightmill/react-experiment': minor
---

Add log content in use logger typing with default values.
10 changes: 5 additions & 5 deletions packages/react-experiment/__tests__/run.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { act, fireEvent, render, screen } from '@testing-library/react';
import userEventPackage from '@testing-library/user-event';
import { Run, RunElements, useTask } from '../src/main.js';

const userEvent =
userEventPackage as unknown as typeof userEventPackage.default;
// @ts-expect-error - userEventPackage is not typed correctly
const userEvent: typeof userEventPackage.default = userEventPackage;

function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
Expand All @@ -26,11 +26,11 @@ describe('run', () => {

beforeEach(() => {
Task = ({ type, dataProp }) => {
let { task, onTaskCompleted } = useTask();
let { task, onTaskCompleted } = useTask(type);
return (
<div>
<h1>Type {type}</h1>
<p data-testid="data">{task[dataProp]}</p>
<h1>Type {task.type}</h1>
<p data-testid="data">{task[dataProp] as string}</p>
<button onClick={onTaskCompleted}>Complete</button>
</div>
);
Expand Down
62 changes: 62 additions & 0 deletions packages/react-experiment/__tests__/useLogger.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { render, screen } from '@testing-library/react';
import { Run, useLogger } from '../src/main.js';
import userEventPackage from '@testing-library/user-event';

// @ts-expect-error - userEventPackage is not typed correctly
const userEvent: typeof userEventPackage.default = userEventPackage;

describe('useLogger', () => {
it('can be used with a provided type', async () => {
const user = userEvent.setup();
const Task = () => {
let handleTaskLog = useLogger<'task'>('task');
return (
<button
onClick={() => {
handleTaskLog({ value: 'value' });
}}
>
log
</button>
);
};
let config = {
tasks: { t: <Task /> },
completed: <div data-testid="end" />,
};
const onLog = vi.fn(() => Promise.resolve());
render(<Run elements={config} timeline={[{ type: 't' }]} onLog={onLog} />);
await user.click(screen.getByRole('button'));
expect(onLog).toHaveBeenCalledWith({
type: 'task',
value: 'value',
});
});

it('can be used without providing a type', async () => {
const user = userEvent.setup();
const Task = () => {
let handleLog = useLogger();
return (
<button
onClick={() => {
handleLog({ type: 'task', value: 'value' });
}}
>
log
</button>
);
};
let config = {
tasks: { t: <Task /> },
completed: <div data-testid="end" />,
};
const onLog = vi.fn(() => Promise.resolve());
render(<Run elements={config} timeline={[{ type: 't' }]} onLog={onLog} />);
await user.click(screen.getByRole('button'));
expect(onLog).toHaveBeenCalledWith({
type: 'task',
value: 'value',
});
});
});
6 changes: 3 additions & 3 deletions packages/react-experiment/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export type Typed<T extends string = string> = {
type: T;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyTask = Typed & { [key: PropertyKey]: any };
export type AnyTask = Typed & { [key: PropertyKey]: unknown };

export type RegisteredTask = RegisterExperiment extends { task: infer T }
? T extends Typed
? T
Expand All @@ -21,4 +21,4 @@ export type RegisteredLog = RegisterExperiment extends { log: infer L }
? L extends Typed
? L
: never
: Typed;
: AnyLog;
4 changes: 3 additions & 1 deletion packages/react-experiment/src/contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { TimelineState } from './useManagedTimeline.js';
export const timelineContext =
React.createContext<TimelineState<RegisteredTask> | null>(null);

export const noLoggerSymbol = Symbol('no logger');

export const loggerContext = React.createContext<
((log: RegisteredLog) => void) | null
((log: RegisteredLog) => void) | null | typeof noLoggerSymbol
>(null);
4 changes: 2 additions & 2 deletions packages/react-experiment/src/run.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { RegisteredTask, Typed, RegisteredLog } from './config.js';
import { loggerContext, timelineContext } from './contexts.js';
import { loggerContext, noLoggerSymbol, timelineContext } from './contexts.js';
import useManagedTimeline, {
Timeline,
TimelineState,
Expand Down Expand Up @@ -58,7 +58,7 @@ export function Run<T extends RegisteredTask>({
case 'canceled':
case 'loading':
return (
<loggerContext.Provider value={onLog}>
<loggerContext.Provider value={onLog ?? noLoggerSymbol}>
{elements.loading}
</loggerContext.Provider>
);
Expand Down
45 changes: 25 additions & 20 deletions packages/react-experiment/src/useLogger.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
import * as React from 'react';
import { RegisteredLog } from './config.js';
import { loggerContext } from './contexts.js';
import { loggerContext, noLoggerSymbol } from './contexts.js';
import { NotUnion } from './utils.js';

type Simplify<T> = { [K in keyof T]: T[K] } & {};

export function useLogger<T extends RegisteredLog['type']>(
// This forces the user to provide one unique type of log, so that the type
// of the log is inferred, and the injection of logType is correct. If the
// user wants to log multiple types of logs, they should use useLogger without
// argument.
logType: NotUnion<T>,
): (log: Simplify<Omit<RegisteredLog & { type: T }, 'type'>>) => void;
type UntypedLogFromType<T extends RegisteredLog['type']> = Simplify<
Omit<RegisteredLog & { type: T }, 'type'>
>;

export function useLogger<
T extends RegisteredLog['type'],
L extends UntypedLogFromType<NotUnion<T>> = UntypedLogFromType<NotUnion<T>>,
>(logType: T): (log: L) => void;
export function useLogger(): (log: RegisteredLog) => void;
export function useLogger<T extends RegisteredLog['type']>(
logType?: NotUnion<T>,
):
| ((log: RegisteredLog) => void)
| ((log: Simplify<Omit<RegisteredLog & { type: T }, 'type'>>) => void) {
export function useLogger<
T extends RegisteredLog['type'],
L extends UntypedLogFromType<NotUnion<T>> = UntypedLogFromType<NotUnion<T>>,
>(logType?: T): ((log: RegisteredLog) => void) | ((log: L) => void) {
const addLog = React.useContext(loggerContext);
if (addLog == null) {
throw new Error(
'No logger found. Is this component rendered in a <Run />, and was a logger provided as a prop to <Run />?',
'No logger found. Is this component rendered in a <Run />?',
);
}
if (logType == null) {
return addLog;
if (addLog === noLoggerSymbol) {
throw new Error('No logger found. Was onLog provided in <Run />?');
}
return (log: Omit<RegisteredLog & { type: T }, 'type'>) =>
// LogType cannot be a union type so we it should be safe to cast log to
// RegisteredLog.
addLog({ ...log, type: logType } as unknown as RegisteredLog);

return React.useMemo(() => {
if (logType == null) {
return addLog;
}
return (log: L) => {
return addLog({ ...log, type: logType } as unknown as L & { type: T });
};
}, [addLog, logType]);
}

0 comments on commit 7696b2f

Please sign in to comment.