makethisbetter 1.1.3 → 1.3.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 -62
- package/README.md +4 -4
- package/dist/context/frustration.d.ts +1 -1
- package/dist/context/frustration.d.ts.map +1 -1
- package/dist/i18n/de.d.ts +2 -0
- package/dist/i18n/de.d.ts.map +1 -1
- package/dist/i18n/en.d.ts +2 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/es.d.ts +2 -0
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/fr.d.ts +2 -0
- package/dist/i18n/fr.d.ts.map +1 -1
- package/dist/i18n/ja.d.ts +2 -0
- package/dist/i18n/ja.d.ts.map +1 -1
- package/dist/i18n/ko.d.ts +2 -0
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/zh-CN.d.ts +2 -0
- package/dist/i18n/zh-CN.d.ts.map +1 -1
- package/dist/makethisbetter.cjs.js +21 -35
- package/dist/makethisbetter.cjs.js.map +1 -1
- package/dist/makethisbetter.esm.js +178 -190
- package/dist/makethisbetter.esm.js.map +1 -1
- package/dist/makethisbetter.js +18 -32
- package/dist/makethisbetter.js.map +1 -1
- package/dist/popup/success.d.ts +4 -1
- package/dist/popup/success.d.ts.map +1 -1
- package/dist/record/session.cdn.test.d.ts +2 -0
- package/dist/record/session.cdn.test.d.ts.map +1 -0
- package/dist/record/session.d.ts.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/widget/controller.d.ts +0 -1
- package/dist/widget/controller.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/context/frustration.test.ts +2 -3
- package/src/context/frustration.ts +0 -1
- package/src/i18n/de.ts +2 -0
- package/src/i18n/en.ts +2 -0
- package/src/i18n/es.ts +2 -0
- package/src/i18n/fr.ts +2 -0
- package/src/i18n/ja.ts +2 -0
- package/src/i18n/ko.ts +2 -0
- package/src/i18n/zh-CN.ts +2 -0
- package/src/popup/success.test.ts +13 -0
- package/src/popup/success.ts +10 -3
- package/src/record/session.cdn.test.ts +89 -0
- package/src/record/session.ts +21 -7
- package/src/types.ts +2 -1
- package/src/widget/controller.test.ts +4 -1
- package/src/widget/controller.ts +15 -23
- package/src/widget/pet.test.ts +34 -32
package/dist/popup/success.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { ShadowContainer } from '../widget/shadow';
|
|
2
2
|
import type { I18nMessages } from '../i18n';
|
|
3
|
+
export interface SuccessCardOptions {
|
|
4
|
+
skipFollowup?: boolean;
|
|
5
|
+
}
|
|
3
6
|
export declare class SuccessCard {
|
|
4
7
|
private el;
|
|
5
|
-
constructor(shadow: ShadowContainer, messages: I18nMessages, onClose: () => void, onViewFeedback?: () => void);
|
|
8
|
+
constructor(shadow: ShadowContainer, messages: I18nMessages, onClose: () => void, onViewFeedback?: () => void, options?: SuccessCardOptions);
|
|
6
9
|
destroy(): void;
|
|
7
10
|
}
|
|
8
11
|
//# sourceMappingURL=success.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"success.d.ts","sourceRoot":"","sources":["../../src/popup/success.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAK3C,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAgB;gBAEd,MAAM,EAAE,eAAe,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,IAAI,EAAE,cAAc,CAAC,EAAE,MAAM,IAAI;
|
|
1
|
+
{"version":3,"file":"success.d.ts","sourceRoot":"","sources":["../../src/popup/success.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAK3C,MAAM,WAAW,kBAAkB;IACjC,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAgB;gBAEd,MAAM,EAAE,eAAe,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,IAAI,EAAE,cAAc,CAAC,EAAE,MAAM,IAAI,EAAE,OAAO,CAAC,EAAE,kBAAkB;IAiC3I,OAAO,IAAI,IAAI;CAGhB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.cdn.test.d.ts","sourceRoot":"","sources":["../../src/record/session.cdn.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/record/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAEvD,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAQzC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,UAAU,EAAE,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;CACjB;
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/record/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAEvD,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAQzC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,UAAU,EAAE,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAuDD,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,KAAK,CAA6C;IAC1D,OAAO,CAAC,YAAY,CAAyC;IAC7D,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,YAAY,CAAC,CAAY;IACjC,OAAO,CAAC,OAAO,CAAQ;gBAEX,MAAM,EAAE,eAAe,EAAE,YAAY,CAAC,EAAE,MAAM,IAAI;IAKxD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA2B5B,IAAI,IAAI,eAAe;IAavB,WAAW,IAAI,MAAM;IAKrB,OAAO,IAAI,IAAI;IAUf,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,eAAe;CAOxB"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export interface MakeThisBetterConfig {
|
|
2
2
|
projectKey: string;
|
|
3
|
-
entryMode?: 'button'
|
|
3
|
+
entryMode?: 'button';
|
|
4
4
|
locale?: string;
|
|
5
5
|
position?: 'left' | 'right';
|
|
6
6
|
theme?: 'light' | 'dark' | 'auto';
|
|
@@ -50,6 +50,7 @@ export interface FeedbackResponse {
|
|
|
50
50
|
status: string;
|
|
51
51
|
project_id: string;
|
|
52
52
|
ai_clarify_available?: boolean;
|
|
53
|
+
skip_followup?: boolean;
|
|
53
54
|
}
|
|
54
55
|
export interface ClarifyMessage {
|
|
55
56
|
role: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,QAAQ,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,QAAQ,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC3B,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAA;IACjC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;IACnC,IAAI,CAAC,EAAE;QACL,EAAE,CAAC,EAAE,MAAM,CAAA;QACX,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,IAAI,CAAC,EAAE,MAAM,CAAA;KACd,CAAA;CACF;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,KAAK,GAAG,MAAM,CAAA;IACpB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,EAAE,EAAE,MAAM,CAAA;IACV,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,cAAc,CAAC,EAAE;QACf,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;IACD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;IAC5C,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,IAAI,EAAE,OAAO,CAAA;CACd"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/widget/controller.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,oBAAoB,EAAc,MAAM,UAAU,CAAA;AAIhE,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,GAAG,CAA2B;IACtC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/widget/controller.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,oBAAoB,EAAc,MAAM,UAAU,CAAA;AAIhE,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,GAAG,CAA2B;IACtC,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,KAAK,CAA4B;IACzC,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,iBAAiB,CAAqC;IAC9D,OAAO,CAAC,SAAS,CAAW;IAC5B,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,mBAAmB,CAAmC;IAC9D,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,gBAAgB,CAAgC;gBAE5C,MAAM,EAAE,oBAAoB;IAcxC,OAAO,CAAC,OAAO;IAYf,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,wBAAwB;IAKhC,OAAO,CAAC,mBAAmB;IAgB3B,OAAO,CAAC,uBAAuB;IA8B/B,OAAO,CAAC,SAAS;YAcH,YAAY;IAwC1B,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,WAAW;IAiBnB,OAAO,CAAC,OAAO;IAgBf,OAAO,IAAI,IAAI;CAWhB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "makethisbetter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Make This Better JS Widget SDK — embed user feedback collection in any website",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/makethisbetter.cjs.js",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"type": "git",
|
|
71
71
|
"url": "https://github.com/makethisbetter/makethisbetter-js.git"
|
|
72
72
|
},
|
|
73
|
-
"license": "
|
|
73
|
+
"license": "MIT"
|
|
74
74
|
}
|
|
@@ -112,15 +112,14 @@ describe('FrustrationDetector', () => {
|
|
|
112
112
|
document.body.removeChild(host)
|
|
113
113
|
})
|
|
114
114
|
|
|
115
|
-
it('does not directly emit console errors (only counts them as
|
|
115
|
+
it('does not directly emit console errors (only counts them as dead-click input)', () => {
|
|
116
116
|
makeDetector()
|
|
117
117
|
|
|
118
118
|
if (window.onerror) {
|
|
119
119
|
;(window.onerror as Function)('Test error', 'test.js', 1, 1, new Error('Test error'))
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
expect(errorEvents.length).toBe(0)
|
|
122
|
+
expect(events.length).toBe(0)
|
|
124
123
|
})
|
|
125
124
|
|
|
126
125
|
it('stops listening after stop()', () => {
|
package/src/i18n/de.ts
CHANGED
|
@@ -22,6 +22,8 @@ export default {
|
|
|
22
22
|
success: {
|
|
23
23
|
title: 'Vielen Dank!',
|
|
24
24
|
message: 'Ihr Feedback wurde empfangen.',
|
|
25
|
+
title_no_ai: 'Gesendet!',
|
|
26
|
+
message_no_ai: 'Wir kümmern uns darum. Sie werden benachrichtigt, sobald es gelöst ist.',
|
|
25
27
|
view_feedback: 'Mein Feedback ansehen',
|
|
26
28
|
close: 'Schließen',
|
|
27
29
|
},
|
package/src/i18n/en.ts
CHANGED
|
@@ -22,6 +22,8 @@ export default {
|
|
|
22
22
|
success: {
|
|
23
23
|
title: 'Thanks!',
|
|
24
24
|
message: 'Your feedback has been received.',
|
|
25
|
+
title_no_ai: 'Sent — thanks!',
|
|
26
|
+
message_no_ai: "We'll take it from here. You'll hear back when it's resolved.",
|
|
25
27
|
view_feedback: 'View my feedback',
|
|
26
28
|
close: 'Close',
|
|
27
29
|
},
|
package/src/i18n/es.ts
CHANGED
|
@@ -22,6 +22,8 @@ export default {
|
|
|
22
22
|
success: {
|
|
23
23
|
title: '¡Gracias!',
|
|
24
24
|
message: 'Tu comentario ha sido recibido.',
|
|
25
|
+
title_no_ai: '¡Enviado!',
|
|
26
|
+
message_no_ai: 'Nos encargamos. Te avisaremos cuando se resuelva.',
|
|
25
27
|
view_feedback: 'Ver mis comentarios',
|
|
26
28
|
close: 'Cerrar',
|
|
27
29
|
},
|
package/src/i18n/fr.ts
CHANGED
|
@@ -22,6 +22,8 @@ export default {
|
|
|
22
22
|
success: {
|
|
23
23
|
title: 'Merci !',
|
|
24
24
|
message: 'Votre avis a bien été reçu.',
|
|
25
|
+
title_no_ai: 'Envoyé !',
|
|
26
|
+
message_no_ai: "On s'en occupe. Vous serez informé(e) dès que ce sera résolu.",
|
|
25
27
|
view_feedback: 'Voir mes avis',
|
|
26
28
|
close: 'Fermer',
|
|
27
29
|
},
|
package/src/i18n/ja.ts
CHANGED
package/src/i18n/ko.ts
CHANGED
package/src/i18n/zh-CN.ts
CHANGED
|
@@ -72,4 +72,17 @@ describe('SuccessCard', () => {
|
|
|
72
72
|
|
|
73
73
|
shadow.destroy()
|
|
74
74
|
})
|
|
75
|
+
|
|
76
|
+
it('shows depleted copy when skipFollowup is true', () => {
|
|
77
|
+
const shadow = new ShadowContainer()
|
|
78
|
+
const onClose = vi.fn()
|
|
79
|
+
const messages = getMessages('en')
|
|
80
|
+
const card = new SuccessCard(shadow, messages, onClose, undefined, { skipFollowup: true })
|
|
81
|
+
|
|
82
|
+
expect(shadow.root.querySelector('.mtb-success-title')?.textContent).toBe(messages.success.title_no_ai)
|
|
83
|
+
expect(shadow.root.querySelector('.mtb-success-msg')?.textContent).toBe(messages.success.message_no_ai)
|
|
84
|
+
|
|
85
|
+
card.destroy()
|
|
86
|
+
shadow.destroy()
|
|
87
|
+
})
|
|
75
88
|
})
|
package/src/popup/success.ts
CHANGED
|
@@ -4,14 +4,21 @@ import { BRAND_COLOR } from '../styles'
|
|
|
4
4
|
|
|
5
5
|
const SUCCESS_AUTO_CLOSE_MS = 5000
|
|
6
6
|
|
|
7
|
+
export interface SuccessCardOptions {
|
|
8
|
+
skipFollowup?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
export class SuccessCard {
|
|
8
12
|
private el: HTMLDivElement
|
|
9
13
|
|
|
10
|
-
constructor(shadow: ShadowContainer, messages: I18nMessages, onClose: () => void, onViewFeedback?: () => void) {
|
|
14
|
+
constructor(shadow: ShadowContainer, messages: I18nMessages, onClose: () => void, onViewFeedback?: () => void, options?: SuccessCardOptions) {
|
|
11
15
|
this.el = shadow.el<HTMLDivElement>('div', 'mtb-success')
|
|
12
16
|
this.el.style.right = '20px'
|
|
13
17
|
this.el.style.bottom = '20px'
|
|
14
18
|
|
|
19
|
+
const title = options?.skipFollowup ? messages.success.title_no_ai : messages.success.title
|
|
20
|
+
const msg = options?.skipFollowup ? messages.success.message_no_ai : messages.success.message
|
|
21
|
+
|
|
15
22
|
const viewLink = onViewFeedback
|
|
16
23
|
? `<button class="mtb-view-feedback-link">${messages.success.view_feedback}</button>`
|
|
17
24
|
: ''
|
|
@@ -22,8 +29,8 @@ export class SuccessCard {
|
|
|
22
29
|
<polyline points="20 6 9 17 4 12"/>
|
|
23
30
|
</svg>
|
|
24
31
|
</div>
|
|
25
|
-
<div class="mtb-success-title">${
|
|
26
|
-
<div class="mtb-success-msg">${
|
|
32
|
+
<div class="mtb-success-title">${title}</div>
|
|
33
|
+
<div class="mtb-success-msg">${msg}</div>
|
|
27
34
|
${viewLink}
|
|
28
35
|
<button class="mtb-close-link">${messages.success.close}</button>
|
|
29
36
|
`
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import type { RecordSession } from './session'
|
|
3
|
+
import type { ShadowContainer } from '../widget/shadow'
|
|
4
|
+
|
|
5
|
+
const mockStopFn = vi.fn()
|
|
6
|
+
const mockRecord = Object.assign(
|
|
7
|
+
vi.fn(() => mockStopFn),
|
|
8
|
+
{ addCustomEvent: vi.fn() },
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// Force the bundler import path to fail so start() exercises the CDN fallback,
|
|
12
|
+
// mirroring the IIFE/CDN build where the bare specifier is unresolvable.
|
|
13
|
+
vi.mock('rrweb', () => {
|
|
14
|
+
throw new Error('rrweb not installed')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
function createMockShadow(): ShadowContainer {
|
|
18
|
+
return {
|
|
19
|
+
el: <T extends HTMLElement>(tag: string, className?: string): T => {
|
|
20
|
+
const el = document.createElement(tag) as T
|
|
21
|
+
if (className) el.className = className
|
|
22
|
+
return el
|
|
23
|
+
},
|
|
24
|
+
append: vi.fn(),
|
|
25
|
+
remove: vi.fn(),
|
|
26
|
+
root: document.createElement('div') as unknown as ShadowRoot,
|
|
27
|
+
destroy: vi.fn(),
|
|
28
|
+
} as unknown as ShadowContainer
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// session.ts caches the rrweb loader promise at module level, so each test
|
|
32
|
+
// re-imports a fresh module instance.
|
|
33
|
+
async function freshSession(): Promise<RecordSession> {
|
|
34
|
+
vi.resetModules()
|
|
35
|
+
const { RecordSession: Cls } = await import('./session')
|
|
36
|
+
return new Cls(createMockShadow())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('RecordSession CDN fallback', () => {
|
|
40
|
+
let session: RecordSession | null = null
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.clearAllMocks()
|
|
44
|
+
delete (globalThis as Record<string, unknown>).rrwebRecord
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
session?.destroy()
|
|
49
|
+
session = null
|
|
50
|
+
vi.restoreAllMocks()
|
|
51
|
+
delete (globalThis as Record<string, unknown>).rrwebRecord
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('loads @rrweb/record UMD build via classic script tag', async () => {
|
|
55
|
+
const appended: HTMLScriptElement[] = []
|
|
56
|
+
vi.spyOn(document.head, 'appendChild').mockImplementation((node) => {
|
|
57
|
+
const script = node as HTMLScriptElement
|
|
58
|
+
appended.push(script)
|
|
59
|
+
// Simulate the UMD script executing: it sets the global, then load fires
|
|
60
|
+
;(globalThis as Record<string, unknown>).rrwebRecord = { record: mockRecord }
|
|
61
|
+
script.onload?.(new Event('load'))
|
|
62
|
+
return node
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
session = await freshSession()
|
|
66
|
+
await session.start()
|
|
67
|
+
|
|
68
|
+
expect(appended).toHaveLength(1)
|
|
69
|
+
const script = appended[0]
|
|
70
|
+
// rrweb 2.x publishes no UMD build in the `rrweb` package — its dist files
|
|
71
|
+
// are ESM and throw "Unexpected token 'export'" in a classic script tag.
|
|
72
|
+
// The record-only UMD lives in @rrweb/record's static /umd/ directory.
|
|
73
|
+
expect(script.src).toContain('/npm/@rrweb/record@')
|
|
74
|
+
expect(script.src).toContain('/umd/record.min.js')
|
|
75
|
+
expect(script.integrity).toMatch(/^sha384-/)
|
|
76
|
+
expect(script.crossOrigin).toBe('anonymous')
|
|
77
|
+
expect(mockRecord).toHaveBeenCalledOnce()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('rejects when the CDN script exposes no rrwebRecord global', async () => {
|
|
81
|
+
vi.spyOn(document.head, 'appendChild').mockImplementation((node) => {
|
|
82
|
+
;(node as HTMLScriptElement).onload?.(new Event('load'))
|
|
83
|
+
return node
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
session = await freshSession()
|
|
87
|
+
await expect(session.start()).rejects.toThrow(/not found after script load/)
|
|
88
|
+
})
|
|
89
|
+
})
|
package/src/record/session.ts
CHANGED
|
@@ -18,8 +18,12 @@ const MAX_DURATION_S = 60
|
|
|
18
18
|
// floating tag or unverified content would be a supply-chain hole. Bumping
|
|
19
19
|
// the version requires recomputing the hash from the exact CDN file:
|
|
20
20
|
// curl -sL <url> | openssl dgst -sha384 -binary | openssl base64 -A
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// Must stay on @rrweb/record's static /umd/ build: the `rrweb` package ships
|
|
22
|
+
// no UMD in 2.x (its dist files are ESM and throw "Unexpected token 'export'"
|
|
23
|
+
// in a classic script tag), and jsDelivr's auto-minified files are unsafe for
|
|
24
|
+
// SRI because their hash changes when jsDelivr upgrades its minifier.
|
|
25
|
+
const RRWEB_CDN = 'https://cdn.jsdelivr.net/npm/@rrweb/record@2.1.0/umd/record.min.js'
|
|
26
|
+
const RRWEB_SRI = 'sha384-MrD66HBNSykaP2N95+6hQCFlF5oH2tvL3TD/zyvHNkP/sAFWZx98DX9MEDy8MdVT'
|
|
23
27
|
|
|
24
28
|
let rrwebLoaded: Promise<RRWebRecordFn> | null = null
|
|
25
29
|
|
|
@@ -34,18 +38,28 @@ async function loadViaImport(): Promise<RRWebRecordFn> {
|
|
|
34
38
|
return mod.record as unknown as RRWebRecordFn
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
interface RRWebGlobals {
|
|
42
|
+
rrwebRecord?: { record: RRWebRecordFn }
|
|
43
|
+
rrweb?: { record: RRWebRecordFn }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function findGlobalRecord(): RRWebRecordFn | undefined {
|
|
47
|
+
const win = globalThis as unknown as Window & RRWebGlobals
|
|
48
|
+
return win.rrwebRecord?.record ?? win.rrweb?.record
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
function loadViaCdn(): Promise<RRWebRecordFn> {
|
|
38
52
|
return new Promise((resolve, reject) => {
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
53
|
+
const existing = findGlobalRecord()
|
|
54
|
+
if (existing) { resolve(existing); return }
|
|
41
55
|
const script = document.createElement('script')
|
|
42
56
|
script.src = RRWEB_CDN
|
|
43
57
|
script.integrity = RRWEB_SRI
|
|
44
58
|
script.crossOrigin = 'anonymous'
|
|
45
59
|
script.onload = () => {
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
else reject(new Error('rrweb global not found after script load'))
|
|
60
|
+
const record = findGlobalRecord()
|
|
61
|
+
if (record) resolve(record)
|
|
62
|
+
else reject(new Error('rrweb record global not found after script load'))
|
|
49
63
|
}
|
|
50
64
|
script.onerror = () => reject(new Error('Failed to load rrweb from CDN'))
|
|
51
65
|
document.head.appendChild(script)
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export interface MakeThisBetterConfig {
|
|
2
2
|
projectKey: string
|
|
3
|
-
entryMode?: 'button'
|
|
3
|
+
entryMode?: 'button'
|
|
4
4
|
locale?: string
|
|
5
5
|
position?: 'left' | 'right'
|
|
6
6
|
theme?: 'light' | 'dark' | 'auto'
|
|
@@ -53,6 +53,7 @@ export interface FeedbackResponse {
|
|
|
53
53
|
status: string
|
|
54
54
|
project_id: string
|
|
55
55
|
ai_clarify_available?: boolean
|
|
56
|
+
skip_followup?: boolean
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export interface ClarifyMessage {
|
|
@@ -92,9 +92,12 @@ describe('MakeThisBetter widget flow', () => {
|
|
|
92
92
|
shadow.querySelector<HTMLButtonElement>('.mtb-submit-btn')!.click()
|
|
93
93
|
|
|
94
94
|
await vi.waitFor(() => {
|
|
95
|
-
expect(shadow.querySelector('.mtb-success-title')?.textContent).toBe('
|
|
95
|
+
expect(shadow.querySelector('.mtb-success-title')?.textContent).toBe('Sent — thanks!')
|
|
96
96
|
}, { timeout: 5000 })
|
|
97
97
|
|
|
98
|
+
expect(shadow.querySelector('.mtb-success-msg')?.textContent).toBe(
|
|
99
|
+
"We'll take it from here. You'll hear back when it's resolved."
|
|
100
|
+
)
|
|
98
101
|
expect(shadow.querySelector('.mtb-clarify')).toBeNull()
|
|
99
102
|
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
|
100
103
|
})
|
package/src/widget/controller.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ShadowContainer } from './shadow'
|
|
2
2
|
import { FeedbackTab } from './tab'
|
|
3
|
-
import { PetEntry } from './pet'
|
|
4
3
|
import { FrustrationPromptCard } from './frustration-prompt'
|
|
5
4
|
import { AnnotationToolbar } from '../annotate/toolbar'
|
|
6
5
|
import type { ToolbarMode } from '../annotate/toolbar'
|
|
@@ -8,6 +7,7 @@ import { AnnotationSession } from '../annotate/session'
|
|
|
8
7
|
import { CommentPopup } from '../popup/comment'
|
|
9
8
|
import { ClarifyCard } from '../popup/clarify'
|
|
10
9
|
import { SuccessCard } from '../popup/success'
|
|
10
|
+
import type { SuccessCardOptions } from '../popup/success'
|
|
11
11
|
import { captureScreenshot } from '../screenshot/capture'
|
|
12
12
|
import { ConsoleErrorCollector } from '../context/console'
|
|
13
13
|
import { FrustrationDetector } from '../context/frustration'
|
|
@@ -24,7 +24,6 @@ export class WidgetController {
|
|
|
24
24
|
private config: MakeThisBetterConfig
|
|
25
25
|
private shadow!: ShadowContainer
|
|
26
26
|
private tab: FeedbackTab | null = null
|
|
27
|
-
private pet: PetEntry | null = null
|
|
28
27
|
private toolbar: AnnotationToolbar | null = null
|
|
29
28
|
private session: AnnotationSession | null = null
|
|
30
29
|
private popup: CommentPopup | null = null
|
|
@@ -56,17 +55,13 @@ export class WidgetController {
|
|
|
56
55
|
private buildUI(): void {
|
|
57
56
|
this.shadow = new ShadowContainer(this.config.theme ?? 'auto')
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
this.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
() => this.handleTabClick(),
|
|
67
|
-
this.config.tabText,
|
|
68
|
-
)
|
|
69
|
-
}
|
|
58
|
+
this.tab = new FeedbackTab(
|
|
59
|
+
this.shadow,
|
|
60
|
+
this.messages,
|
|
61
|
+
this.config.position ?? 'right',
|
|
62
|
+
() => this.handleTabClick(),
|
|
63
|
+
this.config.tabText,
|
|
64
|
+
)
|
|
70
65
|
}
|
|
71
66
|
|
|
72
67
|
private handleTabClick(): void {
|
|
@@ -98,7 +93,6 @@ export class WidgetController {
|
|
|
98
93
|
private enterAnnotationMode(): void {
|
|
99
94
|
this.mode = 'annotating'
|
|
100
95
|
this.tab?.setActive(true)
|
|
101
|
-
this.pet?.setActive(true)
|
|
102
96
|
|
|
103
97
|
this.toolbar = new AnnotationToolbar(
|
|
104
98
|
this.shadow,
|
|
@@ -122,8 +116,9 @@ export class WidgetController {
|
|
|
122
116
|
if (!this.recordingManager) return
|
|
123
117
|
this.recordingManager.stop()
|
|
124
118
|
this.showPopup(window.innerWidth / 2, window.innerHeight / 2, this.messages.record.timer_label)
|
|
125
|
-
}).catch(() => {
|
|
119
|
+
}).catch((error) => {
|
|
126
120
|
// rrweb failed to load (network / SRI mismatch) — fall back to markup
|
|
121
|
+
console.warn('[MakeThisBetter] Screen recording unavailable, falling back to markup:', error)
|
|
127
122
|
if (this.mode !== 'recording') return
|
|
128
123
|
this.toolbar?.setMode('markup')
|
|
129
124
|
this.handleToolbarModeChange('markup')
|
|
@@ -183,8 +178,9 @@ export class WidgetController {
|
|
|
183
178
|
this.session = null
|
|
184
179
|
this.recordingManager?.destroy()
|
|
185
180
|
this.recordingManager = null
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
const skipFollowup = feedback.skip_followup === true || feedback.ai_clarify_available === false
|
|
182
|
+
if (skipFollowup) {
|
|
183
|
+
this.showSuccess(feedback.project_id, { skipFollowup: true })
|
|
188
184
|
} else {
|
|
189
185
|
this.showClarify(feedback.id, feedback.project_id)
|
|
190
186
|
}
|
|
@@ -197,7 +193,6 @@ export class WidgetController {
|
|
|
197
193
|
private showClarify(feedbackId: string, projectId: string): void {
|
|
198
194
|
this.mode = 'idle'
|
|
199
195
|
this.tab?.setActive(false)
|
|
200
|
-
this.pet?.setActive(false)
|
|
201
196
|
this.clarify = new ClarifyCard(this.shadow, {
|
|
202
197
|
feedbackId,
|
|
203
198
|
apiClient: this.apiClient,
|
|
@@ -210,10 +205,9 @@ export class WidgetController {
|
|
|
210
205
|
})
|
|
211
206
|
}
|
|
212
207
|
|
|
213
|
-
private showSuccess(projectId: string): void {
|
|
208
|
+
private showSuccess(projectId: string, options?: SuccessCardOptions): void {
|
|
214
209
|
this.mode = 'idle'
|
|
215
210
|
this.tab?.setActive(false)
|
|
216
|
-
this.pet?.setActive(false)
|
|
217
211
|
const baseUrl = this.config.apiUrl
|
|
218
212
|
? this.config.apiUrl.replace(/\/api\/v1\/?$/, '')
|
|
219
213
|
: 'https://makethisbetter.dev'
|
|
@@ -225,13 +219,12 @@ export class WidgetController {
|
|
|
225
219
|
}, hasIdentity ? async () => {
|
|
226
220
|
const token = await this.apiClient.resolveUserToken()
|
|
227
221
|
if (token) window.open(`${boardUrl}?identity=${encodeURIComponent(token)}`, '_blank')
|
|
228
|
-
} : undefined)
|
|
222
|
+
} : undefined, options)
|
|
229
223
|
}
|
|
230
224
|
|
|
231
225
|
private exitAll(): void {
|
|
232
226
|
this.mode = 'idle'
|
|
233
227
|
this.tab?.setActive(false)
|
|
234
|
-
this.pet?.setActive(false)
|
|
235
228
|
this.toolbar?.destroy()
|
|
236
229
|
this.toolbar = null
|
|
237
230
|
this.session?.destroy()
|
|
@@ -253,7 +246,6 @@ export class WidgetController {
|
|
|
253
246
|
this.clarify?.destroy()
|
|
254
247
|
this.success?.destroy()
|
|
255
248
|
this.tab?.destroy()
|
|
256
|
-
this.pet?.destroy()
|
|
257
249
|
this.recordingManager?.destroy()
|
|
258
250
|
this.shadow.destroy()
|
|
259
251
|
}
|
package/src/widget/pet.test.ts
CHANGED
|
@@ -1,56 +1,58 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { PetEntry } from './pet'
|
|
3
|
+
import { ShadowContainer } from './shadow'
|
|
4
|
+
import { getMessages } from '../i18n'
|
|
2
5
|
import { MakeThisBetter } from '../index'
|
|
3
6
|
|
|
4
7
|
vi.mock('html-to-image', () => ({
|
|
5
8
|
toPng: vi.fn(async () => 'data:image/png;base64,AA=='),
|
|
6
9
|
}))
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
// PetEntry is kept in the repo for the phase-2 pet mode but is not wired into
|
|
12
|
+
// the widget build, so it is unit-tested directly instead of via init().
|
|
13
|
+
describe('PetEntry', () => {
|
|
14
|
+
let shadow: ShadowContainer
|
|
15
|
+
|
|
9
16
|
afterEach(() => {
|
|
10
|
-
|
|
11
|
-
vi.restoreAllMocks()
|
|
17
|
+
shadow?.destroy()
|
|
12
18
|
document.body.innerHTML = ''
|
|
13
19
|
})
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
apiUrl: 'https://api.example.com/api/v1',
|
|
20
|
-
})
|
|
21
|
+
function makePet(onClick: () => void = () => {}) {
|
|
22
|
+
shadow = new ShadowContainer('light')
|
|
23
|
+
return new PetEntry(shadow, getMessages('en'), onClick)
|
|
24
|
+
}
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
})
|
|
26
|
+
function shadowRoot() {
|
|
27
|
+
return document.getElementById('mtb-widget-host')!.shadowRoot!
|
|
28
|
+
}
|
|
26
29
|
|
|
27
|
-
it('
|
|
28
|
-
|
|
29
|
-
projectKey: 'proj_test',
|
|
30
|
-
entryMode: 'pet',
|
|
31
|
-
apiUrl: 'https://api.example.com/api/v1',
|
|
32
|
-
})
|
|
30
|
+
it('renders the pet element with a speech bubble', () => {
|
|
31
|
+
makePet()
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
const bubble =
|
|
33
|
+
expect(shadowRoot().querySelector('.mtb-pet')).toBeTruthy()
|
|
34
|
+
const bubble = shadowRoot().querySelector('.mtb-pet-bubble')
|
|
36
35
|
expect(bubble).toBeTruthy()
|
|
37
36
|
expect(bubble!.textContent).toBe('Need help?')
|
|
38
37
|
})
|
|
39
38
|
|
|
40
|
-
it('
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
entryMode: 'pet',
|
|
44
|
-
apiUrl: 'https://api.example.com/api/v1',
|
|
45
|
-
})
|
|
39
|
+
it('invokes the click callback on click', () => {
|
|
40
|
+
const onClick = vi.fn()
|
|
41
|
+
makePet(onClick)
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
shadowRoot().querySelector<HTMLDivElement>('.mtb-pet')!.click()
|
|
44
|
+
expect(onClick).toHaveBeenCalledOnce()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('Widget entry', () => {
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
MakeThisBetter.destroy()
|
|
51
|
+
vi.restoreAllMocks()
|
|
52
|
+
document.body.innerHTML = ''
|
|
51
53
|
})
|
|
52
54
|
|
|
53
|
-
it('renders tab
|
|
55
|
+
it('renders the tab entry and never the pet', () => {
|
|
54
56
|
MakeThisBetter.init({
|
|
55
57
|
projectKey: 'proj_test',
|
|
56
58
|
apiUrl: 'https://api.example.com/api/v1',
|