sm-click-library-ui 0.0.614 → 0.0.616-dev.78
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/dist/{index-9nrggOJ-.js → index-xJ8KtM_J.js} +17082 -16924
- package/dist/sm-click-library-ui.es.js +183 -181
- package/dist/sm-click-library-ui.umd.js +22 -22
- package/dist/style.css +1 -1
- package/dist/tailwind.css +6 -7
- package/dist/{vue3-lottie.es-P3Zf2RlT.js → vue3-lottie.es-BHSMLcdu.js} +1 -1
- package/dist/{web-7rG00tDy.js → web-B4R0fcQj.js} +2 -2
- package/dist/{web-C7axtMyh.js → web-DDm7zRan.js} +2 -2
- package/package.json +2 -1
- package/src/test-utils/components/chat/audioPlayback.test.js +226 -128
package/dist/tailwind.css
CHANGED
|
@@ -1209,9 +1209,6 @@ video {
|
|
|
1209
1209
|
.h-\[200px\] {
|
|
1210
1210
|
height: 200px;
|
|
1211
1211
|
}
|
|
1212
|
-
.h-\[350px\] {
|
|
1213
|
-
height: 350px;
|
|
1214
|
-
}
|
|
1215
1212
|
.h-\[50vh\] {
|
|
1216
1213
|
height: 50vh;
|
|
1217
1214
|
}
|
|
@@ -1248,6 +1245,9 @@ video {
|
|
|
1248
1245
|
.max-h-\[224px\] {
|
|
1249
1246
|
max-height: 224px;
|
|
1250
1247
|
}
|
|
1248
|
+
.max-h-\[240px\] {
|
|
1249
|
+
max-height: 240px;
|
|
1250
|
+
}
|
|
1251
1251
|
.max-h-\[250px\] {
|
|
1252
1252
|
max-height: 250px;
|
|
1253
1253
|
}
|
|
@@ -2105,6 +2105,9 @@ video {
|
|
|
2105
2105
|
--tw-bg-opacity: 1;
|
|
2106
2106
|
background-color: rgb(var(--bg-base-300) / var(--tw-bg-opacity, 1));
|
|
2107
2107
|
}
|
|
2108
|
+
.bg-base-300\/30 {
|
|
2109
|
+
background-color: rgb(var(--bg-base-300) / 0.3);
|
|
2110
|
+
}
|
|
2108
2111
|
.bg-base-300\/70 {
|
|
2109
2112
|
background-color: rgb(var(--bg-base-300) / 0.7);
|
|
2110
2113
|
}
|
|
@@ -2173,10 +2176,6 @@ video {
|
|
|
2173
2176
|
.bg-gray-800\/80 {
|
|
2174
2177
|
background-color: rgb(31 41 55 / 0.8);
|
|
2175
2178
|
}
|
|
2176
|
-
.bg-gray-900 {
|
|
2177
|
-
--tw-bg-opacity: 1;
|
|
2178
|
-
background-color: rgb(17 24 39 / var(--tw-bg-opacity, 1));
|
|
2179
|
-
}
|
|
2180
2179
|
.bg-gray-900\/80 {
|
|
2181
2180
|
background-color: rgb(17 24 39 / 0.8);
|
|
2182
2181
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineComponent, ref, watchEffect, computed, watch, createElementBlock, openBlock, normalizeStyle } from "vue";
|
|
2
|
-
import { c as commonjsGlobal, g as getDefaultExportFromCjs } from "./index-
|
|
2
|
+
import { c as commonjsGlobal, g as getDefaultExportFromCjs } from "./index-xJ8KtM_J.js";
|
|
3
3
|
var lottie = { exports: {} };
|
|
4
4
|
(function(module, exports) {
|
|
5
5
|
typeof navigator < "u" && function(t, e) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { W as P, b as x } from "./index-
|
|
2
|
-
import { E } from "./web-
|
|
1
|
+
import { W as P, b as x } from "./index-xJ8KtM_J.js";
|
|
2
|
+
import { E } from "./web-DDm7zRan.js";
|
|
3
3
|
function m(w) {
|
|
4
4
|
const e = w.split("/").filter((t) => t !== "."), r = [];
|
|
5
5
|
return e.forEach((t) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as le, C as ge, g as me, W as we } from "./index-
|
|
1
|
+
import { r as le, C as ge, g as me, W as we } from "./index-xJ8KtM_J.js";
|
|
2
2
|
const F = {
|
|
3
3
|
RECORDING: "RECORDING",
|
|
4
4
|
PAUSED: "PAUSED",
|
|
@@ -58,7 +58,7 @@ var te;
|
|
|
58
58
|
n.UTF8 = "utf8", n.ASCII = "ascii", n.UTF16 = "utf16";
|
|
59
59
|
})(te || (te = {}));
|
|
60
60
|
const k = le("Filesystem", {
|
|
61
|
-
web: () => import("./web-
|
|
61
|
+
web: () => import("./web-B4R0fcQj.js").then((n) => new n.FilesystemWeb())
|
|
62
62
|
});
|
|
63
63
|
Ee();
|
|
64
64
|
const _e = le("BlobWriter");
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"dist",
|
|
6
6
|
"src/test-utils"
|
|
7
7
|
],
|
|
8
|
-
"version": "0.0.
|
|
8
|
+
"version": "0.0.616-dev.78",
|
|
9
9
|
"main": "dist/sm-click-library-ui.es.js",
|
|
10
10
|
"module": "dist/sm-click-library-ui.umd.js",
|
|
11
11
|
"style": "dist/style.css",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@chromatic-com/storybook": "1.6.1",
|
|
73
|
+
"@playwright/test": "1.63.0",
|
|
73
74
|
"@storybook/addon-essentials": "^8.2.3",
|
|
74
75
|
"@storybook/addon-interactions": "^8.2.3",
|
|
75
76
|
"@storybook/addon-links": "^8.2.3",
|
|
@@ -1,48 +1,65 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { mount } from "@vue/test-utils";
|
|
2
|
+
import { mount, flushPromises } from "@vue/test-utils";
|
|
3
3
|
import { createPinia, setActivePinia } from "pinia";
|
|
4
4
|
|
|
5
5
|
// Wavesurfer real precisa de fetch/decode. O dublê reproduz o que importa: toca o
|
|
6
6
|
// elemento recebido em `media`, e o `destroy` mata esse elemento mesmo sendo externo
|
|
7
7
|
// (como as versões < 7.4) — era isso que cortava o áudio na troca de conversa.
|
|
8
8
|
const instances = [];
|
|
9
|
+
const seeks = [];
|
|
10
|
+
const peakState = vi.hoisted(() => ({ pending: null }));
|
|
11
|
+
vi.mock("~/utils/audioPeaks", () => ({
|
|
12
|
+
resolveAudioPeaks: () => peakState.pending || Promise.resolve(null)
|
|
13
|
+
}));
|
|
14
|
+
// jsdom não calcula layout: o portão de viewport nunca intersectaria, e a onda é o objeto do teste.
|
|
15
|
+
vi.mock("~/utils/viewportGate", () => ({
|
|
16
|
+
onceInViewport: (el, callback) => {
|
|
17
|
+
callback();
|
|
18
|
+
return () => {};
|
|
19
|
+
}
|
|
20
|
+
}));
|
|
9
21
|
vi.mock("wavesurfer.js", () => ({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
22
|
+
default: {
|
|
23
|
+
create(options) {
|
|
24
|
+
const handlers = {};
|
|
25
|
+
const ws = {
|
|
26
|
+
options,
|
|
27
|
+
media: options.media,
|
|
28
|
+
loads: 0,
|
|
29
|
+
subscriptions: [],
|
|
30
|
+
timer: { start: () => {} },
|
|
31
|
+
initPlayerEvents: () => {},
|
|
32
|
+
on: (evt, cb) => {
|
|
33
|
+
(handlers[evt] ||= []).push(cb);
|
|
34
|
+
},
|
|
35
|
+
emit: (evt) => (handlers[evt] || []).forEach((cb) => cb()),
|
|
36
|
+
unAll: () => {},
|
|
37
|
+
destroy: () => {
|
|
38
|
+
ws.media.pause();
|
|
39
|
+
ws.media.removeAttribute("src");
|
|
40
|
+
},
|
|
41
|
+
getDuration: () => 12,
|
|
42
|
+
getCurrentTime: () => ws.media.currentTime,
|
|
43
|
+
setPlaybackRate: (rate) => {
|
|
44
|
+
ws.media.playbackRate = rate;
|
|
45
|
+
},
|
|
46
|
+
load: async (url) => {
|
|
47
|
+
ws.loads += 1;
|
|
48
|
+
ws.media.src = url;
|
|
49
|
+
ws.emit("ready");
|
|
50
|
+
},
|
|
51
|
+
seekTo: (progress) => {
|
|
52
|
+
seeks.push(progress);
|
|
53
|
+
},
|
|
54
|
+
playPause: () => {
|
|
55
|
+
if (ws.media.paused) ws.media.play();
|
|
56
|
+
else ws.media.pause();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
instances.push(ws);
|
|
60
|
+
return ws;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
46
63
|
}));
|
|
47
64
|
|
|
48
65
|
import MessageAudio from "~/components/chat/messages/MessageAudio.vue";
|
|
@@ -51,101 +68,182 @@ import { useAudioPlaybackStore } from "~/stores/audioPlaybackStore";
|
|
|
51
68
|
|
|
52
69
|
// jsdom não implementa play/pause de mídia.
|
|
53
70
|
beforeEach(() => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
peakState.pending = null;
|
|
72
|
+
instances.length = 0;
|
|
73
|
+
seeks.length = 0;
|
|
74
|
+
Object.defineProperty(HTMLMediaElement.prototype, "paused", {
|
|
75
|
+
configurable: true,
|
|
76
|
+
get() {
|
|
77
|
+
return this.__paused !== false;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
HTMLMediaElement.prototype.play = function () {
|
|
81
|
+
this.__paused = false;
|
|
82
|
+
this.dispatchEvent(new Event("play"));
|
|
83
|
+
return Promise.resolve();
|
|
84
|
+
};
|
|
85
|
+
HTMLMediaElement.prototype.pause = function () {
|
|
86
|
+
this.__paused = true;
|
|
87
|
+
this.dispatchEvent(new Event("pause"));
|
|
88
|
+
};
|
|
89
|
+
HTMLMediaElement.prototype.load = function () {};
|
|
90
|
+
setActivePinia(createPinia());
|
|
72
91
|
});
|
|
73
92
|
|
|
74
93
|
async function mountAudio(trackId = "m1") {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
const wrapper = mount(MessageAudio, {
|
|
95
|
+
props: {
|
|
96
|
+
audio: "https://example.test/a.mp3",
|
|
97
|
+
trackId,
|
|
98
|
+
trackMeta: { title: "Contato", chatId: 7, messageId: trackId }
|
|
99
|
+
},
|
|
100
|
+
attachTo: document.body
|
|
101
|
+
});
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
103
|
+
return wrapper;
|
|
85
104
|
}
|
|
86
105
|
|
|
87
106
|
describe("áudio da mensagem fora da árvore da bolha", () => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
107
|
+
it("não instala observers depois de desmontar durante a carga", async () => {
|
|
108
|
+
let finish;
|
|
109
|
+
peakState.pending = new Promise((resolve) => {
|
|
110
|
+
finish = resolve;
|
|
111
|
+
});
|
|
112
|
+
const wrapper = await mountAudio();
|
|
113
|
+
wrapper.unmount();
|
|
114
|
+
const observe = vi.spyOn(MutationObserver.prototype, "observe");
|
|
115
|
+
finish(null);
|
|
116
|
+
await flushPromises();
|
|
117
|
+
expect(observe).not.toHaveBeenCalled();
|
|
118
|
+
observe.mockRestore();
|
|
119
|
+
});
|
|
120
|
+
it("continua tocando depois que a bolha desmonta", async () => {
|
|
121
|
+
const store = useAudioPlaybackStore();
|
|
122
|
+
const wrapper = await mountAudio();
|
|
123
|
+
|
|
124
|
+
await wrapper.find("button.audio-play").trigger("click");
|
|
125
|
+
const el = store.activeElement();
|
|
126
|
+
expect(store.trackId).toBe("m1");
|
|
127
|
+
expect(store.isPlaying).toBe(true);
|
|
128
|
+
|
|
129
|
+
wrapper.unmount();
|
|
130
|
+
|
|
131
|
+
expect(store.activeElement()).toBe(el);
|
|
132
|
+
expect(el.paused).toBe(false);
|
|
133
|
+
expect(el.getAttribute("src")).not.toBeNull();
|
|
134
|
+
expect(store.isPlaying).toBe(true);
|
|
135
|
+
expect(store.isFloating).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("volta para a bolha sem cortar o áudio", async () => {
|
|
139
|
+
const store = useAudioPlaybackStore();
|
|
140
|
+
const first = await mountAudio();
|
|
141
|
+
await first.find("button.audio-play").trigger("click");
|
|
142
|
+
const el = store.activeElement();
|
|
143
|
+
el.currentTime = 5;
|
|
144
|
+
first.unmount();
|
|
145
|
+
|
|
146
|
+
const again = await mountAudio();
|
|
147
|
+
expect(store.activeElement()).toBe(el);
|
|
148
|
+
expect(el.paused).toBe(false);
|
|
149
|
+
expect(el.currentTime).toBe(5);
|
|
150
|
+
expect(store.isFloating).toBe(false);
|
|
151
|
+
// A onda remontada controla o mesmo elemento.
|
|
152
|
+
await again.find("button.audio-play").trigger("click");
|
|
153
|
+
expect(el.paused).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("faixa de controle aparece e comanda a reprodução", async () => {
|
|
157
|
+
const store = useAudioPlaybackStore();
|
|
158
|
+
const wrapper = await mountAudio();
|
|
159
|
+
await wrapper.find("button.audio-play").trigger("click");
|
|
160
|
+
wrapper.unmount();
|
|
161
|
+
|
|
162
|
+
const player = mount(AudioPlaybackBar);
|
|
163
|
+
expect(player.find(".apb-root").exists()).toBe(true);
|
|
164
|
+
expect(player.text()).toContain("Contato");
|
|
165
|
+
|
|
166
|
+
await player.find(".apb-play").trigger("click");
|
|
167
|
+
expect(store.isPlaying).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("outra faixa derruba a anterior", async () => {
|
|
171
|
+
const store = useAudioPlaybackStore();
|
|
172
|
+
const first = await mountAudio("m1");
|
|
173
|
+
const second = await mountAudio("m2");
|
|
174
|
+
|
|
175
|
+
await first.find("button.audio-play").trigger("click");
|
|
176
|
+
const firstEl = store.activeElement();
|
|
177
|
+
await second.find("button.audio-play").trigger("click");
|
|
178
|
+
|
|
179
|
+
expect(store.trackId).toBe("m2");
|
|
180
|
+
expect(firstEl.paused).toBe(true);
|
|
181
|
+
expect(store.activeElement().paused).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// jsdom não tem PointerEvent; os listeners escutam por nome do evento.
|
|
186
|
+
function pointer(type, target, clientX = 120) {
|
|
187
|
+
target.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, clientX, clientY: 20 }));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
describe("seek por gesto na onda", () => {
|
|
191
|
+
it("toque na onda não seeka por conta do componente", async () => {
|
|
192
|
+
const wrapper = await mountAudio();
|
|
193
|
+
const wave = wrapper.find(".ws-surface").element;
|
|
194
|
+
|
|
195
|
+
pointer("pointerdown", wave);
|
|
196
|
+
pointer("pointerup", wave);
|
|
197
|
+
|
|
198
|
+
// Quem decide se um toque virou clique ou arrasto é o wavesurfer, com threshold.
|
|
199
|
+
expect(seeks).toEqual([]);
|
|
200
|
+
wrapper.unmount();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("ponteiro cancelado pela rolagem não deixa o arrasto ligado", async () => {
|
|
204
|
+
const wrapper = await mountAudio();
|
|
205
|
+
const wave = wrapper.find(".ws-surface").element;
|
|
206
|
+
|
|
207
|
+
// O WebView assume o gesto como rolagem da conversa: vem pointercancel, não pointerup.
|
|
208
|
+
pointer("pointerdown", wave, 120);
|
|
209
|
+
pointer("pointercancel", wave, 120);
|
|
210
|
+
pointer("pointermove", window, 300);
|
|
211
|
+
pointer("pointermove", window, 60);
|
|
212
|
+
|
|
213
|
+
expect(seeks).toEqual([]);
|
|
214
|
+
wrapper.unmount();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("delega threshold e cancelamento de arrasto ao wavesurfer", async () => {
|
|
218
|
+
const store = useAudioPlaybackStore();
|
|
219
|
+
const bubble = await mountAudio();
|
|
220
|
+
expect(instances.at(-1).options.dragToSeek).toBe(true);
|
|
221
|
+
|
|
222
|
+
await bubble.find("button.audio-play").trigger("click");
|
|
223
|
+
bubble.unmount();
|
|
224
|
+
|
|
225
|
+
const player = mount(AudioPlaybackBar);
|
|
226
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
227
|
+
expect(store.isFloating).toBe(true);
|
|
228
|
+
expect(instances.at(-1).options.dragToSeek).toBe(true);
|
|
229
|
+
player.unmount();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("resize da janela não recarrega o áudio", async () => {
|
|
233
|
+
const wrapper = await mountAudio();
|
|
234
|
+
await wrapper.find("button.audio-play").trigger("click");
|
|
235
|
+
const ws = instances.at(-1);
|
|
236
|
+
ws.media.currentTime = 5;
|
|
237
|
+
|
|
238
|
+
window.dispatchEvent(new Event("resize"));
|
|
239
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
240
|
+
|
|
241
|
+
// O renderer do wavesurfer redesenha sozinho; recriar tudo baixava e decodificava de novo.
|
|
242
|
+
expect(instances).toHaveLength(1);
|
|
243
|
+
expect(ws.loads).toBe(1);
|
|
244
|
+
expect(seeks).toEqual([]);
|
|
245
|
+
expect(ws.media.currentTime).toBe(5);
|
|
246
|
+
expect(ws.media.paused).toBe(false);
|
|
247
|
+
wrapper.unmount();
|
|
248
|
+
});
|
|
151
249
|
});
|