@guardian/stand 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.prettierrc +1 -0
- package/.storybook/main.ts +12 -0
- package/.storybook/preview.tsx +83 -0
- package/CHANGELOG.md +7 -0
- package/README.md +15 -0
- package/dist/byline/Byline.cjs +375 -0
- package/dist/byline/Byline.js +273 -0
- package/dist/byline/Preview.cjs +52 -0
- package/dist/byline/Preview.js +26 -0
- package/dist/byline/lib.cjs +240 -0
- package/dist/byline/lib.js +181 -0
- package/dist/byline/placeholder.cjs +29 -0
- package/dist/byline/placeholder.js +27 -0
- package/dist/byline/plugins.cjs +144 -0
- package/dist/byline/plugins.js +123 -0
- package/dist/byline/schema.cjs +66 -0
- package/dist/byline/schema.js +59 -0
- package/dist/byline/styles.cjs +244 -0
- package/dist/byline/styles.js +234 -0
- package/dist/index.cjs +4 -4
- package/dist/index.js +1 -5
- package/dist/types/.storybook/main.d.ts +3 -0
- package/dist/types/.storybook/preview.d.ts +3 -0
- package/dist/types/jest-setup-after-env.d.ts +1 -0
- package/dist/types/src/byline/Byline.d.ts +17 -0
- package/dist/types/src/byline/Byline.stories.d.ts +206 -0
- package/dist/types/src/byline/Byline.test.d.ts +1 -0
- package/dist/types/src/byline/Preview.d.ts +4 -0
- package/dist/types/src/byline/contributors-fixture.d.ts +1 -0
- package/dist/types/src/byline/lib.d.ts +48 -0
- package/dist/types/src/byline/lib.test.d.ts +1 -0
- package/dist/types/src/byline/placeholder.d.ts +2 -0
- package/dist/types/src/byline/plugins.d.ts +4 -0
- package/dist/types/src/byline/schema.d.ts +2 -0
- package/dist/types/src/byline/styles.d.ts +11 -0
- package/dist/types/src/byline/theme.d.ts +44 -0
- package/dist/types/src/byline/util.d.ts +3 -0
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/mocks/prosemirror-view.d.ts +10 -0
- package/eslint.config.js +14 -0
- package/jest-setup-after-env.ts +1 -0
- package/jest.config.js +12 -0
- package/package.json +60 -129
- package/rollup.config.js +49 -0
- package/src/byline/Byline.stories.tsx +186 -0
- package/src/byline/Byline.test.tsx +450 -0
- package/src/byline/Byline.tsx +524 -0
- package/src/byline/Preview.tsx +59 -0
- package/src/byline/contributors-fixture.ts +1006 -0
- package/src/byline/lib.test.ts +179 -0
- package/src/byline/lib.ts +426 -0
- package/src/byline/placeholder.ts +30 -0
- package/src/byline/plugins.ts +186 -0
- package/src/byline/schema.ts +62 -0
- package/src/byline/styles.ts +246 -0
- package/src/byline/theme.ts +45 -0
- package/src/byline/util.ts +5 -0
- package/src/index.ts +2 -0
- package/src/mocks/prosemirror-view.ts +19 -0
- package/tsconfig.json +19 -0
- package/LICENSE +0 -201
- package/dist/index.d.ts +0 -3
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { Byline } from './Byline';
|
|
3
|
+
import { contributors } from './contributors-fixture';
|
|
4
|
+
import type { TaggedContributor } from './lib';
|
|
5
|
+
|
|
6
|
+
const searchContributors = (
|
|
7
|
+
selectedText: string,
|
|
8
|
+
): Promise<TaggedContributor[]> => {
|
|
9
|
+
return new Promise<TaggedContributor[]>((resolve) => {
|
|
10
|
+
const results = contributors
|
|
11
|
+
.filter((name) =>
|
|
12
|
+
name.toLowerCase().includes(selectedText.toLowerCase()),
|
|
13
|
+
)
|
|
14
|
+
.map((name, index) => ({
|
|
15
|
+
path: `profile/${name.toLowerCase().replace(/\s/g, '-')}`,
|
|
16
|
+
label: name,
|
|
17
|
+
type: 'Contributor',
|
|
18
|
+
tagId: `${index + 1}`,
|
|
19
|
+
// show internal label for every 5th contributor for testing internalLabel
|
|
20
|
+
internalLabel:
|
|
21
|
+
index % 5 === 0 ? `${name} (internal)` : undefined,
|
|
22
|
+
}))
|
|
23
|
+
.slice(0, 20);
|
|
24
|
+
|
|
25
|
+
return resolve(results);
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const disableSnapshot = {
|
|
30
|
+
parameters: {
|
|
31
|
+
chromatic: { disableSnapshot: true },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const meta = {
|
|
36
|
+
title: 'Stand/Byline',
|
|
37
|
+
component: Byline,
|
|
38
|
+
parameters: {},
|
|
39
|
+
args: {
|
|
40
|
+
handleSave: () => {},
|
|
41
|
+
initialValue: [],
|
|
42
|
+
searchContributors,
|
|
43
|
+
enablePreview: true,
|
|
44
|
+
},
|
|
45
|
+
} satisfies Meta<typeof Byline>;
|
|
46
|
+
|
|
47
|
+
type Story = StoryObj<typeof Byline>;
|
|
48
|
+
|
|
49
|
+
export const Default = {} satisfies Story;
|
|
50
|
+
|
|
51
|
+
export const WithTheme = {
|
|
52
|
+
args: {
|
|
53
|
+
allowUntaggedContributors: true,
|
|
54
|
+
searchContributors,
|
|
55
|
+
theme: {
|
|
56
|
+
editor: {
|
|
57
|
+
invisibles: {
|
|
58
|
+
color: 'lightblue',
|
|
59
|
+
},
|
|
60
|
+
color: 'rgba(255, 255, 255, 0.87)',
|
|
61
|
+
background: 'rgb(51, 51, 51)',
|
|
62
|
+
border: '1px solid rgb(173, 216, 230)',
|
|
63
|
+
chip: {
|
|
64
|
+
color: 'initial',
|
|
65
|
+
taggedBackground: 'rgb(173, 216, 230)',
|
|
66
|
+
border: '1px solid rgb(173, 216, 230)',
|
|
67
|
+
borderRadius: '3px',
|
|
68
|
+
padding: '5.5px 7px',
|
|
69
|
+
untagged: {
|
|
70
|
+
color: 'rgba(255, 255, 255, 0.87)',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
dropdown: {
|
|
75
|
+
background: 'rgb(36, 36, 36)',
|
|
76
|
+
li: {
|
|
77
|
+
color: 'rgba(255, 255, 255, 0.87)',
|
|
78
|
+
borderBottom: 'none',
|
|
79
|
+
selected: {
|
|
80
|
+
color: 'rgba(255, 255, 255, 0.87)',
|
|
81
|
+
background: 'rgb(51, 51, 51)',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
} satisfies Story;
|
|
88
|
+
|
|
89
|
+
export const WithUntaggedContributors = {
|
|
90
|
+
args: {
|
|
91
|
+
allowUntaggedContributors: true,
|
|
92
|
+
},
|
|
93
|
+
...disableSnapshot,
|
|
94
|
+
} satisfies Story;
|
|
95
|
+
|
|
96
|
+
export const WithInitialValue = {
|
|
97
|
+
args: {
|
|
98
|
+
allowUntaggedContributors: true,
|
|
99
|
+
initialValue: [
|
|
100
|
+
{
|
|
101
|
+
type: 'contributor',
|
|
102
|
+
value: 'Joe Bloggs',
|
|
103
|
+
tagId: '1',
|
|
104
|
+
path: 'profile/joebloggs',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: 'text',
|
|
108
|
+
value: ' in London, ',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
type: 'contributor',
|
|
112
|
+
value: 'Jane Doe',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: 'text',
|
|
116
|
+
value: ' in New York',
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
} satisfies Story;
|
|
121
|
+
|
|
122
|
+
export const WithNoSearch = {
|
|
123
|
+
args: {
|
|
124
|
+
allowUntaggedContributors: true,
|
|
125
|
+
searchContributors: undefined,
|
|
126
|
+
},
|
|
127
|
+
...disableSnapshot,
|
|
128
|
+
} satisfies Story;
|
|
129
|
+
|
|
130
|
+
export const WithNoSearchAndNoUntagged = {
|
|
131
|
+
args: {
|
|
132
|
+
allowUntaggedContributors: false,
|
|
133
|
+
searchContributors: undefined,
|
|
134
|
+
},
|
|
135
|
+
...disableSnapshot,
|
|
136
|
+
} satisfies Story;
|
|
137
|
+
|
|
138
|
+
export const WithCustomPlaceholder = {
|
|
139
|
+
args: {
|
|
140
|
+
allowUntaggedContributors: true,
|
|
141
|
+
placeholder: 'A custom placeholder...',
|
|
142
|
+
},
|
|
143
|
+
} satisfies Story;
|
|
144
|
+
|
|
145
|
+
export const WithContributorLimit = {
|
|
146
|
+
args: { contributorLimit: 1 },
|
|
147
|
+
...disableSnapshot,
|
|
148
|
+
} satisfies Story;
|
|
149
|
+
|
|
150
|
+
export const WithoutPreview = {
|
|
151
|
+
args: {
|
|
152
|
+
allowUntaggedContributors: true,
|
|
153
|
+
enablePreview: false,
|
|
154
|
+
},
|
|
155
|
+
...disableSnapshot,
|
|
156
|
+
} satisfies Story;
|
|
157
|
+
|
|
158
|
+
export const ReadOnly = {
|
|
159
|
+
args: {
|
|
160
|
+
readOnly: true,
|
|
161
|
+
allowUntaggedContributors: true,
|
|
162
|
+
enablePreview: true,
|
|
163
|
+
initialValue: [
|
|
164
|
+
{
|
|
165
|
+
type: 'contributor',
|
|
166
|
+
value: 'Joe Bloggs',
|
|
167
|
+
tagId: '1',
|
|
168
|
+
path: 'profile/joebloggs',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
type: 'text',
|
|
172
|
+
value: ' in London and ',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
type: 'contributor',
|
|
176
|
+
value: 'Jane Doe',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: 'text',
|
|
180
|
+
value: ' in New York',
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
} satisfies Story;
|
|
185
|
+
|
|
186
|
+
export default meta;
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { act, render } from '@testing-library/react';
|
|
2
|
+
import { screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import type { EditorView } from 'prosemirror-view';
|
|
5
|
+
import { mockEditorViewMethods } from '../mocks/prosemirror-view';
|
|
6
|
+
import { Byline } from './Byline';
|
|
7
|
+
import type { BylineModel, TaggedContributor } from './lib';
|
|
8
|
+
|
|
9
|
+
jest.mock('prosemirror-view', () => {
|
|
10
|
+
const actualProsemirrorView = jest.requireActual('prosemirror-view');
|
|
11
|
+
const Class = actualProsemirrorView.EditorView as typeof EditorView;
|
|
12
|
+
class MockEditorView extends Class {
|
|
13
|
+
posAtCoords = mockEditorViewMethods.posAtCoords;
|
|
14
|
+
coordsAtPos = mockEditorViewMethods.coordsAtPos;
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
...actualProsemirrorView,
|
|
18
|
+
EditorView: MockEditorView,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Mock the offsetParent used for visibility in the test dom
|
|
23
|
+
// See https://github.com/jsdom/jsdom/issues/1261
|
|
24
|
+
Object.defineProperty(HTMLElement.prototype, 'offsetParent', {
|
|
25
|
+
get() {
|
|
26
|
+
return this.parentNode;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Mock scrollIntoView for testing, else we get `TypeError: selectedOption.scrollIntoView is not a function` errors
|
|
31
|
+
Element.prototype.scrollIntoView = jest.fn();
|
|
32
|
+
|
|
33
|
+
const mockSearchContributors: (
|
|
34
|
+
selectedText: string,
|
|
35
|
+
) => Promise<TaggedContributor[]> = (selectedText: string) => {
|
|
36
|
+
const allContributors: TaggedContributor[] = [
|
|
37
|
+
{
|
|
38
|
+
path: 'profile/mahesh-makani',
|
|
39
|
+
label: 'Mahesh Makani',
|
|
40
|
+
internalLabel: 'Mahesh Makani (Software Engineer)',
|
|
41
|
+
tagId: '1',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
path: 'profile/andrew-howe-ely',
|
|
45
|
+
label: 'Andrew Howe-Ely',
|
|
46
|
+
tagId: '2',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
path: 'profile/jane-smith',
|
|
50
|
+
label: 'Jane Smith',
|
|
51
|
+
internalLabel: 'Jane Smith (Journalist)',
|
|
52
|
+
tagId: '3',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
path: 'profile/john-doe',
|
|
56
|
+
label: 'John Doe',
|
|
57
|
+
tagId: '4',
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const filteredContributors = allContributors.filter((contributor) =>
|
|
62
|
+
contributor.label
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
.startsWith(selectedText.toLowerCase().trim()),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return Promise.resolve(filteredContributors);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
describe('Byline editor', () => {
|
|
71
|
+
it('shows dropdown with untagged contributor suggestion, hides when click away', async () => {
|
|
72
|
+
const user = userEvent.setup();
|
|
73
|
+
render(
|
|
74
|
+
<Byline allowUntaggedContributors={true} handleSave={() => {}} />,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
await act(async () => {
|
|
78
|
+
await user.type(screen.getByRole('combobox'), 'Test');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const dropdown = screen.getByRole('listbox');
|
|
82
|
+
expect(dropdown).toHaveTextContent(
|
|
83
|
+
'Add "Test" as untagged contributor',
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
await act(async () => {
|
|
87
|
+
await user.click(document.body);
|
|
88
|
+
});
|
|
89
|
+
expect(dropdown).not.toBeVisible();
|
|
90
|
+
});
|
|
91
|
+
it('adds a chip from the dropdown', async () => {
|
|
92
|
+
const user = userEvent.setup();
|
|
93
|
+
render(
|
|
94
|
+
<Byline allowUntaggedContributors={true} handleSave={() => {}} />,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const editor = screen.getByRole('combobox');
|
|
98
|
+
await act(async () => {
|
|
99
|
+
await user.type(screen.getByRole('combobox'), 'Test');
|
|
100
|
+
await user.click(screen.getByRole('option'));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const chip = editor.querySelector('chip[data-label="Test"]');
|
|
104
|
+
expect(chip).toBeInTheDocument();
|
|
105
|
+
expect(chip).toHaveTextContent('Test');
|
|
106
|
+
});
|
|
107
|
+
it('deletes a chip when clicking x', async () => {
|
|
108
|
+
const user = userEvent.setup();
|
|
109
|
+
render(
|
|
110
|
+
<Byline allowUntaggedContributors={true} handleSave={() => {}} />,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const editor = screen.getByRole('combobox');
|
|
114
|
+
await act(async () => {
|
|
115
|
+
await user.type(screen.getByRole('combobox'), 'Test');
|
|
116
|
+
await user.click(screen.getByRole('option'));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const chip = editor.querySelector('chip[data-label="Test"]');
|
|
120
|
+
expect(chip).toBeInTheDocument();
|
|
121
|
+
|
|
122
|
+
const deleteHander = screen.getByTitle('Delete Test');
|
|
123
|
+
|
|
124
|
+
await act(async () => {
|
|
125
|
+
await user.click(deleteHander);
|
|
126
|
+
});
|
|
127
|
+
expect(chip).not.toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
it('hides dropdown with escape key', async () => {
|
|
130
|
+
const user = userEvent.setup();
|
|
131
|
+
render(
|
|
132
|
+
<Byline allowUntaggedContributors={true} handleSave={() => {}} />,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
await act(async () => {
|
|
136
|
+
await user.type(screen.getByRole('combobox'), 'Test');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const dropdown = screen.getByRole('listbox');
|
|
140
|
+
expect(dropdown).toHaveTextContent(
|
|
141
|
+
'Add "Test" as untagged contributor',
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
await act(async () => {
|
|
145
|
+
await user.type(screen.getByRole('combobox'), '{Escape}');
|
|
146
|
+
});
|
|
147
|
+
expect(dropdown).not.toBeVisible();
|
|
148
|
+
});
|
|
149
|
+
it('displays placeholder text when no content is present', async () => {
|
|
150
|
+
const user = userEvent.setup();
|
|
151
|
+
render(
|
|
152
|
+
<Byline
|
|
153
|
+
allowUntaggedContributors={true}
|
|
154
|
+
placeholder="Placeholder"
|
|
155
|
+
handleSave={() => {}}
|
|
156
|
+
/>,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const placeholder = await screen.findByText('Placeholder');
|
|
160
|
+
expect(placeholder).toBeInTheDocument();
|
|
161
|
+
|
|
162
|
+
await act(async () => {
|
|
163
|
+
await user.type(screen.getByRole('combobox'), 'Test');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(placeholder).not.toBeVisible();
|
|
167
|
+
});
|
|
168
|
+
it('renders search options in dropdown', async () => {
|
|
169
|
+
const user = userEvent.setup();
|
|
170
|
+
render(
|
|
171
|
+
<Byline
|
|
172
|
+
allowUntaggedContributors={true}
|
|
173
|
+
placeholder="Placeholder"
|
|
174
|
+
handleSave={() => {}}
|
|
175
|
+
searchContributors={mockSearchContributors}
|
|
176
|
+
/>,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
await act(async () => {
|
|
180
|
+
await user.click(screen.getByRole('combobox'));
|
|
181
|
+
await user.type(screen.getByRole('combobox'), 'Ma'); // Type "Ma" to match "Mahesh Makani"
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// testing internalLabel
|
|
185
|
+
const mahesh = screen.getByText('Mahesh Makani (Software Engineer)');
|
|
186
|
+
expect(mahesh).toBeInTheDocument();
|
|
187
|
+
|
|
188
|
+
await act(async () => {
|
|
189
|
+
await user.clear(screen.getByRole('combobox'));
|
|
190
|
+
await user.type(screen.getByRole('combobox'), 'A');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// testing label
|
|
194
|
+
const andrew = screen.getByText('Andrew Howe-Ely');
|
|
195
|
+
expect(andrew).toBeInTheDocument();
|
|
196
|
+
});
|
|
197
|
+
it('moves the selected option in dropdown with arrow keys', async () => {
|
|
198
|
+
const user = userEvent.setup();
|
|
199
|
+
render(
|
|
200
|
+
<Byline
|
|
201
|
+
allowUntaggedContributors={true}
|
|
202
|
+
placeholder="Placeholder"
|
|
203
|
+
handleSave={() => {}}
|
|
204
|
+
searchContributors={mockSearchContributors}
|
|
205
|
+
/>,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
await act(async () => {
|
|
209
|
+
await user.click(screen.getByRole('combobox'));
|
|
210
|
+
await user.type(screen.getByRole('combobox'), 'J'); // Type "J" to match both "Jane Smith" and "John Doe"
|
|
211
|
+
await user.type(screen.getByRole('combobox'), '{ArrowDown}');
|
|
212
|
+
await user.type(screen.getByRole('combobox'), '{ArrowDown}');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const lastOption = screen.getAllByRole('option').pop();
|
|
216
|
+
expect(lastOption).toHaveAttribute('aria-selected', 'true');
|
|
217
|
+
});
|
|
218
|
+
it('adds a chip with the Enter key', async () => {
|
|
219
|
+
const user = userEvent.setup();
|
|
220
|
+
render(
|
|
221
|
+
<Byline
|
|
222
|
+
allowUntaggedContributors={true}
|
|
223
|
+
placeholder="Placeholder"
|
|
224
|
+
handleSave={() => {}}
|
|
225
|
+
searchContributors={mockSearchContributors}
|
|
226
|
+
/>,
|
|
227
|
+
);
|
|
228
|
+
const editor = screen.getByRole('combobox');
|
|
229
|
+
|
|
230
|
+
await act(async () => {
|
|
231
|
+
await user.click(editor);
|
|
232
|
+
await user.type(screen.getByRole('combobox'), 'Andrew'); // Type full name to ensure it's found
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Wait for the dropdown to appear and verify the option exists
|
|
236
|
+
const andrewOption = await screen.findByText('Andrew Howe-Ely');
|
|
237
|
+
expect(andrewOption).toBeInTheDocument();
|
|
238
|
+
|
|
239
|
+
await act(async () => {
|
|
240
|
+
await user.type(screen.getByRole('combobox'), '{Enter}'); // Select the first option
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const chip = editor.querySelector('chip[data-label="Andrew Howe-Ely"]');
|
|
244
|
+
expect(chip).toBeInTheDocument();
|
|
245
|
+
expect(chip).toHaveTextContent('Andrew Howe-Ely');
|
|
246
|
+
});
|
|
247
|
+
it('executes save function on every keypress', async () => {
|
|
248
|
+
const user = userEvent.setup();
|
|
249
|
+
const mockHandleSave = jest.fn();
|
|
250
|
+
render(
|
|
251
|
+
<Byline
|
|
252
|
+
allowUntaggedContributors={true}
|
|
253
|
+
placeholder="Placeholder"
|
|
254
|
+
handleSave={mockHandleSave}
|
|
255
|
+
searchContributors={mockSearchContributors}
|
|
256
|
+
/>,
|
|
257
|
+
);
|
|
258
|
+
const editor = screen.getByRole('combobox');
|
|
259
|
+
|
|
260
|
+
await act(async () => {
|
|
261
|
+
await user.type(editor, 'T');
|
|
262
|
+
await user.type(editor, 'e');
|
|
263
|
+
await user.type(editor, 's');
|
|
264
|
+
await user.type(editor, 't');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(mockHandleSave).toHaveBeenCalledTimes(4);
|
|
268
|
+
});
|
|
269
|
+
it('executes save function with correct input at each keypress', async () => {
|
|
270
|
+
const user = userEvent.setup();
|
|
271
|
+
const saveLog: BylineModel[] = [];
|
|
272
|
+
const mockHandleSave = (value: BylineModel) => {
|
|
273
|
+
saveLog.push(value);
|
|
274
|
+
};
|
|
275
|
+
render(
|
|
276
|
+
<Byline
|
|
277
|
+
allowUntaggedContributors={true}
|
|
278
|
+
placeholder="Placeholder"
|
|
279
|
+
handleSave={mockHandleSave}
|
|
280
|
+
searchContributors={mockSearchContributors}
|
|
281
|
+
/>,
|
|
282
|
+
);
|
|
283
|
+
const editor = screen.getByRole('combobox');
|
|
284
|
+
|
|
285
|
+
await act(async () => {
|
|
286
|
+
await user.type(editor, 'T');
|
|
287
|
+
await user.type(editor, 'e');
|
|
288
|
+
await user.type(editor, 's');
|
|
289
|
+
await user.type(editor, 't');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(saveLog.at(0)?.pop()?.value).toBe('T');
|
|
293
|
+
expect(saveLog.at(1)?.pop()?.value).toBe('Te');
|
|
294
|
+
expect(saveLog.at(2)?.pop()?.value).toBe('Tes');
|
|
295
|
+
expect(saveLog.at(3)?.pop()?.value).toBe('Test');
|
|
296
|
+
});
|
|
297
|
+
it('passes * character to search function', async () => {
|
|
298
|
+
const user = userEvent.setup();
|
|
299
|
+
const mockSearch = jest.fn().mockImplementation(mockSearchContributors);
|
|
300
|
+
render(
|
|
301
|
+
<Byline
|
|
302
|
+
allowUntaggedContributors={true}
|
|
303
|
+
placeholder="Placeholder"
|
|
304
|
+
handleSave={() => {}}
|
|
305
|
+
searchContributors={mockSearch}
|
|
306
|
+
/>,
|
|
307
|
+
);
|
|
308
|
+
const editor = screen.getByRole('combobox');
|
|
309
|
+
|
|
310
|
+
await act(async () => {
|
|
311
|
+
await user.type(editor, '*Test');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(mockSearch).toHaveBeenLastCalledWith('*Test');
|
|
315
|
+
|
|
316
|
+
await act(async () => {
|
|
317
|
+
await user.clear(editor);
|
|
318
|
+
await user.type(editor, 'T*est');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
expect(mockSearch).toHaveBeenLastCalledWith('T*est');
|
|
322
|
+
|
|
323
|
+
await act(async () => {
|
|
324
|
+
await user.clear(editor);
|
|
325
|
+
await user.type(editor, 'Te-St*');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(mockSearch).toHaveBeenLastCalledWith('Te-St*');
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('trackTypingFromStart behavior', () => {
|
|
333
|
+
it('starts tracking when typing from start', async () => {
|
|
334
|
+
const user = userEvent.setup();
|
|
335
|
+
const mockHandleSave = jest.fn();
|
|
336
|
+
|
|
337
|
+
render(
|
|
338
|
+
<Byline
|
|
339
|
+
allowUntaggedContributors={true}
|
|
340
|
+
handleSave={mockHandleSave}
|
|
341
|
+
searchContributors={mockSearchContributors}
|
|
342
|
+
initialValue={[
|
|
343
|
+
{ type: 'text', value: 'Existing content and more text' },
|
|
344
|
+
]}
|
|
345
|
+
/>,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const editor = screen.getByRole('combobox');
|
|
349
|
+
|
|
350
|
+
// Start typing from the beginning
|
|
351
|
+
await act(async () => {
|
|
352
|
+
await user.click(editor);
|
|
353
|
+
await user.keyboard('{Home}J');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(
|
|
357
|
+
screen.queryByText('JExisting content and more text'),
|
|
358
|
+
).toBeInTheDocument();
|
|
359
|
+
|
|
360
|
+
expect(screen.getByRole('listbox')).toBeVisible();
|
|
361
|
+
expect(screen.getByText('Jane Smith (Journalist)')).toBeInTheDocument();
|
|
362
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
363
|
+
|
|
364
|
+
await act(async () => {
|
|
365
|
+
await user.keyboard('ohn');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
expect(
|
|
369
|
+
screen.queryByText('JohnExisting content and more text'),
|
|
370
|
+
).toBeInTheDocument();
|
|
371
|
+
expect(screen.getByRole('listbox')).toBeVisible();
|
|
372
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
373
|
+
expect(
|
|
374
|
+
screen.queryByText('Jane Smith (Journalist)'),
|
|
375
|
+
).not.toBeInTheDocument();
|
|
376
|
+
|
|
377
|
+
await act(async () => {
|
|
378
|
+
await user.click(screen.getByText('John Doe'));
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const chip = editor.querySelector('chip[data-label="John Doe"]');
|
|
382
|
+
expect(chip).toBeInTheDocument();
|
|
383
|
+
|
|
384
|
+
expect(
|
|
385
|
+
screen.queryByText('Existing content and more text'),
|
|
386
|
+
).toBeInTheDocument();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('handles backspace correctly within tracked range', async () => {
|
|
390
|
+
const user = userEvent.setup();
|
|
391
|
+
const mockHandleSave = jest.fn();
|
|
392
|
+
|
|
393
|
+
render(
|
|
394
|
+
<Byline
|
|
395
|
+
allowUntaggedContributors={true}
|
|
396
|
+
handleSave={mockHandleSave}
|
|
397
|
+
searchContributors={mockSearchContributors}
|
|
398
|
+
initialValue={[
|
|
399
|
+
{ type: 'text', value: 'Existing content and more text' },
|
|
400
|
+
]}
|
|
401
|
+
/>,
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const editor = screen.getByRole('combobox');
|
|
405
|
+
|
|
406
|
+
// Type some text
|
|
407
|
+
await act(async () => {
|
|
408
|
+
await user.click(editor);
|
|
409
|
+
await user.keyboard('{Home}John');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
expect(screen.getByRole('listbox')).toBeVisible();
|
|
413
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
414
|
+
|
|
415
|
+
// backspace to fix a typo
|
|
416
|
+
await act(async () => {
|
|
417
|
+
await user.keyboard('{Backspace}'.repeat(2));
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// should still be tracking (within range) and show contributors starting with "Jo"
|
|
421
|
+
expect(screen.getByRole('listbox')).toBeVisible();
|
|
422
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
423
|
+
|
|
424
|
+
// backspace and edit name
|
|
425
|
+
await act(async () => {
|
|
426
|
+
await user.keyboard('{Backspace}ane');
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
expect(
|
|
430
|
+
screen.queryByText('JaneExisting content and more text'),
|
|
431
|
+
).toBeInTheDocument();
|
|
432
|
+
|
|
433
|
+
expect(screen.getByRole('listbox')).toBeVisible();
|
|
434
|
+
expect(
|
|
435
|
+
screen.queryByText('Jane Smith (Journalist)'),
|
|
436
|
+
).toBeInTheDocument();
|
|
437
|
+
expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
|
|
438
|
+
|
|
439
|
+
await act(async () => {
|
|
440
|
+
await user.click(screen.getByText('Jane Smith (Journalist)'));
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const chip = editor.querySelector('chip[data-label="Jane Smith"]');
|
|
444
|
+
expect(chip).toBeInTheDocument();
|
|
445
|
+
|
|
446
|
+
expect(
|
|
447
|
+
screen.queryByText('Existing content and more text'),
|
|
448
|
+
).toBeInTheDocument();
|
|
449
|
+
});
|
|
450
|
+
});
|