Skip to content

Commit a075de8

Browse files
committed
add dedupe logic
1 parent 3be864d commit a075de8

File tree

4 files changed

+256
-145
lines changed

4 files changed

+256
-145
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/* eslint-disable jest/no-done-callback */
2+
import { sleep } from '@segment/analytics-core'
3+
import {
4+
MutationObservable,
5+
MutationObservableSettings,
6+
MutationObservableSubscriber,
7+
} from '../mutation-observer'
8+
9+
describe('MutationObservable', () => {
10+
let mutationObservable: MutationObservable
11+
let testButton: HTMLButtonElement
12+
let testInput: HTMLInputElement
13+
const subscribeFn = jest.fn() as jest.Mock<MutationObservableSubscriber>
14+
beforeEach(() => {
15+
document.body.innerHTML =
16+
'<div id="test-element" role="button" aria-pressed="false"></div>' +
17+
'<input id="test-input" value="" aria-foo="123" />'
18+
testButton = document.getElementById('test-element') as HTMLButtonElement
19+
testInput = document.getElementById('test-input') as HTMLInputElement
20+
})
21+
22+
afterEach(() => {
23+
mutationObservable.cleanup()
24+
})
25+
26+
it('should capture attribute changes', async () => {
27+
mutationObservable = new MutationObservable(
28+
new MutationObservableSettings({
29+
observedRoles: () => ['button'],
30+
observedAttributes: () => ['aria-pressed'],
31+
debounceMs: 500,
32+
})
33+
)
34+
35+
mutationObservable.subscribe(subscribeFn)
36+
testButton.setAttribute('aria-pressed', 'true')
37+
await sleep(0)
38+
39+
expect(subscribeFn).toHaveBeenCalledTimes(1)
40+
expect(subscribeFn).toHaveBeenCalledWith({
41+
element: testButton,
42+
attributes: { 'aria-pressed': 'true' },
43+
})
44+
})
45+
46+
it('should capture multiple attribute changes', async () => {
47+
mutationObservable = new MutationObservable(
48+
new MutationObservableSettings({
49+
observedRoles: () => ['button'],
50+
observedAttributes: () => ['aria-pressed'],
51+
debounceMs: 500,
52+
})
53+
)
54+
55+
mutationObservable.subscribe(subscribeFn)
56+
testButton.setAttribute('aria-pressed', 'true')
57+
await sleep(0)
58+
testButton.setAttribute('aria-pressed', 'false')
59+
await sleep(0)
60+
61+
expect(subscribeFn).toHaveBeenCalledTimes(2)
62+
expect(subscribeFn).toHaveBeenNthCalledWith(1, {
63+
element: testButton,
64+
attributes: { 'aria-pressed': 'true' },
65+
})
66+
expect(subscribeFn).toHaveBeenNthCalledWith(2, {
67+
element: testButton,
68+
attributes: { 'aria-pressed': 'false' },
69+
})
70+
})
71+
72+
it('should debounce attribute changes if they occur in text inputs', async () => {
73+
mutationObservable = new MutationObservable(
74+
new MutationObservableSettings({
75+
debounceMs: 100,
76+
})
77+
)
78+
mutationObservable.subscribe(subscribeFn)
79+
testInput.setAttribute('value', 'hello')
80+
await sleep(0)
81+
testInput.setAttribute('value', 'hello wor')
82+
await sleep(0)
83+
testInput.setAttribute('value', 'hello world')
84+
85+
await sleep(200)
86+
expect(subscribeFn).toHaveBeenCalledTimes(1)
87+
expect(subscribeFn).toHaveBeenCalledWith({
88+
element: testInput,
89+
attributes: { value: 'hello world' },
90+
})
91+
})
92+
93+
it('should handle multiple attributes changeing', async () => {
94+
mutationObservable = new MutationObservable(
95+
new MutationObservableSettings({
96+
debounceMs: 100,
97+
observedAttributes: (roles) => [...roles, 'aria-foo'],
98+
})
99+
)
100+
mutationObservable.subscribe(subscribeFn)
101+
testInput.setAttribute('value', 'hello')
102+
testInput.setAttribute('aria-foo', 'bar')
103+
await sleep(200)
104+
105+
expect(subscribeFn).toHaveBeenCalledTimes(1)
106+
expect(subscribeFn).toHaveBeenCalledWith({
107+
element: testInput,
108+
attributes: { value: 'hello', 'aria-foo': 'bar' },
109+
})
110+
})
111+
112+
it('should debounce if happening in the same tick', async () => {
113+
mutationObservable = new MutationObservable(
114+
new MutationObservableSettings({
115+
debounceMs: 50,
116+
})
117+
)
118+
mutationObservable.subscribe(subscribeFn)
119+
testInput.setAttribute('value', 'hello')
120+
testInput.setAttribute('value', 'hello wor')
121+
testInput.setAttribute('value', 'hello world')
122+
await sleep(100)
123+
124+
expect(subscribeFn).toHaveBeenCalledTimes(1)
125+
expect(subscribeFn).toHaveBeenCalledWith({
126+
element: testInput,
127+
attributes: { value: 'hello world' },
128+
})
129+
})
130+
131+
it('should not emit duplicate events', async () => {
132+
mutationObservable = new MutationObservable(
133+
new MutationObservableSettings({
134+
observedRoles: () => ['button'],
135+
observedAttributes: () => ['aria-pressed'],
136+
debounceMs: 0,
137+
})
138+
)
139+
140+
mutationObservable.subscribe(subscribeFn)
141+
testButton.setAttribute('aria-pressed', 'true')
142+
await sleep(0)
143+
testButton.setAttribute('aria-pressed', 'true')
144+
await sleep(0)
145+
146+
expect(subscribeFn).toHaveBeenCalledTimes(1)
147+
expect(subscribeFn).toHaveBeenCalledWith({
148+
element: testButton,
149+
attributes: { 'aria-pressed': 'true' },
150+
})
151+
})
152+
153+
it('should not emit duplicate events if overlapping', async () => {
154+
mutationObservable = new MutationObservable(
155+
new MutationObservableSettings({
156+
observedRoles: () => ['button'],
157+
observedAttributes: () => ['aria-pressed', 'aria-foo'],
158+
debounceMs: 0,
159+
})
160+
)
161+
162+
mutationObservable.subscribe(subscribeFn)
163+
testButton.setAttribute('aria-pressed', 'true')
164+
testButton.setAttribute('aria-foo', 'bar')
165+
await sleep(0)
166+
167+
testButton.setAttribute('aria-pressed', 'false')
168+
await sleep(50)
169+
170+
testButton.setAttribute('aria-pressed', 'false')
171+
await sleep(50)
172+
173+
testButton.setAttribute('aria-foo', 'bar')
174+
await sleep(50)
175+
176+
expect(subscribeFn).toHaveBeenNthCalledWith(1, {
177+
element: testButton,
178+
attributes: { 'aria-pressed': 'true', 'aria-foo': 'bar' },
179+
})
180+
181+
expect(subscribeFn).toHaveBeenNthCalledWith(2, {
182+
element: testButton,
183+
attributes: { 'aria-pressed': 'false' },
184+
})
185+
expect(subscribeFn).toHaveBeenCalledTimes(2)
186+
})
187+
188+
it('should not emit event for aria-selected=false', (done) => {
189+
mutationObservable = new MutationObservable(
190+
new MutationObservableSettings({
191+
observedRoles: () => ['button'],
192+
observedAttributes: () => ['aria-selected'],
193+
})
194+
)
195+
196+
mutationObservable.subscribe(() => {
197+
done.fail('Should not emit event for aria-selected=false')
198+
})
199+
200+
testButton.setAttribute('aria-selected', 'false')
201+
setTimeout(done, 1000) // Wait to ensure no event is emitted
202+
})
203+
})

packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,6 @@ export class MutationChangeGenerator implements SignalGenerator {
2525
}
2626

2727
register(emitter: SignalEmitter) {
28-
type NormalizedAttributes = { [attributeName: string]: string | null }
29-
const normalizeAttributes = (
30-
attributeMutation: AttributeChangedEvent
31-
): NormalizedAttributes => {
32-
const attributes =
33-
attributeMutation.attributes.reduce<NormalizedAttributes>(
34-
(acc, { attributeName, newValue }) => {
35-
acc[attributeName] = newValue
36-
return acc
37-
},
38-
{}
39-
)
40-
return attributes
41-
}
42-
4328
const callback = (ev: AttributeChangedEvent) => {
4429
const target = ev.element as HTMLElement | null
4530
if (!target || shouldIgnoreElement(target)) {
@@ -51,7 +36,7 @@ export class MutationChangeGenerator implements SignalGenerator {
5136
eventType: 'change',
5237
target: el,
5338
listener: 'mutation',
54-
change: normalizeAttributes(ev),
39+
change: ev.attributes,
5540
})
5641
)
5742
}

packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.test.ts

Lines changed: 0 additions & 108 deletions
This file was deleted.

0 commit comments

Comments
 (0)