Skip to content

Commit fa14c71

Browse files
authored
Merge pull request #3 from sjsjsj1246/codex/fix-runtime-contract
[-]: runtime contract 안정화
2 parents 8fd2d75 + 1b3bf7a commit fa14c71

5 files changed

Lines changed: 150 additions & 17 deletions

File tree

README.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,34 @@ yarn add react-tutorial-overlay
3131
```
3232

3333
```jsx
34-
import { TutorialOverlay, tutorial } from "../src";
34+
import { TutorialOverlay, tutorial } from 'react-tutorial-overlay';
3535

3636
const App = () => {
3737
const handleClick = () => {
38-
tutorial.open([
39-
{
40-
targetIds: ["target1"],
41-
title: "title",
42-
content: "content",
43-
},
44-
]);
38+
tutorial.open({
39+
steps: [
40+
{
41+
targetIds: ['target1'],
42+
title: 'title',
43+
content: 'content',
44+
},
45+
],
46+
options: {},
47+
});
4548
};
4649

4750
return (
4851
<div>
4952
<button onClick={handleClick}>open</button>
53+
<div id="target1">target</div>
5054
<TutorialOverlay />
5155
</div>
5256
);
5357
};
5458
```
5559

60+
`content` is rendered as a plain string. HTML markup in the string is not interpreted.
61+
5662
## Documentation
5763

5864
- [Document](https://react-tutorial-overlay.vercel.app/docs)

packages/main/src/components/content.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
2-
import { ActionType, dispatch, useTutorialStore } from '../core/store';
2+
import { useTutorialStore } from '../core/store';
3+
import { tutorial } from '../core/tutorial';
34
import { styled } from 'goober';
45

56
export const Content = React.forwardRef((_, ref?: React.ForwardedRef<HTMLInputElement>) => {
@@ -8,20 +9,17 @@ export const Content = React.forwardRef((_, ref?: React.ForwardedRef<HTMLInputEl
89
tutorial: { steps },
910
} = useTutorialStore();
1011
const currentStep = steps[index];
11-
const { onPrevStep, onNextStep } = currentStep;
1212

1313
const handlePrev = () => {
14-
onPrevStep?.();
15-
dispatch({ type: ActionType.PREV });
14+
tutorial.prev();
1615
};
1716

1817
const handleNext = () => {
19-
onNextStep?.();
20-
dispatch({ type: ActionType.NEXT });
18+
tutorial.next();
2119
};
2220

2321
const handleClose = () => {
24-
dispatch({ type: ActionType.CLOSE });
22+
tutorial.close();
2523
};
2624

2725
return (
@@ -31,7 +29,7 @@ export const Content = React.forwardRef((_, ref?: React.ForwardedRef<HTMLInputEl
3129
<p>{currentStep.title}</p>
3230
<button onClick={handleClose}>건너뛰기</button>
3331
</InfoTitle>
34-
<InfoContent dangerouslySetInnerHTML={{ __html: currentStep.content ?? '' }} />
32+
<InfoContent>{currentStep.content ?? ''}</InfoContent>
3533
</Heander>
3634
<Footer className="flex items-center justify-between">
3735
<InfoSteps className="text-[.75rem] font-medium text-sub-2.5">

packages/main/src/core/store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ const reducer = (state: State, action: Action): State => {
4949
};
5050
}
5151
case ActionType.PREV:
52-
state.tutorial.steps[state.index]?.onPrevStep?.();
5352
if (state.index > 0) {
53+
state.tutorial.steps[state.index]?.onPrevStep?.();
5454
return {
5555
...state,
5656
index: state.index - 1,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react';
2+
import { act, fireEvent, render, screen } from '@testing-library/react';
3+
import { TutorialOverlay } from '../src/components/tutorial-overlay';
4+
import { tutorial } from '../src/core/tutorial';
5+
6+
function renderOverlay() {
7+
render(
8+
<div>
9+
<div id="first-target">First target</div>
10+
<div id="second-target">Second target</div>
11+
<TutorialOverlay />
12+
</div>
13+
);
14+
}
15+
16+
describe('Content', () => {
17+
test('button navigation invokes each step callback once', () => {
18+
const onNextStep = jest.fn();
19+
const onPrevStep = jest.fn();
20+
21+
renderOverlay();
22+
23+
act(() => {
24+
tutorial.open({
25+
steps: [
26+
{ title: 'Step 1', content: 'Step 1 content', targetIds: ['first-target'], onNextStep },
27+
{ title: 'Step 2', content: 'Step 2 content', targetIds: ['second-target'], onPrevStep },
28+
],
29+
options: {},
30+
});
31+
});
32+
33+
fireEvent.click(screen.getByRole('button', { name: '다음' }));
34+
35+
expect(onNextStep).toHaveBeenCalledTimes(1);
36+
expect(screen.getByText('Step 2 content')).toBeInTheDocument();
37+
38+
fireEvent.click(screen.getByRole('button', { name: '이전' }));
39+
40+
expect(onPrevStep).toHaveBeenCalledTimes(1);
41+
expect(screen.getByText('Step 1 content')).toBeInTheDocument();
42+
});
43+
44+
test('renders content strings as text instead of interpreting html', () => {
45+
renderOverlay();
46+
47+
act(() => {
48+
tutorial.open({
49+
steps: [
50+
{
51+
title: 'HTML content',
52+
content: '<strong>Literal HTML</strong>',
53+
targetIds: ['first-target'],
54+
},
55+
],
56+
options: {},
57+
});
58+
});
59+
60+
expect(screen.getByText('<strong>Literal HTML</strong>')).toBeInTheDocument();
61+
expect(document.querySelector('strong')).toBeNull();
62+
});
63+
});

packages/main/test/store.test.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React from 'react';
2+
import { act, render, screen } from '@testing-library/react';
3+
import { useTutorialStore } from '../src/core/store';
4+
import { tutorial } from '../src/core/tutorial';
5+
6+
function StateProbe() {
7+
const {
8+
index,
9+
tutorial: { steps },
10+
} = useTutorialStore();
11+
12+
return <span data-testid="title">{steps[index]?.title ?? 'none'}</span>;
13+
}
14+
15+
describe('store step callbacks', () => {
16+
test('tutorial.next and tutorial.prev invoke step callbacks once per transition', () => {
17+
const onNextStep = jest.fn();
18+
const onPrevStep = jest.fn();
19+
20+
render(<StateProbe />);
21+
22+
act(() => {
23+
tutorial.open({
24+
steps: [
25+
{ title: 'Step 1', targetIds: ['first-target'], onNextStep },
26+
{ title: 'Step 2', targetIds: ['second-target'], onPrevStep },
27+
],
28+
options: {},
29+
});
30+
});
31+
32+
act(() => {
33+
tutorial.next();
34+
});
35+
36+
expect(onNextStep).toHaveBeenCalledTimes(1);
37+
expect(screen.getByTestId('title')).toHaveTextContent('Step 2');
38+
39+
act(() => {
40+
tutorial.prev();
41+
});
42+
43+
expect(onPrevStep).toHaveBeenCalledTimes(1);
44+
expect(screen.getByTestId('title')).toHaveTextContent('Step 1');
45+
});
46+
47+
test('tutorial.prev does not invoke onPrevStep when already on the first step', () => {
48+
const onPrevStep = jest.fn();
49+
50+
render(<StateProbe />);
51+
52+
act(() => {
53+
tutorial.open({
54+
steps: [{ title: 'Step 1', targetIds: ['first-target'], onPrevStep }],
55+
options: {},
56+
});
57+
});
58+
59+
act(() => {
60+
tutorial.prev();
61+
});
62+
63+
expect(onPrevStep).not.toHaveBeenCalled();
64+
expect(screen.getByTestId('title')).toHaveTextContent('Step 1');
65+
});
66+
});

0 commit comments

Comments
 (0)