@stromcom/react-snippet 0.1.0
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/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/index.cjs +222 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +191 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
- package/src/StromcomConf.jsx +95 -0
- package/src/StromcomHome.jsx +26 -0
- package/src/StromcomProvider.jsx +105 -0
- package/src/StromcomThread.jsx +45 -0
- package/src/StromcomUser.jsx +41 -0
- package/src/__tests__/StromcomConf.test.jsx +52 -0
- package/src/__tests__/StromcomHome.test.jsx +57 -0
- package/src/__tests__/StromcomProvider.test.jsx +137 -0
- package/src/__tests__/StromcomThread.test.jsx +75 -0
- package/src/__tests__/StromcomUser.test.jsx +63 -0
- package/src/index.d.ts +85 -0
- package/src/index.js +5 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { render, cleanup } from '@testing-library/react';
|
|
3
|
+
import { StromcomContext } from '../StromcomProvider.jsx';
|
|
4
|
+
import { StromcomHome } from '../StromcomHome.jsx';
|
|
5
|
+
|
|
6
|
+
function renderWithLayer(layer, props) {
|
|
7
|
+
return render(
|
|
8
|
+
<StromcomContext.Provider value={layer}>
|
|
9
|
+
<StromcomHome {...props} />
|
|
10
|
+
</StromcomContext.Provider>,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
afterEach(() => cleanup());
|
|
15
|
+
|
|
16
|
+
describe('StromcomHome', () => {
|
|
17
|
+
it('initializes the notification center once on mount with the container element', () => {
|
|
18
|
+
const layer = { home: vi.fn() };
|
|
19
|
+
const { container } = renderWithLayer(layer);
|
|
20
|
+
|
|
21
|
+
expect(layer.home).toHaveBeenCalledTimes(1);
|
|
22
|
+
expect(layer.home).toHaveBeenCalledWith(container.firstChild);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('does not re-initialize on re-render', () => {
|
|
26
|
+
const layer = { home: vi.fn() };
|
|
27
|
+
const { rerender } = renderWithLayer(layer, { className: 'a' });
|
|
28
|
+
|
|
29
|
+
rerender(
|
|
30
|
+
<StromcomContext.Provider value={layer}>
|
|
31
|
+
<StromcomHome className="b" />
|
|
32
|
+
</StromcomContext.Provider>,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(layer.home).toHaveBeenCalledTimes(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('passes className, style and data-* attributes through to the container div', () => {
|
|
39
|
+
const layer = { home: vi.fn() };
|
|
40
|
+
const { container } = renderWithLayer(layer, {
|
|
41
|
+
className: 'my-home',
|
|
42
|
+
style: { bottom: 24 },
|
|
43
|
+
'data-testid': 'home',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const div = container.firstChild;
|
|
47
|
+
expect(div.className).toBe('my-home');
|
|
48
|
+
expect(div.style.bottom).toBe('24px');
|
|
49
|
+
expect(div.getAttribute('data-testid')).toBe('home');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('does not call home when there is no layer', () => {
|
|
53
|
+
const { container } = render(<StromcomHome />);
|
|
54
|
+
|
|
55
|
+
expect(container.firstChild).toBeInstanceOf(HTMLDivElement);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { render, cleanup } from '@testing-library/react';
|
|
3
|
+
import { StromcomProvider, useStromcom } from '../StromcomProvider.jsx';
|
|
4
|
+
|
|
5
|
+
function scriptFor(clientKey) {
|
|
6
|
+
return document.querySelector(`script[data-ck="${clientKey}"][data-type="stromcom"]`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
cleanup();
|
|
11
|
+
document.querySelectorAll('script[data-type="stromcom"]').forEach((el) => el.remove());
|
|
12
|
+
delete window.stromCom;
|
|
13
|
+
delete window.stromComDL;
|
|
14
|
+
delete window.customLayer;
|
|
15
|
+
delete window.customLayerDL;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('StromcomProvider', () => {
|
|
19
|
+
it('renders its children', () => {
|
|
20
|
+
const { getByText } = render(
|
|
21
|
+
<StromcomProvider clientKey="ck_1" clientSecret="cs_1">
|
|
22
|
+
<div>hello</div>
|
|
23
|
+
</StromcomProvider>,
|
|
24
|
+
);
|
|
25
|
+
expect(getByText('hello')).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('injects a script tag with the expected dataset and default (production) src', () => {
|
|
29
|
+
render(
|
|
30
|
+
<StromcomProvider clientKey="ck_1" clientSecret="cs_1">
|
|
31
|
+
<div />
|
|
32
|
+
</StromcomProvider>,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const script = scriptFor('ck_1');
|
|
36
|
+
expect(script).not.toBeNull();
|
|
37
|
+
expect(script.dataset.l).toBe('stromCom');
|
|
38
|
+
expect(script.dataset.dl).toBe('stromComDL');
|
|
39
|
+
expect(script.dataset.cs).toBe('cs_1');
|
|
40
|
+
expect(script.src).toBe('https://cdn.stromcom.cz/loader.js?ck_1');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('points at the staging loader when environment="staging"', () => {
|
|
44
|
+
render(
|
|
45
|
+
<StromcomProvider clientKey="ck_2" clientSecret="cs_2" environment="staging">
|
|
46
|
+
<div />
|
|
47
|
+
</StromcomProvider>,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(scriptFor('ck_2').src).toBe('https://cdn.staging.stromcom.cz/loader.js?ck_2');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('treats an unknown environment value as a custom loader URL', () => {
|
|
54
|
+
render(
|
|
55
|
+
<StromcomProvider
|
|
56
|
+
clientKey="ck_3"
|
|
57
|
+
clientSecret="cs_3"
|
|
58
|
+
environment="https://loader.example.com/custom.js"
|
|
59
|
+
>
|
|
60
|
+
<div />
|
|
61
|
+
</StromcomProvider>,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(scriptFor('ck_3').src).toBe('https://loader.example.com/custom.js?ck_3');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('does not inject a second script for the same clientKey', () => {
|
|
68
|
+
const { rerender } = render(
|
|
69
|
+
<StromcomProvider clientKey="ck_4" clientSecret="cs_4">
|
|
70
|
+
<div />
|
|
71
|
+
</StromcomProvider>,
|
|
72
|
+
);
|
|
73
|
+
rerender(
|
|
74
|
+
<StromcomProvider clientKey="ck_4" clientSecret="cs_4">
|
|
75
|
+
<div>changed</div>
|
|
76
|
+
</StromcomProvider>,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(document.querySelectorAll('script[data-ck="ck_4"]')).toHaveLength(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('removes the script on unmount', () => {
|
|
83
|
+
const { unmount } = render(
|
|
84
|
+
<StromcomProvider clientKey="ck_5" clientSecret="cs_5">
|
|
85
|
+
<div />
|
|
86
|
+
</StromcomProvider>,
|
|
87
|
+
);
|
|
88
|
+
expect(scriptFor('ck_5')).not.toBeNull();
|
|
89
|
+
|
|
90
|
+
unmount();
|
|
91
|
+
|
|
92
|
+
expect(scriptFor('ck_5')).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('respects a custom dataLayer name', () => {
|
|
96
|
+
render(
|
|
97
|
+
<StromcomProvider clientKey="ck_6" clientSecret="cs_6" dataLayer="customLayer">
|
|
98
|
+
<div />
|
|
99
|
+
</StromcomProvider>,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(window.customLayer).toBeDefined();
|
|
103
|
+
expect(window.customLayerDL).toBeDefined();
|
|
104
|
+
expect(scriptFor('ck_6').dataset.l).toBe('customLayer');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('exposes a stub layer with the expected queueing methods via useStromcom', () => {
|
|
108
|
+
let layer;
|
|
109
|
+
function Probe() {
|
|
110
|
+
layer = useStromcom();
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
render(
|
|
115
|
+
<StromcomProvider clientKey="ck_7" clientSecret="cs_7">
|
|
116
|
+
<Probe />
|
|
117
|
+
</StromcomProvider>,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(layer).toBeTruthy();
|
|
121
|
+
expect(typeof layer.initUser).toBe('function');
|
|
122
|
+
expect(typeof layer.thread).toBe('function');
|
|
123
|
+
expect(typeof layer.conf).toBe('function');
|
|
124
|
+
expect(typeof layer.home).toBe('function');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('returns null from useStromcom outside of a provider', () => {
|
|
128
|
+
let layer = 'not-set';
|
|
129
|
+
function Probe() {
|
|
130
|
+
layer = useStromcom();
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
render(<Probe />);
|
|
134
|
+
|
|
135
|
+
expect(layer).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { render, cleanup } from '@testing-library/react';
|
|
3
|
+
import { StromcomContext } from '../StromcomProvider.jsx';
|
|
4
|
+
import { StromcomThread } from '../StromcomThread.jsx';
|
|
5
|
+
|
|
6
|
+
function renderWithLayer(layer, props) {
|
|
7
|
+
return render(
|
|
8
|
+
<StromcomContext.Provider value={layer}>
|
|
9
|
+
<StromcomThread {...props} />
|
|
10
|
+
</StromcomContext.Provider>,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
afterEach(() => cleanup());
|
|
15
|
+
|
|
16
|
+
describe('StromcomThread', () => {
|
|
17
|
+
it('initializes the thread once on mount with the container element', () => {
|
|
18
|
+
const layer = { thread: vi.fn() };
|
|
19
|
+
const { container } = renderWithLayer(layer, { code: 'hashed-thread-1' });
|
|
20
|
+
|
|
21
|
+
expect(layer.thread).toHaveBeenCalledTimes(1);
|
|
22
|
+
expect(layer.thread).toHaveBeenCalledWith(container.firstChild, { code: 'hashed-thread-1' });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('includes optional props that are provided', () => {
|
|
26
|
+
const layer = { thread: vi.fn() };
|
|
27
|
+
renderWithLayer(layer, {
|
|
28
|
+
code: 'hashed-thread-2',
|
|
29
|
+
name: 'Order #42',
|
|
30
|
+
url: 'https://example.com/orders/42',
|
|
31
|
+
userHint: false,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(layer.thread).toHaveBeenCalledWith(expect.anything(), {
|
|
35
|
+
code: 'hashed-thread-2',
|
|
36
|
+
name: 'Order #42',
|
|
37
|
+
url: 'https://example.com/orders/42',
|
|
38
|
+
userHint: false,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not re-initialize when props change after mount', () => {
|
|
43
|
+
const layer = { thread: vi.fn() };
|
|
44
|
+
const { rerender } = renderWithLayer(layer, { code: 'hashed-thread-3', name: 'A' });
|
|
45
|
+
|
|
46
|
+
rerender(
|
|
47
|
+
<StromcomContext.Provider value={layer}>
|
|
48
|
+
<StromcomThread code="hashed-thread-3" name="B" />
|
|
49
|
+
</StromcomContext.Provider>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(layer.thread).toHaveBeenCalledTimes(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('passes className, style and data-* attributes through to the container div', () => {
|
|
56
|
+
const layer = { thread: vi.fn() };
|
|
57
|
+
const { container } = renderWithLayer(layer, {
|
|
58
|
+
code: 'hashed-thread-4',
|
|
59
|
+
className: 'my-thread',
|
|
60
|
+
style: { height: 400 },
|
|
61
|
+
'data-testid': 'thread-4',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const div = container.firstChild;
|
|
65
|
+
expect(div.className).toBe('my-thread');
|
|
66
|
+
expect(div.style.height).toBe('400px');
|
|
67
|
+
expect(div.getAttribute('data-testid')).toBe('thread-4');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('does not call thread and renders an (uninitialized) div when there is no layer', () => {
|
|
71
|
+
const { container } = render(<StromcomThread code="hashed-thread-5" />);
|
|
72
|
+
|
|
73
|
+
expect(container.firstChild).toBeInstanceOf(HTMLDivElement);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { render, cleanup } from '@testing-library/react';
|
|
3
|
+
import { StromcomContext } from '../StromcomProvider.jsx';
|
|
4
|
+
import { StromcomUser } from '../StromcomUser.jsx';
|
|
5
|
+
|
|
6
|
+
function renderWithLayer(layer, props) {
|
|
7
|
+
return render(
|
|
8
|
+
<StromcomContext.Provider value={layer}>
|
|
9
|
+
<StromcomUser {...props} />
|
|
10
|
+
</StromcomContext.Provider>,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
afterEach(() => cleanup());
|
|
15
|
+
|
|
16
|
+
describe('StromcomUser', () => {
|
|
17
|
+
it('calls initUser with only the code when no optional props are given', () => {
|
|
18
|
+
const layer = { initUser: vi.fn() };
|
|
19
|
+
renderWithLayer(layer, { code: 'hashed-user-1' });
|
|
20
|
+
|
|
21
|
+
expect(layer.initUser).toHaveBeenCalledTimes(1);
|
|
22
|
+
expect(layer.initUser).toHaveBeenCalledWith({ code: 'hashed-user-1' });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('includes optional props that are provided', () => {
|
|
26
|
+
const layer = { initUser: vi.fn() };
|
|
27
|
+
renderWithLayer(layer, {
|
|
28
|
+
code: 'hashed-user-2',
|
|
29
|
+
name: 'Jane',
|
|
30
|
+
emailAddress: 'jane@example.com',
|
|
31
|
+
readOnly: true,
|
|
32
|
+
avatarURL: 'https://example.com/avatar.png',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(layer.initUser).toHaveBeenCalledWith({
|
|
36
|
+
code: 'hashed-user-2',
|
|
37
|
+
name: 'Jane',
|
|
38
|
+
emailAddress: 'jane@example.com',
|
|
39
|
+
readOnly: true,
|
|
40
|
+
avatarURL: 'https://example.com/avatar.png',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('re-calls initUser when a prop changes', () => {
|
|
45
|
+
const layer = { initUser: vi.fn() };
|
|
46
|
+
const { rerender } = renderWithLayer(layer, { code: 'hashed-user-3', name: 'Jane' });
|
|
47
|
+
|
|
48
|
+
rerender(
|
|
49
|
+
<StromcomContext.Provider value={layer}>
|
|
50
|
+
<StromcomUser code="hashed-user-3" name="Janet" />
|
|
51
|
+
</StromcomContext.Provider>,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(layer.initUser).toHaveBeenCalledTimes(2);
|
|
55
|
+
expect(layer.initUser).toHaveBeenLastCalledWith({ code: 'hashed-user-3', name: 'Janet' });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not call initUser and renders nothing when there is no layer', () => {
|
|
59
|
+
const { container } = render(<StromcomUser code="hashed-user-4" />);
|
|
60
|
+
|
|
61
|
+
expect(container.firstChild).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type StromcomEnvironment = 'production' | 'staging' | (string & {});
|
|
4
|
+
|
|
5
|
+
export interface StromcomLayer {
|
|
6
|
+
initUser: (options: StromcomUserOptions) => void;
|
|
7
|
+
thread: (target: HTMLElement, options: StromcomThreadOptions) => void;
|
|
8
|
+
conf: (options: StromcomConfOptions) => void;
|
|
9
|
+
home: (target: HTMLElement) => void;
|
|
10
|
+
[key: string]: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StromcomProviderProps {
|
|
14
|
+
/** Project client key from the Stromcom dashboard. */
|
|
15
|
+
clientKey: string;
|
|
16
|
+
/** Bearer token / client secret from the Stromcom dashboard. */
|
|
17
|
+
clientSecret: string;
|
|
18
|
+
/** Global JS variable name. Default `"stromCom"`. */
|
|
19
|
+
dataLayer?: string;
|
|
20
|
+
/** `"production"`, `"staging"`, or a full custom loader URL. Default `"production"`. */
|
|
21
|
+
environment?: StromcomEnvironment;
|
|
22
|
+
children?: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function StromcomProvider(props: StromcomProviderProps): JSX.Element;
|
|
26
|
+
|
|
27
|
+
/** Returns the global stromCom object once the loader is set up (or `null` server-side). */
|
|
28
|
+
export function useStromcom(): StromcomLayer | null;
|
|
29
|
+
|
|
30
|
+
export interface StromcomUserOptions {
|
|
31
|
+
/** Hashed unique user identifier (max 100 chars, [a-zA-Z0-9-_]). Hash server-side. */
|
|
32
|
+
code: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
emailAddress?: string;
|
|
35
|
+
readOnly?: boolean;
|
|
36
|
+
avatarURL?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function StromcomUser(props: StromcomUserOptions): null;
|
|
40
|
+
|
|
41
|
+
export interface StromcomThreadOptions {
|
|
42
|
+
/** Hashed unique thread identifier (max 100 chars, [a-zA-Z0-9-_]). Hash server-side. */
|
|
43
|
+
code: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
url?: string;
|
|
46
|
+
/** Enable @mention suggestions. Default `true`. */
|
|
47
|
+
userHint?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface StromcomThreadProps extends StromcomThreadOptions {
|
|
51
|
+
className?: string;
|
|
52
|
+
style?: CSSProperties;
|
|
53
|
+
[key: `data-${string}`]: string | number | boolean | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function StromcomThread(props: StromcomThreadProps): JSX.Element;
|
|
57
|
+
|
|
58
|
+
export interface StromcomHomeProps {
|
|
59
|
+
className?: string;
|
|
60
|
+
style?: CSSProperties;
|
|
61
|
+
[key: `data-${string}`]: string | number | boolean | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function StromcomHome(props: StromcomHomeProps): JSX.Element;
|
|
65
|
+
|
|
66
|
+
export interface StromcomConfOptions {
|
|
67
|
+
notificationRenderer?: (notification: unknown) => unknown;
|
|
68
|
+
onNotification?: (count: number) => void;
|
|
69
|
+
pageCSSPath?: string;
|
|
70
|
+
notificationElementTargetElement?:
|
|
71
|
+
(() => HTMLElement | Promise<HTMLElement>) | HTMLElement | Promise<HTMLElement>;
|
|
72
|
+
notificationElementShowAlways?: boolean;
|
|
73
|
+
/** Icon position: 1=top-left, 2=top-right, 3=bottom-right, 4=bottom-left. */
|
|
74
|
+
notificationElementPosition?: 1 | 2 | 3 | 4;
|
|
75
|
+
notificationElementStyles?: Record<string, string | number | null>;
|
|
76
|
+
notificationElementCSSPath?: string;
|
|
77
|
+
notificationElementNoShadowRoot?: boolean;
|
|
78
|
+
homeBeforeRender?: () => void | Promise<void>;
|
|
79
|
+
notificationElementBeforeRender?: () => void | Promise<void>;
|
|
80
|
+
notificationElementAfterRender?: () => void;
|
|
81
|
+
/** Theme: `"stromcom-light"`, `"stromcom-dark"`, or `null` to follow browser preference. */
|
|
82
|
+
theme?: 'stromcom-light' | 'stromcom-dark' | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function StromcomConf(props: StromcomConfOptions): null;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { StromcomProvider, useStromcom } from './StromcomProvider.jsx';
|
|
2
|
+
export { StromcomUser } from './StromcomUser.jsx';
|
|
3
|
+
export { StromcomConf } from './StromcomConf.jsx';
|
|
4
|
+
export { StromcomThread } from './StromcomThread.jsx';
|
|
5
|
+
export { StromcomHome } from './StromcomHome.jsx';
|